465 lines
30 KiB
JavaScript
465 lines
30 KiB
JavaScript
// /public/js/ui.js
|
||
// Этот файл отвечает за обновление DOM на основе состояния игры,
|
||
// полученного от client.js (который, в свою очередь, получает его от сервера).
|
||
|
||
// console.log("Loading ui.js...");
|
||
|
||
// GAME_CONFIG и gameData теперь предполагается, что будут установлены в window
|
||
// из client.js после получения данных от сервера.
|
||
// Это не лучшая практика (глобальные переменные), но для адаптации
|
||
// существующего кода это может быть проще на первом этапе.
|
||
// В идеале, все функции updateUI должны принимать gameState и gameData как параметры.
|
||
|
||
(function() {
|
||
// --- DOM Элементы (остаются как были) ---
|
||
const uiElements = {
|
||
player: {
|
||
panel: document.getElementById('player-panel'),
|
||
name: document.getElementById('player-name'), // Для обновления имени в PvP
|
||
avatar: document.getElementById('player-panel')?.querySelector('.player-avatar'),
|
||
hpFill: document.getElementById('player-hp-fill'), hpText: document.getElementById('player-hp-text'), maxHpSpan: document.getElementById('player-max-hp'),
|
||
resourceFill: document.getElementById('player-resource-fill'), resourceText: document.getElementById('player-resource-text'), maxResourceSpan: document.getElementById('player-max-resource'),
|
||
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'), // Для обновления имени в PvP
|
||
avatar: document.getElementById('opponent-panel')?.querySelector('.opponent-avatar'),
|
||
hpFill: document.getElementById('opponent-hp-fill'), hpText: document.getElementById('opponent-hp-text'), maxHpSpan: document.getElementById('opponent-max-hp'),
|
||
resourceFill: document.getElementById('opponent-resource-fill'), resourceText: document.getElementById('opponent-resource-text'), maxResourceSpan: document.getElementById('opponent-max-resource'),
|
||
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'),
|
||
modalContent: document.getElementById('game-over-screen')?.querySelector('.modal-content')
|
||
},
|
||
// Дополнительные элементы для PvP информации, если нужно
|
||
gameHeaderTitle: document.querySelector('.game-header h1') // Заголовок "Елена vs Балард"
|
||
};
|
||
|
||
/** Добавляет сообщение в лог. */
|
||
function addToLog(message, type = 'info') { // Используем строковый тип по умолчанию
|
||
const logListElement = uiElements.log.list;
|
||
if (!logListElement) {
|
||
console.warn("addToLog: Элемент списка логов не найден.");
|
||
return;
|
||
}
|
||
const li = document.createElement('li');
|
||
li.textContent = message;
|
||
|
||
// Используем типы логов из GAME_CONFIG, если он определен
|
||
// Иначе, используем переданный строковый тип как есть
|
||
const logTypeClass = window.GAME_CONFIG && window.GAME_CONFIG[`LOG_TYPE_${type.toUpperCase()}`]
|
||
? `log-${window.GAME_CONFIG[`LOG_TYPE_${type.toUpperCase()}`]}`
|
||
: `log-${type}`;
|
||
li.className = logTypeClass;
|
||
|
||
logListElement.appendChild(li);
|
||
// Плавная прокрутка к последнему сообщению
|
||
requestAnimationFrame(() => {
|
||
logListElement.scrollTop = logListElement.scrollHeight;
|
||
});
|
||
}
|
||
|
||
/** Обновляет бары, статус, имя и аватар бойца. */
|
||
function updateFighterUI(fighterRole, // 'player' (Елена) или 'opponent' (Балард) - технические роли
|
||
fighterActualId, // 'player' или 'opponent' - кто из gameState соответствует этой панели
|
||
fighterState, // gameState[fighterActualId]
|
||
fighterBaseStatsRef, // gameData[fighterActualId+'BaseStats']
|
||
isControlledByThisClient) { // true, если эта панель соответствует игроку за этим клиентом
|
||
|
||
const elements = uiElements[fighterRole]; // uiElements.player или uiElements.opponent
|
||
if (!elements || !elements.hpFill || !elements.hpText || !elements.resourceFill || !elements.resourceText || !elements.status || !fighterState || !fighterBaseStatsRef) {
|
||
console.warn(`updateFighterUI: Отсутствуют элементы/состояние/базовые статы для роли ${fighterRole} (id: ${fighterActualId}).`);
|
||
return;
|
||
}
|
||
|
||
// Обновление имени и иконки
|
||
if (elements.name) {
|
||
let nameHtml = `<i class="fas ${fighterBaseStatsRef.id === 'player' ? 'fa-hat-wizard icon-player' : 'fa-khanda icon-opponent'}"></i> ${fighterBaseStatsRef.name}`;
|
||
if (isControlledByThisClient) {
|
||
nameHtml += " (Вы)";
|
||
}
|
||
elements.name.innerHTML = nameHtml;
|
||
elements.name.style.color = fighterBaseStatsRef.id === 'player' ? 'var(--accent-player)' : 'var(--accent-opponent)';
|
||
}
|
||
// Обновление аватара (если путь к аватару есть в baseStats или приходит от сервера)
|
||
// if (elements.avatar && fighterBaseStatsRef.avatarPath) {
|
||
// elements.avatar.src = fighterBaseStatsRef.avatarPath;
|
||
// }
|
||
|
||
|
||
const maxHp = Math.max(1, fighterBaseStatsRef.maxHp);
|
||
const maxRes = Math.max(1, fighterBaseStatsRef.maxResource);
|
||
const currentHp = Math.max(0, fighterState.currentHp);
|
||
const currentRes = Math.max(0, fighterState.currentResource);
|
||
|
||
const hpPercent = (currentHp / maxHp) * 100;
|
||
const resourcePercent = (currentRes / maxRes) * 100;
|
||
|
||
elements.hpFill.style.width = `${hpPercent}%`;
|
||
elements.hpText.textContent = `${Math.round(currentHp)} / ${fighterBaseStatsRef.maxHp}`;
|
||
|
||
elements.resourceFill.style.width = `${resourcePercent}%`;
|
||
elements.resourceText.textContent = `${Math.round(currentRes)} / ${fighterBaseStatsRef.maxResource}`;
|
||
// Динамическое изменение цвета и иконки для ресурса Баларда (если он не мана)
|
||
const resourceBarContainer = elements.resourceFill.closest('.stat-bar-container');
|
||
if (resourceBarContainer) {
|
||
const iconElement = resourceBarContainer.querySelector('.bar-icon i');
|
||
if (fighterActualId === (window.GAME_CONFIG?.OPPONENT_ID || 'opponent') && fighterBaseStatsRef.resourceName !== "Мана") {
|
||
resourceBarContainer.classList.remove('mana');
|
||
resourceBarContainer.classList.add('stamina'); // Предполагаем, что это выносливость/ярость
|
||
if(iconElement) iconElement.className = 'fas fa-fire-alt'; // Иконка ярости
|
||
} else { // Для Елены или если у Баларда мана
|
||
resourceBarContainer.classList.remove('stamina');
|
||
resourceBarContainer.classList.add('mana');
|
||
if(iconElement) iconElement.className = 'fas fa-flask'; // Иконка маны
|
||
}
|
||
}
|
||
|
||
|
||
const statusText = fighterState.isBlocking ? (window.GAME_CONFIG?.STATUS_BLOCKING || 'Защищается') : (window.GAME_CONFIG?.STATUS_READY || 'Готов');
|
||
elements.status.textContent = statusText;
|
||
elements.status.classList.toggle(window.GAME_CONFIG?.CSS_CLASS_BLOCKING || 'blocking', fighterState.isBlocking);
|
||
|
||
// Обновление glow-эффекта панели
|
||
if (elements.panel) {
|
||
const accentColor = fighterActualId === (window.GAME_CONFIG?.PLAYER_ID || 'player')
|
||
? 'var(--accent-player)'
|
||
: 'var(--accent-opponent)';
|
||
const glowColor = fighterActualId === (window.GAME_CONFIG?.PLAYER_ID || 'player')
|
||
? 'var(--panel-glow-player)'
|
||
: 'var(--panel-glow-opponent)';
|
||
|
||
elements.panel.style.borderColor = accentColor;
|
||
elements.panel.style.boxShadow = `0 0 15px ${glowColor}, inset 0 0 10px rgba(0, 0, 0, 0.3)`;
|
||
}
|
||
}
|
||
|
||
|
||
function classifyAndGenerateEffectsHTMLArrays(effectsArray, fighterRole) {
|
||
const buffsHTMLStrings = [];
|
||
const debuffsHTMLStrings = [];
|
||
const config = window.GAME_CONFIG || {}; // Фоллбэк на пустой объект, если GAME_CONFIG не загружен
|
||
|
||
if (!effectsArray || effectsArray.length === 0) {
|
||
return { buffsHTMLStrings: ['Нет'], debuffsHTMLStrings: ['Нет'] };
|
||
}
|
||
|
||
effectsArray.forEach(eff => {
|
||
let isDebuff = false;
|
||
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.type === config.ACTION_TYPE_DISABLE ||
|
||
eff.type === config.ACTION_TYPE_DEBUFF ||
|
||
eff.id === 'fullSilenceByElena' || // Специфичный эффект Елены
|
||
eff.id.startsWith('playerSilencedOn_') // Эффект безмолвия от Баларда на конкретную абилку
|
||
) {
|
||
isDebuff = true;
|
||
}
|
||
|
||
if (isDebuff) {
|
||
if (eff.id === 'fullSilenceByElena' || eff.id.startsWith('playerSilencedOn_') || eff.type === config.ACTION_TYPE_DISABLE) {
|
||
effectClasses += ' effect-stun'; // Или ваш класс для дизейблов
|
||
} else {
|
||
effectClasses += ' effect-debuff';
|
||
}
|
||
debuffsHTMLStrings.push(`<span class="${effectClasses}" title="${title}">${displayText}</span>`);
|
||
} else {
|
||
if (eff.grantsBlock) {
|
||
effectClasses += ' effect-block';
|
||
} else { // Все остальное считаем баффом
|
||
effectClasses += ' effect-buff';
|
||
}
|
||
buffsHTMLStrings.push(`<span class="${effectClasses}" title="${title}">${displayText}</span>`);
|
||
}
|
||
});
|
||
return {
|
||
buffsHTMLStrings: buffsHTMLStrings.length > 0 ? buffsHTMLStrings : ['Нет'],
|
||
debuffsHTMLStrings: debuffsHTMLStrings.length > 0 ? debuffsHTMLStrings : ['Нет']
|
||
};
|
||
}
|
||
|
||
function updateEffectsUI(currentGameState, myPlayerId) {
|
||
if (!currentGameState || !window.gameData) return; // gameData тоже должен быть доступен
|
||
|
||
const playerRoleForUI = myPlayerId === currentGameState.player.id ? 'player' : 'opponent';
|
||
const opponentRoleForUI = myPlayerId === currentGameState.player.id ? 'opponent' : 'player';
|
||
|
||
// Обновление для панели, соответствующей myPlayerId
|
||
const myPanelElements = uiElements[playerRoleForUI]; // Это будет uiElements.player или uiElements.opponent
|
||
const myState = currentGameState[myPlayerId]; // Это будет gameState.player или gameState.opponent
|
||
|
||
if (myPanelElements && myState) {
|
||
const myClassifiedEffects = classifyAndGenerateEffectsHTMLArrays(myState.activeEffects, playerRoleForUI);
|
||
if (myPanelElements.buffsList) myPanelElements.buffsList.innerHTML = myClassifiedEffects.buffsHTMLStrings.join(' ');
|
||
if (myPanelElements.debuffsList) myPanelElements.debuffsList.innerHTML = myClassifiedEffects.debuffsHTMLStrings.join(' ');
|
||
}
|
||
|
||
// Обновление для панели противника
|
||
const opponentActualId = myPlayerId === currentGameState.player.id ? currentGameState.opponent.id : currentGameState.player.id;
|
||
const opponentPanelElements = uiElements[opponentRoleForUI];
|
||
const opponentState = currentGameState[opponentActualId];
|
||
|
||
if (opponentPanelElements && opponentState) {
|
||
const opponentClassifiedEffects = classifyAndGenerateEffectsHTMLArrays(opponentState.activeEffects, opponentRoleForUI);
|
||
if (opponentPanelElements.buffsList) opponentPanelElements.buffsList.innerHTML = opponentClassifiedEffects.buffsHTMLStrings.join(' ');
|
||
if (opponentPanelElements.debuffsList) opponentPanelElements.debuffsList.innerHTML = opponentClassifiedEffects.debuffsHTMLStrings.join(' ');
|
||
}
|
||
}
|
||
|
||
|
||
/** Главная функция обновления всего UI. */
|
||
function updateUI() {
|
||
// Используем глобальные gameState, gameData, GAME_CONFIG, установленные из client.js
|
||
const currentGameState = window.gameState;
|
||
const gameDataGlobal = window.gameData;
|
||
const configGlobal = window.GAME_CONFIG;
|
||
const myPlayerId = window.myPlayerId; // myPlayerId также должен быть установлен глобально или передан
|
||
|
||
if (!currentGameState || !gameDataGlobal || !configGlobal || !myPlayerId) {
|
||
console.warn("updateUI: Отсутствуют глобальные gameState, gameData, GAME_CONFIG или myPlayerId.");
|
||
return;
|
||
}
|
||
if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) {
|
||
console.warn("updateUI: Некоторые базовые uiElements не найдены.");
|
||
return;
|
||
}
|
||
|
||
// Определяем, какая панель (player/opponent в uiElements) соответствует какому игроку (player/opponent в gameState)
|
||
// и кто из них управляется этим клиентом.
|
||
const playerPanelRepresents = currentGameState.player.id; // Кого отображает панель "player"
|
||
const opponentPanelRepresents = currentGameState.opponent.id; // Кого отображает панель "opponent"
|
||
|
||
updateFighterUI('player', playerPanelRepresents, currentGameState[playerPanelRepresents], gameDataGlobal[playerPanelRepresents === 'player' ? 'playerBaseStats' : 'opponentBaseStats'], playerPanelRepresents === myPlayerId);
|
||
updateFighterUI('opponent', opponentPanelRepresents, currentGameState[opponentPanelRepresents], gameDataGlobal[opponentPanelRepresents === 'player' ? 'playerBaseStats' : 'opponentBaseStats'], opponentPanelRepresents === myPlayerId);
|
||
|
||
updateEffectsUI(currentGameState, myPlayerId);
|
||
|
||
// Обновление заголовка игры (Елена vs Балард)
|
||
if (uiElements.gameHeaderTitle && gameDataGlobal.playerBaseStats && gameDataGlobal.opponentBaseStats) {
|
||
uiElements.gameHeaderTitle.innerHTML = `<span class="title-enchantress">${gameDataGlobal.playerBaseStats.name}</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="title-knight">${gameDataGlobal.opponentBaseStats.name}</span>`;
|
||
}
|
||
|
||
|
||
if (uiElements.controls.turnIndicator) {
|
||
const currentTurnActorId = currentGameState.isPlayerTurn ? currentGameState.player.id : currentGameState.opponent.id;
|
||
const currentTurnName = currentGameState[currentTurnActorId].name;
|
||
uiElements.controls.turnIndicator.textContent = `Ход: ${currentTurnName}`;
|
||
uiElements.controls.turnIndicator.style.color = currentTurnActorId === configGlobal.PLAYER_ID ? 'var(--accent-player)' : 'var(--accent-opponent)';
|
||
}
|
||
|
||
const canThisClientAct = (currentGameState.isPlayerTurn && myPlayerId === currentGameState.player.id) ||
|
||
(!currentGameState.isPlayerTurn && myPlayerId === currentGameState.opponent.id);
|
||
const isGameActive = !currentGameState.isGameOver;
|
||
|
||
// Обновление кнопки атаки
|
||
if (uiElements.controls.buttonAttack) {
|
||
uiElements.controls.buttonAttack.disabled = !(canThisClientAct && isGameActive);
|
||
// Подсветка атаки от "Силы Природы" (только если myPlayerId - это Елена)
|
||
if (myPlayerId === configGlobal.PLAYER_ID) {
|
||
const isNatureBuffReady = currentGameState.player.activeEffects.some(eff => eff.id === configGlobal.ABILITY_ID_NATURE_STRENGTH && !eff.justCast);
|
||
uiElements.controls.buttonAttack.classList.toggle(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed', isNatureBuffReady && canThisClientAct && isGameActive);
|
||
} else {
|
||
uiElements.controls.buttonAttack.classList.remove(configGlobal.CSS_CLASS_ATTACK_BUFFED || 'attack-buffed');
|
||
}
|
||
}
|
||
// Кнопка блока всегда отключена
|
||
if (uiElements.controls.buttonBlock) {
|
||
uiElements.controls.buttonBlock.disabled = true;
|
||
}
|
||
|
||
// Обновление кнопок способностей для текущего игрока (myPlayerId)
|
||
const actingPlayerState = currentGameState[myPlayerId];
|
||
const actingPlayerAbilities = myPlayerId === configGlobal.PLAYER_ID ? gameDataGlobal.playerAbilities : gameDataGlobal.opponentAbilities;
|
||
|
||
uiElements.controls.abilitiesGrid?.querySelectorAll(`.${configGlobal.CSS_CLASS_ABILITY_BUTTON || 'ability-button'}`).forEach(button => {
|
||
if (!(button instanceof HTMLButtonElement) || !actingPlayerState || !actingPlayerAbilities) {
|
||
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 === abilityId);
|
||
|
||
const silencedInfo = actingPlayerState.disabledAbilities?.find(dis => dis.abilityId === abilityId);
|
||
const isSilencedByOpponent = !!silencedInfo;
|
||
|
||
const cooldownTurnsLeft = actingPlayerState.abilityCooldowns?.[ability.id] || 0;
|
||
const isOnCooldown = cooldownTurnsLeft > 0;
|
||
|
||
let isDisabledByDebuffOnTarget = false; // Для специфичных дебаффов типа Печати Слабости
|
||
if (ability.id === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS && myPlayerId === configGlobal.PLAYER_ID) { // Только Елена может накладывать
|
||
const opponentActualId = myPlayerId === currentGameState.player.id ? currentGameState.opponent.id : currentGameState.player.id;
|
||
const effectIdForDebuff = 'effect_' + ability.id;
|
||
isDisabledByDebuffOnTarget = currentGameState[opponentActualId].activeEffects.some(e => e.id === effectIdForDebuff);
|
||
}
|
||
|
||
|
||
let finalDisabledState = !(canThisClientAct && isGameActive) || !hasEnoughResource || isBuffAlreadyActive || isSilencedByOpponent || isOnCooldown || isDisabledByDebuffOnTarget;
|
||
button.disabled = finalDisabledState;
|
||
|
||
// Стилизация кнопки
|
||
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 = `КД: ${cooldownTurnsLeft}`;
|
||
cooldownDisplay.style.display = 'block';
|
||
}
|
||
} else {
|
||
if (cooldownDisplay) cooldownDisplay.style.display = 'none';
|
||
// Применяем другие классы только если не на КД
|
||
button.classList.toggle(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE || 'not-enough-resource', canThisClientAct && isGameActive && !hasEnoughResource && !isBuffAlreadyActive && !isSilencedByOpponent && !isDisabledByDebuffOnTarget);
|
||
button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE || 'buff-is-active', canThisClientAct && isGameActive && isBuffAlreadyActive && !isSilencedByOpponent); // Для баффов на себя
|
||
button.classList.toggle(configGlobal.CSS_CLASS_ABILITY_SILENCED || 'is-silenced', isSilencedByOpponent);
|
||
// Если Печать Слабости уже на цели, можно добавить особый класс или просто дизейблить
|
||
if (isDisabledByDebuffOnTarget) {
|
||
// button.classList.add('debuff-already-active'); // Пример
|
||
}
|
||
}
|
||
|
||
|
||
// Обновление Tooltip
|
||
const resourceName = actingPlayerState.resourceName;
|
||
let titleText = `${ability.name} (${ability.cost} ${resourceName}) - ${ability.description}`;
|
||
if (typeof ability.descriptionFunction === 'function') {
|
||
const configForDesc = window.clientSideConfig?.GAME_CONFIG_FOR_ABILITIES || configGlobal;
|
||
const targetStatsForDesc = (myPlayerId === configGlobal.PLAYER_ID)
|
||
? (window.gameData?.opponentBaseStats)
|
||
: (window.gameData?.playerBaseStats);
|
||
titleText = `${ability.name} (${ability.cost} ${resourceName}) - ${ability.descriptionFunction(configForDesc, targetStatsForDesc)}`;
|
||
}
|
||
|
||
|
||
let abilityBaseCooldown = ability.cooldown;
|
||
if (myPlayerId === configGlobal.OPPONENT_ID) { // Если Балард-игрок
|
||
if (ability.internalCooldownFromConfig && configGlobal) {
|
||
abilityBaseCooldown = configGlobal[ability.internalCooldownFromConfig];
|
||
} else if (ability.internalCooldownValue) {
|
||
abilityBaseCooldown = ability.internalCooldownValue;
|
||
}
|
||
}
|
||
if (abilityBaseCooldown) titleText += ` (КД: ${abilityBaseCooldown} х.)`;
|
||
|
||
|
||
if (isOnCooldown) {
|
||
titleText = `${ability.name} - На перезарядке! Осталось: ${cooldownTurnsLeft} х.`;
|
||
} else if (isSilencedByOpponent) {
|
||
titleText = `Заглушено! Осталось: ${silencedInfo.turnsLeft} х.`;
|
||
} else if (isBuffAlreadyActive) {
|
||
const activeEffect = actingPlayerState.activeEffects.find(eff => eff.id === abilityId);
|
||
titleText = `Эффект "${ability.name}" уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}`;
|
||
} else if (isDisabledByDebuffOnTarget) {
|
||
const opponentActualId = myPlayerId === currentGameState.player.id ? currentGameState.opponent.id : currentGameState.player.id;
|
||
const activeDebuff = currentGameState[opponentActualId].activeEffects.find(e => e.id === 'effect_' + ability.id);
|
||
titleText = `Эффект "${ability.name}" уже наложен на ${currentGameState[opponentActualId].name}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}`;
|
||
}
|
||
button.setAttribute('title', titleText);
|
||
});
|
||
}
|
||
|
||
|
||
/** Показывает экран конца игры с задержкой. */
|
||
function showGameOver(playerWon, reason = "") {
|
||
const config = window.GAME_CONFIG || {};
|
||
const gameDataGlobal = window.gameData || {};
|
||
|
||
const gameOverScreenElement = uiElements.gameOver.screen;
|
||
if (!gameOverScreenElement) return;
|
||
const resultMsgElement = uiElements.gameOver.message;
|
||
const opponentPanelElement = uiElements.opponent.panel; // Панель Баларда в UI
|
||
|
||
if (resultMsgElement) {
|
||
let winText = `Победа! ${gameDataGlobal.playerBaseStats ? gameDataGlobal.playerBaseStats.name : 'Игрок'} празднует!`;
|
||
let loseText = `Поражение! ${gameDataGlobal.opponentBaseStats ? gameDataGlobal.opponentBaseStats.name : 'Противник'} оказался сильнее!`;
|
||
if (reason === 'opponent_disconnected') {
|
||
if (playerWon) { // Если "мы" победили из-за дисконнекта оппонента
|
||
winText = `Противник покинул игру. Победа присуждается вам!`;
|
||
} else { // Если "мы" были оппонентом, который остался
|
||
loseText = `Игрок покинул игру. Техническая победа.`;
|
||
}
|
||
}
|
||
|
||
if (playerWon) {
|
||
resultMsgElement.textContent = winText;
|
||
resultMsgElement.style.color = 'var(--heal-color)';
|
||
} else {
|
||
resultMsgElement.textContent = loseText;
|
||
resultMsgElement.style.color = 'var(--damage-color)';
|
||
}
|
||
}
|
||
|
||
// Анимация растворения панели Баларда, если победила Елена (и это не дисконнект)
|
||
// И если панель оппонента действительно отображает Баларда
|
||
const opponentPanelRepresents = window.gameState?.opponent.id; // Кто сейчас на панели оппонента
|
||
if (playerWon && reason !== 'opponent_disconnected' &&
|
||
opponentPanelElement && opponentPanelRepresents === config.OPPONENT_ID) {
|
||
// addToLog вызывается из client.js при получении gameOver от сервера
|
||
opponentPanelElement.classList.add('dissolving');
|
||
} else if (opponentPanelElement) {
|
||
// Убираем класс, если он был, на случай рестарта
|
||
opponentPanelElement.classList.remove('dissolving');
|
||
}
|
||
|
||
|
||
setTimeout(() => {
|
||
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);
|
||
});
|
||
}, config.DELAY_BEFORE_VICTORY_MODAL || 1500);
|
||
}
|
||
|
||
// --- Экспорт ---
|
||
// Функции теперь доступны через window.gameUI
|
||
window.gameUI = {
|
||
uiElements,
|
||
addToLog,
|
||
updateUI,
|
||
showGameOver
|
||
// classifyAndGenerateEffectsHTMLArrays, // Можно сделать доступным, если нужно извне
|
||
// updateFighterUI,
|
||
// updateEffectsUI
|
||
};
|
||
})();
|
||
// console.log("ui.js loaded and initialized.", window.gameUI);
|