534 lines
37 KiB
JavaScript
534 lines
37 KiB
JavaScript
// /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();
|
||
|
||
})(); |