// /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'), // === ИЗМЕНЕНИЕ: Добавлены элементы таймера === turnTimerContainer: document.getElementById('turn-timer-container'), turnTimerSpan: document.getElementById('turn-timer') // === КОНЕЦ ИЗМЕНЕНИЯ === }, 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]; const config = window.GAME_CONFIG || {}; if (!elements || !elements.hpFill || !elements.hpText || !elements.resourceFill || !elements.resourceText || !elements.status || !fighterState || !fighterBaseStats) { if (elements) { if(elements.name) elements.name.innerHTML = (panelRole === 'player') ? ' Ожидание данных...' : ' Ожидание игрока...'; 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-elena'; } // Используем специфичный класс для цвета else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; } else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-balard'; } // Для Баларда тоже специфичный let nameHtml = ` ${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}`; 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)'; } 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)'; } let glowColorVar = 'rgba(0, 0, 0, 0.4)'; if (fighterBaseStats.characterKey === 'elena') glowColorVar = 'var(--panel-glow-player)'; else if (fighterBaseStats.characterKey === 'almagest') glowColorVar = 'var(--panel-glow-almagest)'; // Отдельный цвет для Альмагест else if (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)`; } } function generateEffectsHTML(effectsArray) { const config = window.GAME_CONFIG || {}; if (!effectsArray || effectsArray.length === 0) return 'Нет'; 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) 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 || eff.type === config.ACTION_TYPE_HEAL) effectClasses += ' effect-buff'; else effectClasses += ' effect-info'; return `${displayText}`; }).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, [config.ACTION_TYPE_DEBUFF]: 4, [config.ACTION_TYPE_DISABLE]: 5 }; const sortEffects = (a, b) => { let orderA = typeOrder[a.type] || 99; if (a.grantsBlock) orderA = typeOrder.grantsBlock; if (a.isFullSilence || a.id.startsWith('playerSilencedOn_')) orderA = typeOrder[config.ACTION_TYPE_DISABLE]; 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); }; if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList && myState && myState.activeEffects) { const myBuffs = []; const myDebuffs = []; myState.activeEffects.forEach(e => { const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL; 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 myDebuffs.push(e); }); 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 = 'Нет'; } if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList && opponentState && opponentState.activeEffects) { const opponentBuffs = []; const opponentDebuffs = []; opponentState.activeEffects.forEach(e => { const isBuff = e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || e.type === config.ACTION_TYPE_HEAL; 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 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 = 'Нет'; } } // === ИЗМЕНЕНИЕ: Новая функция для обновления таймера === /** * Обновляет отображение таймера хода. * @param {number|null} remainingTimeMs - Оставшееся время в миллисекундах, или null если таймер неактивен. * @param {boolean} isCurrentPlayerActualTurn - Флаг, является ли текущий ход ходом этого клиента. * @param {string} gameMode - Режим игры ('ai' или 'pvp'). */ function updateTurnTimerDisplay(remainingTimeMs, isCurrentPlayerActualTurn, gameMode) { const timerSpan = uiElements.controls.turnTimerSpan; const timerContainer = uiElements.controls.turnTimerContainer; const config = window.GAME_CONFIG || {}; if (!timerSpan || !timerContainer) return; if (window.gameState && window.gameState.isGameOver) { timerContainer.style.display = 'block'; // Может быть 'flex' или другой, в зависимости от CSS timerSpan.textContent = 'Конец'; timerSpan.classList.remove('low-time'); return; } if (remainingTimeMs === null || remainingTimeMs === undefined) { timerContainer.style.display = 'block'; timerSpan.classList.remove('low-time'); if (gameMode === 'ai' && !isCurrentPlayerActualTurn) { // Предполагаем, что если не ход игрока в AI, то ход AI timerSpan.textContent = 'Ход ИИ'; } else if (gameMode === 'pvp' && !isCurrentPlayerActualTurn) { timerSpan.textContent = 'Ход оппонента'; } else { // Ход текущего игрока, но нет времени (например, ожидание первого хода) timerSpan.textContent = '--'; } } else { timerContainer.style.display = 'block'; const seconds = Math.ceil(remainingTimeMs / 1000); timerSpan.textContent = `0:${seconds < 10 ? '0' : ''}${seconds}`; if (seconds <= 10 && isCurrentPlayerActualTurn) { // Предупреждение только если это мой ход timerSpan.classList.add('low-time'); } else { timerSpan.classList.remove('low-time'); } } } // === КОНЕЦ ИЗМЕНЕНИЯ === function updateUI() { const currentGameState = window.gameState; const gameDataGlobal = window.gameData; const configGlobal = window.GAME_CONFIG; const myActualPlayerId = window.myPlayerId; if (!currentGameState || !gameDataGlobal || !configGlobal || !myActualPlayerId) { updateFighterPanelUI('player', null, null, true); updateFighterPanelUI('opponent', null, null, false); if(uiElements.gameHeaderTitle) uiElements.gameHeaderTitle.innerHTML = `Ожидание данных...`; 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 = '

