bc/public/js/ui.js

460 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// /public/js/ui.js
// Этот файл отвечает за обновление DOM на основе состояния игры,
// полученного от client.js (который, в свою очередь, получает его от сервера).
(function() {
// --- DOM Элементы ---
const uiElements = {
player: { // Панель для персонажа, которым управляет ЭТОТ клиент
panel: document.getElementById('player-panel'),
name: document.getElementById('player-name'),
avatar: document.getElementById('player-panel')?.querySelector('.player-avatar'),
hpFill: document.getElementById('player-hp-fill'), hpText: document.getElementById('player-hp-text'),
resourceFill: document.getElementById('player-resource-fill'), resourceText: document.getElementById('player-resource-text'),
status: document.getElementById('player-status'),
effectsContainer: document.getElementById('player-effects'),
buffsList: document.getElementById('player-effects')?.querySelector('.player-buffs'),
debuffsList: document.getElementById('player-effects')?.querySelector('.player-debuffs')
},
opponent: { // Панель для персонажа-противника ЭТОГО клиента
panel: document.getElementById('opponent-panel'),
name: document.getElementById('opponent-name'),
avatar: document.getElementById('opponent-panel')?.querySelector('.opponent-avatar'),
hpFill: document.getElementById('opponent-hp-fill'), hpText: document.getElementById('opponent-hp-text'),
resourceFill: document.getElementById('opponent-resource-fill'), resourceText: document.getElementById('opponent-resource-text'),
status: document.getElementById('opponent-status'),
effectsContainer: document.getElementById('opponent-effects'),
buffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-buffs'),
debuffsList: document.getElementById('opponent-effects')?.querySelector('.opponent-debuffs')
},
controls: {
turnIndicator: document.getElementById('turn-indicator'),
buttonAttack: document.getElementById('button-attack'),
buttonBlock: document.getElementById('button-block'), // Защита пока не активна
abilitiesGrid: document.getElementById('abilities-grid'),
},
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 };
})();