bc/public/js/ui.js

534 lines
37 KiB
JavaScript
Raw Permalink 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'),
// === НОВЫЕ ЭЛЕМЕНТЫ для переключателя панелей ===
panelSwitcher: {
controlsContainer: document.querySelector('.panel-switcher-controls'),
showPlayerBtn: document.getElementById('show-player-panel-btn'),
showOpponentBtn: document.getElementById('show-opponent-panel-btn')
},
battleArenaContainer: document.querySelector('.battle-arena-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 = 'Нет';
}
}
function updateTurnTimerDisplay(remainingTimeMs, isCurrentPlayerActualTurn, gameMode) {
const timerSpan = uiElements.controls.turnTimerSpan;
const timerContainer = uiElements.controls.turnTimerContainer;
if (!timerSpan || !timerContainer) return;
if (window.gameState && window.gameState.isGameOver) {
timerContainer.style.display = 'block';
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) {
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;
if (!gameOverScreenElement) { return; }
const resultMsgElement = uiElements.gameOver.message;
const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок";
const opponentNameForResult = clientSpecificGameData?.opponentBaseStats?.name || "Противник";
if (resultMsgElement) {
let winText = `Победа! ${myNameForResult} празднует!`;
let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`;
if (reason === 'opponent_disconnected') {
let disconnectedName = data?.disconnectedCharacterName || opponentNameForResult;
winText = `${disconnectedName} покинул(а) игру. Победа присуждается вам!`;
} else if (reason === 'turn_timeout') {
if (!playerWon) {
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) => {
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);
}
// === НОВАЯ ФУНКЦИЯ для настройки переключателя панелей ===
function setupPanelSwitcher() {
const { showPlayerBtn, showOpponentBtn } = uiElements.panelSwitcher;
const battleArena = uiElements.battleArenaContainer;
if (showPlayerBtn && showOpponentBtn && battleArena) {
showPlayerBtn.addEventListener('click', () => {
battleArena.classList.remove('show-opponent-panel');
showPlayerBtn.classList.add('active');
showOpponentBtn.classList.remove('active');
});
showOpponentBtn.addEventListener('click', () => {
battleArena.classList.add('show-opponent-panel');
showOpponentBtn.classList.add('active');
showPlayerBtn.classList.remove('active');
});
// По умолчанию при загрузке (если кнопки видимы) панель игрока активна
// CSS уже должен это обеспечивать, но для надежности можно убедиться
if (window.getComputedStyle(uiElements.panelSwitcher.controlsContainer).display !== 'none') {
battleArena.classList.remove('show-opponent-panel');
showPlayerBtn.classList.add('active');
showOpponentBtn.classList.remove('active');
}
}
}
// === КОНЕЦ НОВОЙ ФУНКЦИИ ===
window.gameUI = {
uiElements,
addToLog,
updateUI,
showGameOver,
updateTurnTimerDisplay
};
// Настраиваем переключатель панелей при загрузке скрипта
setupPanelSwitcher();
})();