// /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'), 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) { console.warn(`updateFighterPanelUI: Отсутствуют элементы/состояние/статы для панели ${panelRole}.`); return; } if (elements.name) { let iconClass = 'fa-question'; let accentColor = 'var(--text-muted)'; const characterKey = fighterBaseStats.characterKey; if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-player'; accentColor = 'var(--accent-player)'; } else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; accentColor = 'var(--accent-almagest)'; } // Используем новый цвет else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-opponent'; accentColor = 'var(--accent-opponent)'; } let nameHtml = ` ${fighterBaseStats.name}`; if (isControlledByThisClient) nameHtml += " (Вы)"; elements.name.innerHTML = nameHtml; elements.name.style.color = accentColor; } if (elements.avatar && fighterBaseStats.avatarPath) elements.avatar.src = fighterBaseStats.avatarPath; else if (elements.avatar) elements.avatar.src = 'images/default_avatar.png'; 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 = `${Math.round(currentRes)} / ${fighterBaseStats.maxResource}`; const resourceBarContainer = elements[`${panelRole}ResourceBarContainer`]; const resourceIconElement = elements[`${panelRole}ResourceTypeIcon`]; if (resourceBarContainer && resourceIconElement) { resourceBarContainer.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'; } resourceBarContainer.classList.add(resourceClass); resourceIconElement.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 glowColorVar = '--panel-glow-opponent'; let borderColorVar = '--accent-opponent'; if (fighterBaseStats.characterKey === 'elena') { glowColorVar = '--panel-glow-player'; borderColorVar = '--accent-player'; } else if (fighterBaseStats.characterKey === 'almagest') { glowColorVar = '--panel-glow-opponent'; borderColorVar = 'var(--accent-almagest)'; } // Цвет рамки Альмагест elements.panel.style.borderColor = borderColorVar; // Прямое присвоение, т.к. var() не сработает для accent-almagest если он не в :root elements.panel.style.boxShadow = `0 0 15px var(${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.type === config.ACTION_TYPE_DISABLE || eff.isFullSilence || eff.id.startsWith('playerSilencedOn_')) effectClasses += ' effect-stun'; else if (eff.type === config.ACTION_TYPE_DEBUFF || (eff.power && eff.power < 0) || eff.id.startsWith('effect_')) effectClasses += ' effect-debuff'; else if (eff.grantsBlock) effectClasses += ' effect-block'; else effectClasses += ' effect-buff'; return `${displayText}`; }).join(' '); } function updateEffectsUI(currentGameState) { if (!currentGameState || !uiElements.player.buffsList || !uiElements.opponent.buffsList) return; const mySlotId = window.myPlayerId; // Наш слот ('player' или 'opponent') const opponentSlotId = mySlotId === window.GAME_CONFIG.PLAYER_ID ? window.GAME_CONFIG.OPPONENT_ID : window.GAME_CONFIG.PLAYER_ID; const myState = currentGameState[mySlotId]; if (uiElements.player && myState && myState.activeEffects) { uiElements.player.buffsList.innerHTML = generateEffectsHTML(myState.activeEffects.filter(e => e.type === window.GAME_CONFIG.ACTION_TYPE_BUFF || e.grantsBlock)); uiElements.player.debuffsList.innerHTML = generateEffectsHTML(myState.activeEffects.filter(e => e.type !== window.GAME_CONFIG.ACTION_TYPE_BUFF && !e.grantsBlock)); } const opponentState = currentGameState[opponentSlotId]; if (uiElements.opponent && opponentState && opponentState.activeEffects) { uiElements.opponent.buffsList.innerHTML = generateEffectsHTML(opponentState.activeEffects.filter(e => e.type === window.GAME_CONFIG.ACTION_TYPE_BUFF || e.grantsBlock)); uiElements.opponent.debuffsList.innerHTML = generateEffectsHTML(opponentState.activeEffects.filter(e => e.type !== window.GAME_CONFIG.ACTION_TYPE_BUFF && !e.grantsBlock)); } } function updateUI() { const currentGameState = window.gameState; const gameDataGlobal = window.gameData; const configGlobal = window.GAME_CONFIG; const myActualPlayerId = window.myPlayerId; // Слот, который занимает ЭТОТ клиент ('player' или 'opponent') 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; // Обновление панелей бойцов const opponentActualSlotId = myActualPlayerId === configGlobal.PLAYER_ID ? configGlobal.OPPONENT_ID : configGlobal.PLAYER_ID; updateFighterPanelUI('player', currentGameState[myActualPlayerId], gameDataGlobal.playerBaseStats, true); updateFighterPanelUI('opponent', currentGameState[opponentActualSlotId], gameDataGlobal.opponentBaseStats, false); 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'; 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}`; } if (uiElements.controls.turnIndicator) { const currentTurnActorState = currentGameState[actorSlotWhoseTurnItIs]; const currentTurnName = currentTurnActorState?.name || 'Неизвестно'; uiElements.controls.turnIndicator.textContent = `Ход: ${currentTurnName}`; const currentTurnCharacterKey = currentTurnActorState?.characterKey; let turnColor = 'var(--turn-color)'; if (currentTurnCharacterKey === 'elena') turnColor = 'var(--accent-player)'; else if (currentTurnCharacterKey === 'almagest') turnColor = 'var(--accent-almagest)'; else if (currentTurnCharacterKey === 'balard') turnColor = 'var(--accent-opponent)'; uiElements.controls.turnIndicator.style.color = turnColor; } const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId; const isGameActive = !currentGameState.isGameOver; if (uiElements.controls.buttonAttack) { uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive); const myCharKey = gameDataGlobal.playerBaseStats.characterKey; const myState = 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 && myState) { const isAttackBuffReady = myState.activeEffects.some(eff => eff.id === attackBuffId && !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 = 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) { 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 === ability.id); const isOnCooldown = (actingPlayerState.abilityCooldowns?.[ability.id] || 0) > 0; const isGenerallySilenced = actingPlayerState.activeEffects.some(eff => eff.isFullSilence && eff.turnsLeft > 0); const isSpecificallySilenced = actingPlayerState.disabledAbilities?.some(dis => dis.abilityId === abilityId && dis.turnsLeft > 0); const isSilenced = isGenerallySilenced || isSpecificallySilenced; const silenceTurnsLeft = isGenerallySilenced ? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0) : (isSpecificallySilenced ? (actingPlayerState.disabledAbilities.find(dis => dis.abilityId === abilityId)?.turnsLeft || 0) : 0); let isDisabledByDebuffOnTarget = false; const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId]; if ((ability.id === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF) && opponentStateForDebuffCheck) { const effectIdForDebuff = 'effect_' + ability.id; isDisabledByDebuffOnTarget = opponentStateForDebuffCheck.activeEffects.some(e => e.id === effectIdForDebuff); } button.disabled = !(canThisClientAct && isGameActive) || !hasEnoughResource || isBuffAlreadyActive || isSilenced || isOnCooldown || isDisabledByDebuffOnTarget; 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) { cooldownDisplay.textContent = `Безм: ${silenceTurnsLeft}`; cooldownDisplay.style.display = 'block'; } } else { if (cooldownDisplay) cooldownDisplay.style.display = 'none'; button.classList.toggle(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', !hasEnoughResource && !isBuffAlreadyActive && !isDisabledByDebuffOnTarget); button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', isBuffAlreadyActive && !isDisabledByDebuffOnTarget); } let titleText = `${ability.name} (${ability.cost} ${actingPlayerResourceName})`; let descriptionText = ability.description; if (typeof ability.descriptionFunction === 'function') { descriptionText = ability.descriptionFunction(configGlobal, gameDataGlobal.opponentBaseStats); } if (descriptionText) titleText += ` - ${descriptionText}`; let abilityBaseCooldown = ability.cooldown; if (ability.internalCooldownFromConfig && configGlobal[ability.internalCooldownFromConfig]) abilityBaseCooldown = configGlobal[ability.internalCooldownFromConfig]; else if (ability.internalCooldownValue) abilityBaseCooldown = ability.internalCooldownValue; if (abilityBaseCooldown) titleText += ` (КД: ${abilityBaseCooldown} х.)`; if (isOnCooldown) titleText = `${ability.name} - На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[ability.id]} х.`; else if (isSilenced) titleText = `Безмолвие! Осталось: ${silenceTurnsLeft} х.`; else if (isBuffAlreadyActive) { const activeEffect = actingPlayerState.activeEffects.find(eff => eff.id === abilityId); titleText = `Эффект "${ability.name}" уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}`; } else if (isDisabledByDebuffOnTarget && opponentStateForDebuffCheck) { const activeDebuff = opponentStateForDebuffCheck.activeEffects.find(e => e.id === 'effect_' + ability.id); titleText = `Эффект "${ability.name}" уже наложен на ${opponentStateForDebuffCheck.name}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}`; } button.setAttribute('title', titleText); }); } function showGameOver(playerWon, reason = "") { const config = window.GAME_CONFIG || {}; const gameDataGlobal = window.gameData || {}; const currentGameState = window.gameState; const gameOverScreenElement = uiElements.gameOver.screen; if (!gameOverScreenElement || !currentGameState) return; const resultMsgElement = uiElements.gameOver.message; const opponentPanelElement = uiElements.opponent.panel; const myName = gameDataGlobal.playerBaseStats?.name || "Игрок"; const opponentName = gameDataGlobal.opponentBaseStats?.name || "Противник"; const opponentCharacterKey = gameDataGlobal.opponentBaseStats?.characterKey; if (resultMsgElement) { let winText = `Победа! ${myName} празднует!`; let loseText = `Поражение! ${opponentName} оказался(лась) сильнее!`; if (reason === 'opponent_disconnected') winText = `${opponentName} покинул(а) игру. Победа присуждается вам!`; resultMsgElement.textContent = playerWon ? winText : loseText; resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)'; } if (opponentPanelElement) { opponentPanelElement.classList.remove('dissolving'); if (playerWon && reason !== 'opponent_disconnected' && (opponentCharacterKey === 'balard' || opponentCharacterKey === 'almagest')) { opponentPanelElement.classList.add('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 = { uiElements, addToLog, updateUI, showGameOver }; })();