bc/public/js/ui.js

511 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /public/js/ui.js
// Этот файл отвечает за обновление DOM на основе состояния игры,
// полученного от client.js (который, в свою очередь, получает его от сервера).
(function() {
// --- DOM Элементы ---
const uiElements = {
player: {
panel: document.getElementById('player-panel'),
name: document.getElementById('player-name'),
avatar: document.getElementById('player-panel')?.querySelector('.player-avatar'),
hpFill: document.getElementById('player-hp-fill'), hpText: document.getElementById('player-hp-text'),
resourceFill: document.getElementById('player-resource-fill'), resourceText: document.getElementById('player-resource-text'),
status: document.getElementById('player-status'),
effectsContainer: document.getElementById('player-effects'),
buffsList: document.getElementById('player-effects')?.querySelector('.player-buffs'),
debuffsList: document.getElementById('player-effects')?.querySelector('.player-debuffs')
},
opponent: {
panel: document.getElementById('opponent-panel'),
name: document.getElementById('opponent-name'),
avatar: document.getElementById('opponent-panel')?.querySelector('.opponent-avatar'),
hpFill: document.getElementById('opponent-hp-fill'), hpText: document.getElementById('opponent-hp-text'),
resourceFill: document.getElementById('opponent-resource-fill'), resourceText: document.getElementById('opponent-resource-text'),
status: document.getElementById('opponent-status'),
effectsContainer: document.getElementById('opponent-effects'),
buffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-buffs'),
debuffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-debuffs')
},
controls: {
turnIndicator: document.getElementById('turn-indicator'),
buttonAttack: document.getElementById('button-attack'),
buttonBlock: document.getElementById('button-block'),
abilitiesGrid: document.getElementById('abilities-grid'),
// === ИЗМЕНЕНИЕ: Добавлены элементы таймера ===
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') ? '<i class="fas fa-question icon-player"></i> Ожидание данных...' : '<i class="fas fa-question icon-opponent"></i> Ожидание игрока...';
if(elements.hpText) elements.hpText.textContent = 'N/A';
if(elements.resourceText) elements.resourceText.textContent = 'N/A';
if(elements.status) elements.status.textContent = 'Неизвестно';
if(elements.buffsList) elements.buffsList.innerHTML = 'Нет';
if(elements.debuffsList) elements.debuffsList.innerHTML = 'Нет';
if(elements.avatar) elements.avatar.src = 'images/default_avatar.png';
if(panelRole === 'player' && uiElements.playerResourceTypeIcon) uiElements.playerResourceTypeIcon.className = 'fas fa-question';
if(panelRole === 'opponent' && uiElements.opponentResourceTypeIcon) uiElements.opponentResourceTypeIcon.className = 'fas fa-question';
if(panelRole === 'player' && uiElements.playerResourceBarContainer) uiElements.playerResourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
if(panelRole === 'opponent' && uiElements.opponentResourceBarContainer) uiElements.opponentResourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy');
if(elements.panel) elements.panel.style.opacity = '0.5';
}
return;
}
if (elements.panel) elements.panel.style.opacity = '1';
if (elements.name) {
let iconClass = 'fa-question';
const characterKey = fighterBaseStats.characterKey;
if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-elena'; } // Используем специфичный класс для цвета
else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; }
else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-balard'; } // Для Баларда тоже специфичный
let nameHtml = `<i class="fas ${iconClass}"></i> ${fighterBaseStats.name || 'Неизвестно'}`;
if (isControlledByThisClient) nameHtml += " (Вы)";
elements.name.innerHTML = nameHtml;
}
if (elements.avatar && fighterBaseStats.avatarPath) {
elements.avatar.src = fighterBaseStats.avatarPath;
elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard');
elements.avatar.classList.add(`avatar-${fighterBaseStats.characterKey}`);
} else if (elements.avatar) {
elements.avatar.src = 'images/default_avatar.png';
elements.avatar.classList.remove('avatar-elena', 'avatar-almagest', 'avatar-balard');
}
const maxHp = Math.max(1, fighterBaseStats.maxHp);
const maxRes = Math.max(1, fighterBaseStats.maxResource);
const currentHp = Math.max(0, fighterState.currentHp);
const currentRes = Math.max(0, fighterState.currentResource);
elements.hpFill.style.width = `${(currentHp / maxHp) * 100}%`;
elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`;
elements.resourceFill.style.width = `${(currentRes / maxRes) * 100}%`;
elements.resourceText.textContent = `${currentRes} / ${fighterBaseStats.maxResource}`;
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 `<span class="${effectClasses}" title="${title}">${displayText}</span>`;
}).join(' ');
}
function updateEffectsUI(currentGameState) {
if (!currentGameState || !window.GAME_CONFIG) return;
const mySlotId = window.myPlayerId;
const config = window.GAME_CONFIG;
if (!mySlotId) return;
const opponentSlotId = mySlotId === config.PLAYER_ID ? config.OPPONENT_ID : config.PLAYER_ID;
const myState = currentGameState[mySlotId];
const opponentState = currentGameState[opponentSlotId];
const typeOrder = { [config.ACTION_TYPE_BUFF]: 1, grantsBlock: 2, [config.ACTION_TYPE_HEAL]: 3, [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 = `<span>Ожидание данных...</span>`;
if(uiElements.controls.turnIndicator) uiElements.controls.turnIndicator.textContent = "Ожидание данных...";
if(uiElements.controls.buttonAttack) uiElements.controls.buttonAttack.disabled = true;
if(uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true;
if(uiElements.controls.abilitiesGrid) uiElements.controls.abilitiesGrid.innerHTML = '<p class="placeholder-text">Загрузка способностей...</p>';
// === ИЗМЕНЕНИЕ: Сбрасываем таймер, если нет данных ===
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 = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="${opponentClass}">${opponentName}</span>`;
} else if (uiElements.gameHeaderTitle) {
const myName = gameDataGlobal.playerBaseStats?.name || 'Игрок 1'; const myKey = gameDataGlobal.playerBaseStats?.characterKey;
let myClass = 'title-player'; if (myKey === 'elena') myClass = 'title-enchantress'; else if (myKey === 'almagest') myClass = 'title-sorceress';
uiElements.gameHeaderTitle.innerHTML = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="title-opponent">Ожидание игрока...</span>`;
}
const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId;
const isGameActive = !currentGameState.isGameOver;
const myCharacterState = currentGameState[myActualPlayerId];
if (uiElements.controls.turnIndicator) {
if (isGameActive) {
const currentTurnActor = currentGameState.isPlayerTurn ? currentGameState.player : currentGameState.opponent;
uiElements.controls.turnIndicator.textContent = `Ход ${currentGameState.turnNumber}: ${currentTurnActor?.name || 'Неизвестно'}`;
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
// === КОНЕЦ ИЗМЕНЕНИЯ ===
};
})();