Загрузка способностей...

'; // === ИЗМЕНЕНИЕ: Сбрасываем таймер, если нет данных === if (uiElements.controls.turnTimerContainer) uiElements.controls.turnTimerContainer.style.display = 'none'; if (uiElements.controls.turnTimerSpan) { uiElements.controls.turnTimerSpan.textContent = '--'; uiElements.controls.turnTimerSpan.classList.remove('low-time'); } // === КОНЕЦ ИЗМЕНЕНИЯ === return; } if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) { console.warn("updateUI: Некоторые базовые uiElements не найдены."); return; } const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID; const opponentActualSlotId = myActualPlayerId === configGlobal.PLAYER_ID ? configGlobal.OPPONENT_ID : configGlobal.PLAYER_ID; const myStateInGameState = currentGameState[myActualPlayerId]; const myBaseStatsForUI = gameDataGlobal.playerBaseStats; if (myStateInGameState && myBaseStatsForUI) updateFighterPanelUI('player', myStateInGameState, myBaseStatsForUI, true); else updateFighterPanelUI('player', null, null, true); const opponentStateInGameState = currentGameState[opponentActualSlotId]; const opponentBaseStatsForUI = gameDataGlobal.opponentBaseStats; const isOpponentPanelDissolving = uiElements.opponent.panel?.classList.contains('dissolving'); if (opponentStateInGameState && opponentBaseStatsForUI) { if (uiElements.opponent.panel && (uiElements.opponent.panel.style.opacity !== '1' || (uiElements.opponent.panel.classList.contains('dissolving') && currentGameState.isGameOver === false) )) { const panel = uiElements.opponent.panel; if (panel.classList.contains('dissolving')) { panel.classList.remove('dissolving'); panel.style.transition = 'none'; panel.offsetHeight; 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)'; } } else if (uiElements.opponent.panel && !isOpponentPanelDissolving) { uiElements.opponent.panel.style.opacity = '1'; } updateFighterPanelUI('opponent', opponentStateInGameState, opponentBaseStatsForUI, false); } else { if (!isOpponentPanelDissolving) updateFighterPanelUI('opponent', null, null, false); else console.log("[UI UPDATE DEBUG] Opponent panel is dissolving, skipping content update."); } updateEffectsUI(currentGameState); 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'; let opponentClass = 'title-opponent'; if (myKey === 'elena') myClass = 'title-enchantress'; else if (myKey === 'almagest') myClass = 'title-sorceress'; else if (myKey === 'balard') myClass = 'title-knight'; 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}`; } 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 = `${myName} Ожидание игрока...`; } 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 || 'Неизвестно'}`; uiElements.controls.turnIndicator.style.color = (currentTurnActor?.id === myActualPlayerId) ? 'var(--turn-color)' : 'var(--text-muted)'; } else { uiElements.controls.turnIndicator.textContent = "Игра окончена"; uiElements.controls.turnIndicator.style.color = 'var(--text-muted)'; } } if (uiElements.controls.buttonAttack) { 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) { 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 => { 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; } const hasEnoughResource = actingPlayerState.currentResource >= abilityDataFromGameData.cost; const isOnCooldown = (actingPlayerState.abilityCooldowns?.[abilityId] || 0) > 0; const isGenerallySilenced = actingPlayerState.activeEffects?.some(eff => eff.isFullSilence && eff.turnsLeft > 0); 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; const isDebuffAlreadyOnTarget = isTargetedDebuffAbility && opponentStateForDebuffCheck && opponentStateForDebuffCheck.activeEffects?.some(e => e.id === effectIdForDebuff); button.disabled = !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[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'; 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); } } 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} х.)`; if (isOnCooldown) titleText += ` | На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[abilityId]} х.`; if (isSilenced) titleText += ` | Под безмолвием! Осталось: ${silenceTurnsLeft} х.`; if (isBuffAlreadyActive) { const activeEffect = actingPlayerState.activeEffects?.find(eff => eff.id === abilityId); 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); }); } function showGameOver(playerWon, reason = "", opponentCharacterKeyFromClient = null, data = null) { const config = window.GAME_CONFIG || {}; const clientSpecificGameData = window.gameData; const currentActualGameState = window.gameState; const gameOverScreenElement = uiElements.gameOver.screen; console.log(`[UI.JS DEBUG] showGameOver CALLED. PlayerWon: ${playerWon}, Reason: ${reason}`); 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} оказался(лась) сильнее!`; // === ИЗМЕНЕНИЕ: Добавляем обработку причины 'turn_timeout' === if (reason === 'opponent_disconnected') { let disconnectedName = data?.disconnectedCharacterName || opponentNameForResult; winText = `${disconnectedName} покинул(а) игру. Победа присуждается вам!`; } else if (reason === 'turn_timeout') { // Если текущий игрок (чей ход был) проиграл по таймауту if (!playerWon) { // playerWon здесь будет false, если победил оппонент (т.е. мой таймаут) loseText = `Время на ход истекло! Поражение. ${opponentNameForResult} побеждает!`; } else { // Если я победил, потому что у оппонента истекло время winText = `Время на ход у ${opponentNameForResult} истекло! Победа!`; } } // === КОНЕЦ ИЗМЕНЕНИЯ === 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'; opponentPanelElement.offsetHeight; const loserCharacterKeyForDissolve = data?.loserCharacterKey; if (currentActualGameState && currentActualGameState.isGameOver === true && playerWon) { if (loserCharacterKeyForDissolve === 'balard' || loserCharacterKeyForDissolve === 'almagest') { opponentPanelElement.classList.add('dissolving'); opponentPanelElement.style.opacity = '0'; } else { opponentPanelElement.style.opacity = '1'; opponentPanelElement.style.transform = 'scale(1) translateY(0)'; } } else { opponentPanelElement.style.opacity = '1'; opponentPanelElement.style.transform = 'scale(1) translateY(0)'; } opponentPanelElement.style.transition = ''; } setTimeout((finalStateInTimeout, wonInTimeout, reasonInTimeout, keyInTimeout, dataInTimeout) => { if (gameOverScreenElement && finalStateInTimeout && finalStateInTimeout.isGameOver === true) { if (gameOverScreenElement.classList.contains(config.CSS_CLASS_HIDDEN || 'hidden')) { gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden'); } if(window.getComputedStyle(gameOverScreenElement).display === 'none') gameOverScreenElement.style.display = 'flex'; gameOverScreenElement.style.opacity = '0'; requestAnimationFrame(() => { 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'; uiElements.gameOver.modalContent.style.transform = 'scale(1) translateY(0)'; uiElements.gameOver.modalContent.style.opacity = '1'; } }); } else { if (gameOverScreenElement) { 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'; } gameOverScreenElement.offsetHeight; } } }, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState, playerWon, reason, opponentCharacterKeyFromClient, data); } window.gameUI = { uiElements, addToLog, updateUI, showGameOver, // === ИЗМЕНЕНИЕ: Экспортируем функцию обновления таймера === updateTurnTimerDisplay // === КОНЕЦ ИЗМЕНЕНИЯ === }; })();