460 lines
34 KiB
JavaScript
460 lines
34 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'),
|
||
},
|
||
log: {
|
||
list: document.getElementById('log-list'),
|
||
},
|
||
gameOver: {
|
||
screen: document.getElementById('game-over-screen'),
|
||
message: document.getElementById('result-message'),
|
||
// restartButton: document.getElementById('restart-game-button'), // Старый ID, заменен
|
||
returnToMenuButton: document.getElementById('return-to-menu-button'), // Новый ID
|
||
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]; // 'player' или 'opponent'
|
||
const config = window.GAME_CONFIG || {};
|
||
|
||
if (!elements || !elements.hpFill || !elements.hpText || !elements.resourceFill || !elements.resourceText || !elements.status || !fighterState || !fighterBaseStats) {
|
||
// console.warn(`updateFighterPanelUI: Отсутствуют элементы UI, состояние бойца или базовые статы для панели ${panelRole}.`);
|
||
// Если панель должна быть видима, но нет данных, можно ее скрыть или показать плейсхолдер
|
||
if (elements && elements.panel && elements.panel.style.display !== 'none' && (!fighterState || !fighterBaseStats)) {
|
||
// console.warn(`updateFighterPanelUI: Нет данных для видимой панели ${panelRole}.`);
|
||
// elements.panel.style.opacity = '0.3'; // Пример: сделать полупрозрачной
|
||
}
|
||
return;
|
||
}
|
||
// Если панель была полупрозрачной (из-за отсутствия данных), а теперь данные есть, делаем ее полностью видимой
|
||
// if (elements.panel && elements.panel.style.opacity !== '1' && fighterState && fighterBaseStats) {
|
||
// elements.panel.style.opacity = '1';
|
||
// }
|
||
|
||
|
||
// Обновление имени и иконки персонажа
|
||
if (elements.name) {
|
||
let iconClass = 'fa-question'; // Иконка по умолчанию
|
||
let accentColor = 'var(--text-muted)'; // Цвет по умолчанию
|
||
const characterKey = fighterBaseStats.characterKey;
|
||
|
||
if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-player'; accentColor = 'var(--accent-player)'; }
|
||
else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; accentColor = 'var(--accent-almagest)'; }
|
||
else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-opponent'; accentColor = 'var(--accent-opponent)'; }
|
||
else { /* console.warn(`updateFighterPanelUI: Неизвестный characterKey "${characterKey}" для иконки/цвета имени.`); */ }
|
||
|
||
let nameHtml = `<i class="fas ${iconClass}"></i> ${fighterBaseStats.name || 'Неизвестно'}`;
|
||
if (isControlledByThisClient) nameHtml += " (Вы)";
|
||
elements.name.innerHTML = nameHtml;
|
||
elements.name.style.color = accentColor;
|
||
}
|
||
|
||
// Обновление аватара
|
||
if (elements.avatar && fighterBaseStats.avatarPath) elements.avatar.src = fighterBaseStats.avatarPath;
|
||
else if (elements.avatar) elements.avatar.src = 'images/default_avatar.png'; // Запасной аватар
|
||
|
||
// Обновление полос здоровья и ресурса
|
||
const maxHp = Math.max(1, fighterBaseStats.maxHp); // Избегаем деления на ноль
|
||
const maxRes = Math.max(1, fighterBaseStats.maxResource);
|
||
const currentHp = Math.max(0, fighterState.currentHp);
|
||
const currentRes = Math.max(0, fighterState.currentResource);
|
||
|
||
elements.hpFill.style.width = `${(currentHp / maxHp) * 100}%`;
|
||
elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStats.maxHp}`;
|
||
elements.resourceFill.style.width = `${(currentRes / maxRes) * 100}%`;
|
||
elements.resourceText.textContent = `${Math.round(currentRes)} / ${fighterBaseStats.maxResource}`;
|
||
|
||
// Обновление типа ресурса и иконки (mana/stamina/dark-energy)
|
||
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 glowColorVar = '--panel-glow-opponent';
|
||
let borderColorVar = 'var(--accent-opponent)'; // По умолчанию для оппонента
|
||
if (fighterBaseStats.characterKey === 'elena') { glowColorVar = '--panel-glow-player'; borderColorVar = 'var(--accent-player)'; }
|
||
else if (fighterBaseStats.characterKey === 'almagest') { glowColorVar = 'var(--panel-glow-opponent)'; borderColorVar = 'var(--accent-almagest)'; } // Для Альмагест используется ее цвет рамки
|
||
else if (fighterBaseStats.characterKey === 'balard') { glowColorVar = 'var(--panel-glow-opponent)'; borderColorVar = 'var(--accent-opponent)'; }
|
||
else { borderColorVar = 'var(--panel-border)'; } // Фоллбэк
|
||
|
||
elements.panel.style.borderColor = borderColorVar;
|
||
// Убедимся, что переменная glowColorVar существует, иначе тень может не примениться или вызвать ошибку
|
||
elements.panel.style.boxShadow = glowColorVar ? `0 0 15px ${glowColorVar}, inset 0 0 10px rgba(0, 0, 0, 0.3)` : `0 0 15px rgba(0,0,0,0.4), 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 && !eff.grantsBlock) ) {
|
||
effectClasses += ' effect-stun'; // Стан/безмолвие
|
||
} else if (eff.grantsBlock) {
|
||
effectClasses += ' effect-block'; // Эффект блока
|
||
} else if (eff.type === config.ACTION_TYPE_DEBUFF || (eff.power && eff.power < 0 && eff.type !== config.ACTION_TYPE_HEAL )) {
|
||
effectClasses += ' effect-debuff'; // Ослабления, DoT
|
||
} else { // ACTION_TYPE_BUFF или положительные эффекты (например, HoT)
|
||
effectClasses += ' effect-buff';
|
||
}
|
||
return `<span class="${effectClasses}" title="${title}">${displayText}</span>`;
|
||
}).join(' ');
|
||
}
|
||
|
||
function updateEffectsUI(currentGameState) {
|
||
if (!currentGameState || !window.GAME_CONFIG) { return; }
|
||
const mySlotId = window.myPlayerId; // Технический ID слота этого клиента
|
||
if (!mySlotId) { return; }
|
||
|
||
const opponentSlotId = mySlotId === window.GAME_CONFIG.PLAYER_ID ? window.GAME_CONFIG.OPPONENT_ID : window.GAME_CONFIG.PLAYER_ID;
|
||
|
||
const myState = currentGameState[mySlotId]; // Состояние персонажа этого клиента
|
||
if (uiElements.player && uiElements.player.buffsList && uiElements.player.debuffsList && myState && myState.activeEffects) {
|
||
uiElements.player.buffsList.innerHTML = generateEffectsHTML(myState.activeEffects.filter(e => e.type === window.GAME_CONFIG.ACTION_TYPE_BUFF || e.grantsBlock || (e.type === window.GAME_CONFIG.ACTION_TYPE_HEAL && e.turnsLeft > 0) ));
|
||
uiElements.player.debuffsList.innerHTML = generateEffectsHTML(myState.activeEffects.filter(e => e.type !== window.GAME_CONFIG.ACTION_TYPE_BUFF && !e.grantsBlock && !(e.type === window.GAME_CONFIG.ACTION_TYPE_HEAL && e.turnsLeft > 0) ));
|
||
}
|
||
|
||
const opponentState = currentGameState[opponentSlotId]; // Состояние оппонента этого клиента
|
||
if (uiElements.opponent && uiElements.opponent.buffsList && uiElements.opponent.debuffsList && opponentState && opponentState.activeEffects) {
|
||
uiElements.opponent.buffsList.innerHTML = generateEffectsHTML(opponentState.activeEffects.filter(e => e.type === window.GAME_CONFIG.ACTION_TYPE_BUFF || e.grantsBlock || (e.type === window.GAME_CONFIG.ACTION_TYPE_HEAL && e.turnsLeft > 0) ));
|
||
uiElements.opponent.debuffsList.innerHTML = generateEffectsHTML(opponentState.activeEffects.filter(e => e.type !== window.GAME_CONFIG.ACTION_TYPE_BUFF && !e.grantsBlock && !(e.type === window.GAME_CONFIG.ACTION_TYPE_HEAL && e.turnsLeft > 0) ));
|
||
}
|
||
}
|
||
|
||
function updateUI() {
|
||
const currentGameState = window.gameState; // Глобальное состояние игры
|
||
const gameDataGlobal = window.gameData; // Глобальные данные (статы, абилки) для этого клиента
|
||
const configGlobal = window.GAME_CONFIG; // Глобальный конфиг
|
||
const myActualPlayerId = window.myPlayerId; // Технический ID слота этого клиента
|
||
|
||
if (!currentGameState || !gameDataGlobal || !configGlobal || !myActualPlayerId) {
|
||
console.warn("updateUI: Отсутствуют глобальные gameState, gameData, GAME_CONFIG или myActualPlayerId.");
|
||
return;
|
||
}
|
||
if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) {
|
||
console.warn("updateUI: Некоторые базовые uiElements не найдены.");
|
||
return;
|
||
}
|
||
|
||
// Определяем, чей сейчас ход по ID слота
|
||
const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID;
|
||
// Определяем ID слота оппонента для этого клиента
|
||
const opponentActualSlotId = myActualPlayerId === configGlobal.PLAYER_ID ? configGlobal.OPPONENT_ID : configGlobal.PLAYER_ID;
|
||
|
||
// Обновление панели "моего" персонажа
|
||
if (gameDataGlobal.playerBaseStats && currentGameState[myActualPlayerId]) {
|
||
if (uiElements.player.panel && uiElements.player.panel.style.opacity !== '1') uiElements.player.panel.style.opacity = '1';
|
||
updateFighterPanelUI('player', currentGameState[myActualPlayerId], gameDataGlobal.playerBaseStats, true);
|
||
} else {
|
||
if (uiElements.player.panel) uiElements.player.panel.style.opacity = '0.5';
|
||
}
|
||
|
||
// Обновление панели "моего оппонента"
|
||
if (gameDataGlobal.opponentBaseStats && currentGameState[opponentActualSlotId]) {
|
||
if (uiElements.opponent.panel && uiElements.opponent.panel.style.opacity !== '1' && currentGameState.isGameOver === false ) {
|
||
// Если панель оппонента была "затемнена" и игра не окончена, восстанавливаем видимость
|
||
uiElements.opponent.panel.style.opacity = '1';
|
||
}
|
||
// Перед обновлением, если игра НЕ окончена, а панель "тает", отменяем это
|
||
if (uiElements.opponent.panel && uiElements.opponent.panel.classList.contains('dissolving') && currentGameState.isGameOver === false) {
|
||
console.warn("[UI UPDATE DEBUG] Opponent panel has .dissolving but game is NOT over. Forcing visible.");
|
||
const panel = uiElements.opponent.panel;
|
||
panel.classList.remove('dissolving');
|
||
const originalTransition = panel.style.transition; panel.style.transition = 'none';
|
||
panel.style.opacity = '1'; panel.style.transform = 'scale(1) translateY(0)';
|
||
requestAnimationFrame(() => { panel.style.transition = originalTransition || ''; });
|
||
}
|
||
updateFighterPanelUI('opponent', currentGameState[opponentActualSlotId], gameDataGlobal.opponentBaseStats, false);
|
||
} else {
|
||
if (uiElements.opponent.panel) uiElements.opponent.panel.style.opacity = '0.5';
|
||
}
|
||
|
||
updateEffectsUI(currentGameState);
|
||
|
||
// Обновление заголовка игры (Имя1 vs Имя2)
|
||
if (uiElements.gameHeaderTitle && gameDataGlobal.playerBaseStats && gameDataGlobal.opponentBaseStats) {
|
||
const myName = gameDataGlobal.playerBaseStats.name; // Имя моего персонажа
|
||
const opponentName = gameDataGlobal.opponentBaseStats.name; // Имя моего оппонента
|
||
const myKey = gameDataGlobal.playerBaseStats.characterKey;
|
||
const opponentKey = gameDataGlobal.opponentBaseStats.characterKey;
|
||
|
||
let myClass = 'title-player';
|
||
if (myKey === 'elena') myClass = 'title-enchantress'; else if (myKey === 'almagest') myClass = 'title-sorceress';
|
||
|
||
let opponentClass = 'title-opponent';
|
||
if (opponentKey === 'elena') opponentClass = 'title-enchantress'; else if (opponentKey === 'almagest') opponentClass = 'title-sorceress'; else if (opponentKey === 'balard') opponentClass = 'title-knight';
|
||
|
||
uiElements.gameHeaderTitle.innerHTML = `<span class="${myClass}">${myName}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="${opponentClass}">${opponentName}</span>`;
|
||
}
|
||
|
||
// Обновление индикатора хода
|
||
if (uiElements.controls.turnIndicator) {
|
||
// Имя того, чей ход, берем из gameState по ID слота
|
||
const currentTurnActorState = currentGameState[actorSlotWhoseTurnItIs]; // 'player' или 'opponent'
|
||
const currentTurnName = currentTurnActorState?.name || 'Неизвестно';
|
||
uiElements.controls.turnIndicator.textContent = `Ход: ${currentTurnName}`;
|
||
|
||
const currentTurnCharacterKey = currentTurnActorState?.characterKey;
|
||
let turnColor = 'var(--turn-color)';
|
||
if (currentTurnCharacterKey === 'elena') turnColor = 'var(--accent-player)';
|
||
else if (currentTurnCharacterKey === 'almagest') turnColor = 'var(--accent-almagest)';
|
||
else if (currentTurnCharacterKey === 'balard') turnColor = 'var(--accent-opponent)';
|
||
uiElements.controls.turnIndicator.style.color = turnColor;
|
||
}
|
||
|
||
// Управление активностью кнопок
|
||
const canThisClientAct = actorSlotWhoseTurnItIs === myActualPlayerId; // Может ли ЭТОТ клиент ходить
|
||
const isGameActive = !currentGameState.isGameOver;
|
||
|
||
// Кнопка атаки
|
||
if (uiElements.controls.buttonAttack) {
|
||
uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive);
|
||
const myCharKey = gameDataGlobal.playerBaseStats?.characterKey;
|
||
const myStateForAttackBuff = currentGameState[myActualPlayerId]; // Состояние моего персонажа
|
||
let attackBuffId = null;
|
||
if (myCharKey === 'elena') attackBuffId = configGlobal.ABILITY_ID_NATURE_STRENGTH;
|
||
else if (myCharKey === 'almagest') attackBuffId = configGlobal.ABILITY_ID_ALMAGEST_BUFF_ATTACK;
|
||
|
||
if (attackBuffId && myStateForAttackBuff && myStateForAttackBuff.activeEffects) {
|
||
const isAttackBuffReady = myStateForAttackBuff.activeEffects.some(eff => eff.id === attackBuffId && !eff.justCast);
|
||
uiElements.controls.buttonAttack.classList.toggle(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed', isAttackBuffReady && canThisClientAct && isGameActive);
|
||
} else {
|
||
uiElements.controls.buttonAttack.classList.remove(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed');
|
||
}
|
||
}
|
||
if (uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true; // Пока не используется
|
||
|
||
// Кнопки способностей
|
||
const actingPlayerState = currentGameState[myActualPlayerId]; // Состояние моего персонажа
|
||
const actingPlayerAbilities = gameDataGlobal.playerAbilities; // Способности моего персонажа
|
||
const actingPlayerResourceName = gameDataGlobal.playerBaseStats?.resourceName;
|
||
|
||
uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => {
|
||
if (!(button instanceof HTMLButtonElement) || !actingPlayerState || !actingPlayerAbilities || !actingPlayerResourceName) {
|
||
if(button instanceof HTMLButtonElement) button.disabled = true;
|
||
return;
|
||
}
|
||
const abilityId = button.dataset.abilityId;
|
||
const ability = actingPlayerAbilities.find(ab => ab.id === abilityId);
|
||
if (!ability) { button.disabled = true; return; }
|
||
|
||
const hasEnoughResource = actingPlayerState.currentResource >= ability.cost;
|
||
const isBuffAlreadyActive = ability.type === configGlobal.ACTION_TYPE_BUFF && actingPlayerState.activeEffects?.some(eff => eff.id === ability.id);
|
||
const isOnCooldown = (actingPlayerState.abilityCooldowns?.[ability.id] || 0) > 0;
|
||
|
||
const isGenerallySilenced = actingPlayerState.activeEffects?.some(eff => eff.isFullSilence && eff.turnsLeft > 0);
|
||
const specificSilenceEffect = actingPlayerState.disabledAbilities?.find(dis => dis.abilityId === abilityId && dis.turnsLeft > 0);
|
||
const isSpecificallySilenced = !!specificSilenceEffect;
|
||
const isSilenced = isGenerallySilenced || isSpecificallySilenced;
|
||
const silenceTurnsLeft = isGenerallySilenced
|
||
? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0)
|
||
: (specificSilenceEffect?.turnsLeft || 0);
|
||
|
||
let isDisabledByDebuffOnTarget = false;
|
||
const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId]; // Состояние моего оппонента
|
||
if (opponentStateForDebuffCheck && opponentStateForDebuffCheck.activeEffects &&
|
||
(ability.id === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF)) {
|
||
const effectIdForDebuff = 'effect_' + ability.id;
|
||
isDisabledByDebuffOnTarget = opponentStateForDebuffCheck.activeEffects.some(e => e.id === effectIdForDebuff);
|
||
}
|
||
|
||
button.disabled = !(canThisClientAct && isGameActive) || !hasEnoughResource || isBuffAlreadyActive || isSilenced || isOnCooldown || isDisabledByDebuffOnTarget;
|
||
button.classList.remove(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced', configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
|
||
const cooldownDisplay = button.querySelector('.ability-cooldown-display');
|
||
|
||
if (isOnCooldown) {
|
||
button.classList.add(configGlobal.CSS_CLASS_ABILITY_ON_COOLDOWN||'is-on-cooldown');
|
||
if (cooldownDisplay) { cooldownDisplay.textContent = `КД: ${actingPlayerState.abilityCooldowns[ability.id]}`; cooldownDisplay.style.display = 'block'; }
|
||
} else if (isSilenced) {
|
||
button.classList.add(configGlobal.CSS_CLASS_ABILITY_SILENCED||'is-silenced');
|
||
if (cooldownDisplay) { cooldownDisplay.textContent = `Безм: ${silenceTurnsLeft}`; cooldownDisplay.style.display = 'block'; }
|
||
} else {
|
||
if (cooldownDisplay) cooldownDisplay.style.display = 'none';
|
||
button.classList.toggle(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', !hasEnoughResource && !isBuffAlreadyActive && !isDisabledByDebuffOnTarget);
|
||
button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', isBuffAlreadyActive && !isDisabledByDebuffOnTarget);
|
||
}
|
||
|
||
// Обновление title (всплывающей подсказки)
|
||
let titleText = `${ability.name} (${ability.cost} ${actingPlayerResourceName})`;
|
||
let descriptionText = ability.description;
|
||
if (typeof ability.descriptionFunction === 'function') {
|
||
descriptionText = ability.descriptionFunction(configGlobal, gameDataGlobal.opponentBaseStats); // Для описания используем статы оппонента этого клиента
|
||
}
|
||
if (descriptionText) titleText += ` - ${descriptionText}`;
|
||
let abilityBaseCooldown = ability.cooldown;
|
||
if (ability.internalCooldownFromConfig && configGlobal[ability.internalCooldownFromConfig]) abilityBaseCooldown = configGlobal[ability.internalCooldownFromConfig];
|
||
else if (ability.internalCooldownValue) abilityBaseCooldown = ability.internalCooldownValue;
|
||
if (abilityBaseCooldown) titleText += ` (КД: ${abilityBaseCooldown} х.)`;
|
||
|
||
if (isOnCooldown) titleText = `${ability.name} - На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[ability.id]} х.`;
|
||
else if (isSilenced) titleText = `Безмолвие! Осталось: ${silenceTurnsLeft} х.`;
|
||
else if (isBuffAlreadyActive) {
|
||
const activeEffect = actingPlayerState.activeEffects?.find(eff => eff.id === abilityId);
|
||
titleText = `Эффект "${ability.name}" уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}`;
|
||
} else if (isDisabledByDebuffOnTarget && opponentStateForDebuffCheck) {
|
||
const activeDebuff = opponentStateForDebuffCheck.activeEffects?.find(e => e.id === 'effect_' + ability.id);
|
||
titleText = `Эффект "${ability.name}" уже наложен на ${opponentStateForDebuffCheck.name}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}`;
|
||
}
|
||
button.setAttribute('title', titleText);
|
||
});
|
||
}
|
||
|
||
function showGameOver(playerWon, reason = "", opponentCharacterKeyFromClient = null) {
|
||
const config = window.GAME_CONFIG || {};
|
||
// Используем gameData, сохраненное в client.js, так как оно отражает перспективу этого клиента
|
||
const clientSpecificGameData = window.gameData;
|
||
const currentActualGameState = window.gameState; // Самое актуальное состояние игры
|
||
const gameOverScreenElement = uiElements.gameOver.screen;
|
||
|
||
console.log(`[UI.JS DEBUG] showGameOver CALLED. PlayerWon: ${playerWon}, Reason: ${reason}`);
|
||
console.log(`[UI.JS DEBUG] Current window.gameState.isGameOver: ${currentActualGameState?.isGameOver}`);
|
||
console.log(`[UI.JS DEBUG] Opponent Character Key (from client via param): ${opponentCharacterKeyFromClient}`);
|
||
console.log(`[UI.JS DEBUG] My Character Name (from window.gameData): ${clientSpecificGameData?.playerBaseStats?.name}`);
|
||
console.log(`[UI.JS DEBUG] Opponent Character Name (from window.gameData): ${clientSpecificGameData?.opponentBaseStats?.name}`);
|
||
|
||
|
||
if (!gameOverScreenElement) {
|
||
console.warn("[UI.JS DEBUG] showGameOver: gameOverScreenElement not found.");
|
||
return;
|
||
}
|
||
|
||
const resultMsgElement = uiElements.gameOver.message;
|
||
// Имена для сообщения берем из clientSpecificGameData, т.к. они уже "перевернуты" для каждого клиента
|
||
const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок";
|
||
const opponentNameForResult = clientSpecificGameData?.opponentBaseStats?.name || "Противник";
|
||
|
||
if (resultMsgElement) {
|
||
let winText = `Победа! ${myNameForResult} празднует!`;
|
||
let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`;
|
||
if (reason === 'opponent_disconnected') {
|
||
winText = `${opponentNameForResult} покинул(а) игру. Победа присуждается вам!`;
|
||
// Если оппонент отключился, а мы проиграли (технически такое возможно, если сервер так решит)
|
||
// То текст поражения можно оставить стандартным или специфичным.
|
||
// Пока оставим стандартный, если playerWon = false и reason = opponent_disconnected.
|
||
}
|
||
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');
|
||
console.log(`[UI.JS DEBUG] Opponent panel classList after initial remove .dissolving: ${opponentPanelElement.className}`);
|
||
|
||
// Используем opponentCharacterKeyFromClient, так как это ключ реального персонажа оппонента
|
||
const keyForDissolveEffect = opponentCharacterKeyFromClient;
|
||
|
||
if (currentActualGameState && currentActualGameState.isGameOver === true && playerWon && reason !== 'opponent_disconnected') {
|
||
if (keyForDissolveEffect === 'balard' || keyForDissolveEffect === 'almagest') {
|
||
console.log(`[UI.JS DEBUG] ADDING .dissolving to opponent panel. Conditions: isGameOver=${currentActualGameState.isGameOver}, playerWon=${playerWon}, opponentKeyForEffect=${keyForDissolveEffect}`);
|
||
opponentPanelElement.classList.add('dissolving');
|
||
} else {
|
||
console.log(`[UI.JS DEBUG] NOT adding .dissolving (opponent key mismatch for dissolve effect). Key for effect: ${keyForDissolveEffect}`);
|
||
}
|
||
} else {
|
||
console.log(`[UI.JS DEBUG] NOT adding .dissolving. Conditions NOT met: isGameOver=${currentActualGameState?.isGameOver}, playerWon=${playerWon}, reason=${reason}`);
|
||
if (!currentActualGameState || currentActualGameState.isGameOver === false) {
|
||
console.log(`[UI.JS DEBUG] Ensuring opponent panel is visible because game is not 'isGameOver=true' or gameState missing.`);
|
||
const originalTransition = opponentPanelElement.style.transition;
|
||
opponentPanelElement.style.transition = 'none';
|
||
opponentPanelElement.style.opacity = '1';
|
||
opponentPanelElement.style.transform = 'scale(1) translateY(0)';
|
||
requestAnimationFrame(() => {
|
||
opponentPanelElement.style.transition = originalTransition || '';
|
||
});
|
||
}
|
||
}
|
||
console.log(`[UI.JS DEBUG] Opponent panel classList FINAL in showGameOver: ${opponentPanelElement.className}`);
|
||
}
|
||
|
||
setTimeout(() => {
|
||
if (window.gameState && window.gameState.isGameOver === true) { // Перепроверяем перед показом
|
||
console.log(`[UI.JS DEBUG] Showing gameOverScreen modal (isGameOver is true).`);
|
||
gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden');
|
||
requestAnimationFrame(() => {
|
||
gameOverScreenElement.style.opacity = '0';
|
||
setTimeout(() => {
|
||
gameOverScreenElement.style.opacity = '1';
|
||
if (uiElements.gameOver.modalContent) {
|
||
uiElements.gameOver.modalContent.style.transform = 'scale(1) translateY(0)';
|
||
uiElements.gameOver.modalContent.style.opacity = '1';
|
||
}
|
||
}, config.MODAL_TRANSITION_DELAY || 10);
|
||
});
|
||
} else {
|
||
console.log(`[UI.JS DEBUG] NOT showing gameOverScreen modal (isGameOver is now false or gameState missing).`);
|
||
}
|
||
}, config.DELAY_BEFORE_VICTORY_MODAL || 1500);
|
||
}
|
||
|
||
window.gameUI = { uiElements, addToLog, updateUI, showGameOver };
|
||
})(); |