diff --git a/public/js/client.js b/public/js/client.js index 1d07c20..3c42a43 100644 --- a/public/js/client.js +++ b/public/js/client.js @@ -17,6 +17,7 @@ document.addEventListener('DOMContentLoaded', () => { let opponentAbilitiesServer = null; let isLoggedIn = false; let loggedInUsername = ''; + let isInGame = false; // ФЛАГ СОСТОЯНИЯ ИГРЫ // --- DOM Элементы --- // Аутентификация @@ -24,6 +25,7 @@ document.addEventListener('DOMContentLoaded', () => { const registerForm = document.getElementById('register-form'); const loginForm = document.getElementById('login-form'); const authMessage = document.getElementById('auth-message'); + const statusContainer = document.getElementById('status-container'); // Добавим ссылку на контейнер const userInfoDiv = document.getElementById('user-info'); const loggedInUsernameSpan = document.getElementById('logged-in-username'); const logoutButton = document.getElementById('logout-button'); @@ -32,7 +34,7 @@ document.addEventListener('DOMContentLoaded', () => { const gameSetupDiv = document.getElementById('game-setup'); const createAIGameButton = document.getElementById('create-ai-game'); const createPvPGameButton = document.getElementById('create-pvp-game'); - const joinPvPGameButton = document.getElementById('join-pvp-game'); + const joinPvPGameButton = document.getElementById('join-pvP-game'); const findRandomPvPGameButton = document.getElementById('find-random-pvp-game'); const gameIdInput = document.getElementById('game-id-input'); const availableGamesDiv = document.getElementById('available-games-list'); @@ -44,6 +46,8 @@ document.addEventListener('DOMContentLoaded', () => { const attackButton = document.getElementById('button-attack'); const returnToMenuButton = document.getElementById('return-to-menu-button'); const gameOverScreen = document.getElementById('game-over-screen'); + const abilitiesGrid = document.getElementById('abilities-grid'); + console.log('Client.js DOMContentLoaded. Initializing elements...'); @@ -56,8 +60,12 @@ document.addEventListener('DOMContentLoaded', () => { if (gameSetupDiv) gameSetupDiv.style.display = 'none'; if (gameWrapper) gameWrapper.style.display = 'none'; hideGameOverModal(); - setGameStatusMessage("Войдите или зарегистрируйтесь для начала игры."); - setAuthMessage(""); + // setGameStatusMessage("Войдите или зарегистрируйтесь для начала игры."); // Это сообщение перенесено в setAuthMessage/начальный статус + setAuthMessage("Ожидание подключения к серверу..."); // Начальный статус + if (statusContainer) statusContainer.style.display = 'block'; // Убедимся, что статус виден + isInGame = false; + disableGameControls(); + resetGameVariables(); // Сбрасываем переменные игры при выходе на экран логина } function showGameSelectionScreen(username) { @@ -71,11 +79,15 @@ document.addEventListener('DOMContentLoaded', () => { if (gameWrapper) gameWrapper.style.display = 'none'; hideGameOverModal(); setGameStatusMessage("Выберите режим игры или присоединитесь к существующей."); + if (statusContainer) statusContainer.style.display = 'block'; // Убедимся, что статус виден socket.emit('requestPvPGameList'); updateAvailableGamesList([]); if (gameIdInput) gameIdInput.value = ''; const elenaRadio = document.getElementById('char-elena'); if (elenaRadio) elenaRadio.checked = true; + isInGame = false; + disableGameControls(); + resetGameVariables(); // Сбрасываем переменные игры при выходе на экран выбора игры } function showGameScreen() { @@ -85,30 +97,48 @@ document.addEventListener('DOMContentLoaded', () => { if (userInfoDiv) userInfoDiv.style.display = 'block'; if (gameSetupDiv) gameSetupDiv.style.display = 'none'; if (gameWrapper) gameWrapper.style.display = 'flex'; - setGameStatusMessage(""); + setGameStatusMessage(""); // Очищаем статус игры, т.к. теперь есть индикатор хода + if (statusContainer) statusContainer.style.display = 'none'; // Скрываем статус контейнер в игре + isInGame = true; + disableGameControls(); // Отключаем кнопки изначально, updateUI их включит при ходе } + // <--- НОВАЯ ФУНКЦИЯ ДЛЯ СБРОСА ИГРОВЫХ ПЕРЕМЕННЫХ --- + function resetGameVariables() { + currentGameId = null; + currentGameState = null; + myPlayerId = null; + myCharacterKey = null; + opponentCharacterKey = null; + playerBaseStatsServer = null; + opponentBaseStatsServer = null; + playerAbilitiesServer = null; + opponentAbilitiesServer = null; + + window.gameState = null; + window.gameData = null; + window.myPlayerId = null; + // window.GAME_CONFIG = null; // Не сбрасываем, т.к. содержит общие вещи + } + // --- КОНЕЦ НОВОЙ ФУНКЦИИ --- + + function hideGameOverModal() { const hiddenClass = (window.GAME_CONFIG && window.GAME_CONFIG.CSS_CLASS_HIDDEN) ? window.GAME_CONFIG.CSS_CLASS_HIDDEN : 'hidden'; if (gameOverScreen && !gameOverScreen.classList.contains(hiddenClass)) { console.log('[Client.js DEBUG] Hiding GameOver Modal.'); gameOverScreen.classList.add(hiddenClass); - if (window.gameUI && gameUI.uiElements && gameUI.uiElements.gameOver && gameUI.uiElements.gameOver.modalContent) { - gameUI.uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)'; - gameUI.uiElements.gameOver.modalContent.style.opacity = '0'; + if (window.gameUI?.uiElements?.gameOver?.modalContent) { + window.gameUI.uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)'; + window.gameUI.uiElements.gameOver.modalContent.style.opacity = '0'; } - if (window.gameUI && window.gameUI.uiElements && window.gameUI.uiElements.opponent && window.gameUI.uiElements.opponent.panel) { + if (window.gameUI?.uiElements?.opponent?.panel) { const opponentPanel = window.gameUI.uiElements.opponent.panel; if (opponentPanel.classList.contains('dissolving')) { console.log('[Client.js DEBUG] Removing .dissolving from opponent panel during hideGameOverModal.'); opponentPanel.classList.remove('dissolving'); - const originalTransition = opponentPanel.style.transition; - opponentPanel.style.transition = 'none'; opponentPanel.style.opacity = '1'; opponentPanel.style.transform = 'scale(1) translateY(0)'; - requestAnimationFrame(() => { - opponentPanel.style.transition = originalTransition || ''; - }); } } } @@ -120,6 +150,10 @@ document.addEventListener('DOMContentLoaded', () => { authMessage.className = isError ? 'error' : 'success'; authMessage.style.display = message ? 'block' : 'none'; } + // Скрываем gameStatusMessage, если показываем authMessage + if (message && gameStatusMessage) { + gameStatusMessage.style.display = 'none'; + } } function setGameStatusMessage(message, isError = false) { @@ -127,6 +161,11 @@ document.addEventListener('DOMContentLoaded', () => { gameStatusMessage.textContent = message; gameStatusMessage.style.display = message ? 'block' : 'none'; gameStatusMessage.style.color = isError ? 'var(--damage-color, red)' : 'var(--turn-color, yellow)'; + if (statusContainer) statusContainer.style.display = message ? 'block' : 'none'; // Показываем контейнер статуса + } + // Скрываем authMessage, если показываем gameStatusMessage + if (message && authMessage) { + authMessage.style.display = 'none'; } } @@ -142,6 +181,22 @@ document.addEventListener('DOMContentLoaded', () => { return selectedKey; } + function enableGameControls(enableAttack = true, enableAbilities = true) { + if (attackButton) attackButton.disabled = !enableAttack; + if (abilitiesGrid) { + const abilityButtonClass = window.GAME_CONFIG?.CSS_CLASS_ABILITY_BUTTON || 'ability-button'; + abilitiesGrid.querySelectorAll(`.${abilityButtonClass}`).forEach(button => { + button.disabled = !enableAbilities; + }); + } + if (window.gameUI?.uiElements?.controls?.buttonBlock) window.gameUI.uiElements.controls.buttonBlock.disabled = true; + } + + function disableGameControls() { + enableGameControls(false, false); + } + + // --- Инициализация кнопок и обработчиков --- if (registerForm) { @@ -150,6 +205,9 @@ document.addEventListener('DOMContentLoaded', () => { const usernameInput = document.getElementById('register-username'); const passwordInput = document.getElementById('register-password'); if (usernameInput && passwordInput) { + // Отключаем кнопки на время регистрации + registerForm.querySelector('button').disabled = true; + loginForm.querySelector('button').disabled = true; socket.emit('register', { username: usernameInput.value, password: passwordInput.value }); } else { setAuthMessage("Ошибка: поля ввода не найдены.", true); @@ -163,6 +221,9 @@ document.addEventListener('DOMContentLoaded', () => { const usernameInput = document.getElementById('login-username'); const passwordInput = document.getElementById('login-password'); if (usernameInput && passwordInput) { + // Отключаем кнопки на время логина + registerForm.querySelector('button').disabled = true; + loginForm.querySelector('button').disabled = true; socket.emit('login', { username: usernameInput.value, password: passwordInput.value }); } else { setAuthMessage("Ошибка: поля ввода не найдены.", true); @@ -172,16 +233,18 @@ document.addEventListener('DOMContentLoaded', () => { if (logoutButton) { logoutButton.addEventListener('click', () => { + // Отключаем кнопку выхода + logoutButton.disabled = true; socket.emit('logout'); - isLoggedIn = false; - loggedInUsername = ''; - currentGameId = null; currentGameState = null; myPlayerId = null; - myCharacterKey = null; opponentCharacterKey = null; - playerBaseStatsServer = null; opponentBaseStatsServer = null; - playerAbilitiesServer = null; opponentAbilitiesServer = null; - window.gameState = null; window.gameData = null; window.myPlayerId = null; + // Сброс состояния и UI происходит по событию logoutResponse или gameNotFound/gameEnded после logout + // Пока просто сбрасываем флаги и показываем Auth, т.к. сервер не присылает специальный logoutResponse + isLoggedIn = false; loggedInUsername = ''; + resetGameVariables(); + isInGame = false; + disableGameControls(); showAuthScreen(); - setGameStatusMessage("Вы вышли из системы."); + setGameStatusMessage("Вы вышли из системы."); // Используем gameStatusMessage для уведомления + logoutButton.disabled = false; // Включаем кнопку после обработки (хотя она будет скрыта) }); } @@ -190,6 +253,8 @@ document.addEventListener('DOMContentLoaded', () => { if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы начать игру.", true); return; } + // Отключаем кнопки настройки игры + disableSetupButtons(); socket.emit('createGame', { mode: 'ai', characterKey: 'elena' }); setGameStatusMessage("Создание игры против AI..."); }); @@ -199,6 +264,8 @@ document.addEventListener('DOMContentLoaded', () => { if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы начать игру.", true); return; } + // Отключаем кнопки настройки игры + disableSetupButtons(); const selectedCharacter = getSelectedCharacterKey(); socket.emit('createGame', { mode: 'pvp', characterKey: selectedCharacter }); setGameStatusMessage(`Создание PvP игры за ${selectedCharacter === 'elena' ? 'Елену' : 'Альмагест'}...`); @@ -211,6 +278,8 @@ document.addEventListener('DOMContentLoaded', () => { } const gameIdToJoin = gameIdInput.value.trim(); if (gameIdToJoin) { + // Отключаем кнопки настройки игры + disableSetupButtons(); socket.emit('joinGame', { gameId: gameIdToJoin }); setGameStatusMessage(`Присоединение к игре ${gameIdToJoin}...`); } else { @@ -223,16 +292,42 @@ document.addEventListener('DOMContentLoaded', () => { if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы найти игру.", true); return; } + // Отключаем кнопки настройки игры + disableSetupButtons(); const selectedCharacter = getSelectedCharacterKey(); socket.emit('findRandomGame', { characterKey: selectedCharacter }); setGameStatusMessage(`Поиск случайной PvP игры (предпочтение: ${selectedCharacter === 'elena' ? 'Елена' : 'Альмагест'})...`); }); } + // Функция для отключения кнопок на экране настройки игры + function disableSetupButtons() { + if(createAIGameButton) createAIGameButton.disabled = true; + if(createPvPGameButton) createPvPGameButton.disabled = true; + if(joinPvPGameButton) joinPvPGameButton.disabled = true; + if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = true; + if(availableGamesDiv) availableGamesDiv.querySelectorAll('button').forEach(btn => btn.disabled = true); + } + // Функция для включения кнопок на экране настройки игры (вызывается после получения списка игр или сброса состояния) + function enableSetupButtons() { + if(createAIGameButton) createAIGameButton.disabled = false; + if(createPvPGameButton) createPvPGameButton.disabled = false; + if(joinPvPGameButton) joinPvPGameButton.disabled = false; + if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = false; + // Кнопки Join в списке игр включаются при обновлении списка (updateAvailableGamesList) + } + if (attackButton) { attackButton.addEventListener('click', () => { - if (currentGameId && currentGameState && !currentGameState.isGameOver && isLoggedIn) { + // Проверяем isInGame и другие флаги перед отправкой действия + if (isLoggedIn && isInGame && currentGameId && currentGameState && !currentGameState.isGameOver) { socket.emit('playerAction', { actionType: 'attack' }); + } else { + console.warn('[Client] Попытка действия (атака) вне допустимого состояния игры. isLogged:', isLoggedIn, 'isInGame:', isInGame); + disableGameControls(); // Гарантируем, что кнопки будут отключены + // Если мы залогинены, но не в игре (isInGame=false), возможно, стоит вернуться в меню выбора игры + if (isLoggedIn && !isInGame) showGameSelectionScreen(loggedInUsername); + else if (!isLoggedIn) showAuthScreen(); } }); } @@ -240,75 +335,72 @@ document.addEventListener('DOMContentLoaded', () => { function handleAbilityButtonClick(event) { const button = event.currentTarget; const abilityId = button.dataset.abilityId; - if (currentGameId && abilityId && currentGameState && !currentGameState.isGameOver && isLoggedIn) { + // Проверяем isInGame и другие флаги перед отправкой действия + if (isLoggedIn && isInGame && currentGameId && abilityId && currentGameState && !currentGameState.isGameOver) { socket.emit('playerAction', { actionType: 'ability', abilityId: abilityId }); + } else { + console.warn('[Client] Попытка действия (способность) вне допустимого состояния игры. isLogged:', isLoggedIn, 'isInGame:', isInGame); + disableGameControls(); // Гарантируем, что кнопки будут отключены + if (isLoggedIn && !isInGame) showGameSelectionScreen(loggedInUsername); + else if (!isLoggedIn) showAuthScreen(); } } if (returnToMenuButton) { returnToMenuButton.addEventListener('click', () => { if (!isLoggedIn) { - showAuthScreen(); + showAuthScreen(); // Если каким-то образом кнопка активна без логина return; } + // Отключаем кнопку возврата в меню + returnToMenuButton.disabled = true; - console.log('[Client] Return to menu button clicked.'); - currentGameId = null; - currentGameState = null; - myPlayerId = null; - myCharacterKey = null; - opponentCharacterKey = null; - playerBaseStatsServer = null; - opponentBaseStatsServer = null; - playerAbilitiesServer = null; - opponentAbilitiesServer = null; + console.log('[Client] Return to menu button clicked. Resetting game state and showing selection screen.'); + // Сбрасываем все переменные состояния игры и глобальные ссылки + resetGameVariables(); + isInGame = false; + disableGameControls(); // Убедимся, что игровые кнопки отключены + hideGameOverModal(); // Убедимся, что модалка скрыта - window.gameState = null; - window.gameData = null; - window.myPlayerId = null; - - showGameSelectionScreen(loggedInUsername); + showGameSelectionScreen(loggedInUsername); // Возвращаемся на экран выбора игры + // Кнопки настройки игры будут включены в showGameSelectionScreen / updateAvailableGamesList }); } function initializeAbilityButtons() { - const abilitiesGrid = document.getElementById('abilities-grid'); if (!abilitiesGrid || !window.gameUI || !window.GAME_CONFIG) { if(abilitiesGrid) abilitiesGrid.innerHTML = '

Ошибка загрузки способностей.

'; + console.error('[Client.js] initializeAbilityButtons failed: abilitiesGrid, gameUI, or GAME_CONFIG not found.'); return; } abilitiesGrid.innerHTML = ''; const config = window.GAME_CONFIG; - const abilitiesToDisplay = playerAbilitiesServer; // Используем данные, сохраненные при gameStarted - const baseStatsForResource = playerBaseStatsServer; // Используем данные, сохраненные при gameStarted + const abilitiesToDisplay = playerAbilitiesServer; + const baseStatsForResource = playerBaseStatsServer; if (!abilitiesToDisplay || abilitiesToDisplay.length === 0 || !baseStatsForResource) { abilitiesGrid.innerHTML = '

Нет доступных способностей.

'; return; } const resourceName = baseStatsForResource.resourceName || "Ресурс"; + const abilityButtonClass = config.CSS_CLASS_ABILITY_BUTTON || 'ability-button'; abilitiesToDisplay.forEach(ability => { const button = document.createElement('button'); button.id = `ability-btn-${ability.id}`; - button.classList.add(config.CSS_CLASS_ABILITY_BUTTON || 'ability-button'); + button.classList.add(abilityButtonClass); button.dataset.abilityId = ability.id; let descriptionText = ability.description; - if (typeof ability.descriptionFunction === 'function') { - const targetStatsForDesc = opponentBaseStatsServer; // Используем данные, сохраненные при gameStarted - descriptionText = ability.descriptionFunction(config, targetStatsForDesc); + + let cooldown = ability.cooldown; + let cooldownText = ""; + if (typeof cooldown === 'number' && cooldown > 0) { + cooldownText = ` (КД: ${cooldown} х.)`; } - let title = `${ability.name} (${ability.cost} ${resourceName}) - ${descriptionText}`; - let cooldown = ability.cooldown; - if (ability.internalCooldownFromConfig && config[ability.internalCooldownFromConfig]) { - cooldown = config[ability.internalCooldownFromConfig]; - } else if (ability.internalCooldownValue) { - cooldown = ability.internalCooldownValue; - } - if (cooldown) title += ` (КД: ${cooldown} х.)`; + let title = `${ability.name} (${ability.cost} ${resourceName})${cooldownText} - ${descriptionText || 'Нет описания'}`; button.setAttribute('title', title); const nameSpan = document.createElement('span'); @@ -316,16 +408,24 @@ document.addEventListener('DOMContentLoaded', () => { button.appendChild(nameSpan); const descSpan = document.createElement('span'); - descSpan.classList.add('ability-desc'); descSpan.textContent = `(${ability.cost} ${resourceName})`; + descSpan.classList.add('ability-desc'); + descSpan.textContent = `(${ability.cost} ${resourceName})`; + button.appendChild(descSpan); const cdDisplay = document.createElement('span'); - cdDisplay.classList.add('ability-cooldown-display'); cdDisplay.style.display = 'none'; + cdDisplay.classList.add('ability-cooldown-display'); + cdDisplay.style.display = 'none'; button.appendChild(cdDisplay); button.addEventListener('click', handleAbilityButtonClick); + abilitiesGrid.appendChild(button); }); + const placeholder = abilitiesGrid.querySelector('.placeholder-text'); + if (placeholder) placeholder.remove(); + + // Кнопки инициализированы, updateUI будет управлять их disabled состоянием } function updateAvailableGamesList(games) { @@ -344,6 +444,8 @@ document.addEventListener('DOMContentLoaded', () => { if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true); return; } + // Отключаем кнопки настройки игры перед присоединением + disableSetupButtons(); socket.emit('joinGame', { gameId: e.target.dataset.gameId }); }); li.appendChild(joinBtn); @@ -351,229 +453,348 @@ document.addEventListener('DOMContentLoaded', () => { } }); availableGamesDiv.appendChild(ul); + // Включаем кнопки JOIN в списке + availableGamesDiv.querySelectorAll('button').forEach(btn => btn.disabled = false); } else { availableGamesDiv.innerHTML += '

Нет доступных игр. Создайте свою!

'; } + enableSetupButtons(); // Включаем основные кнопки создания игры после обновления списка } // --- Обработчики событий Socket.IO --- socket.on('connect', () => { console.log('[Client] Socket connected to server! Socket ID:', socket.id); - if (!isLoggedIn) { - showAuthScreen(); + // При подключении, если залогинен, запросить состояние игры. + // Это нужно ТОЛЬКО для восстановления игры, если клиент был в игре и переподключился. + if (isLoggedIn) { + console.log(`[Client] Reconnected as ${loggedInUsername}. Requesting state.`); + socket.emit('requestGameState'); } else { - console.log(`[Client] Reconnected as ${loggedInUsername}. Requesting state or showing game selection.`); - showGameSelectionScreen(loggedInUsername); + // Если не залогинен, показываем экран аутентификации + showAuthScreen(); } }); - socket.on('disconnect', (reason) => { - console.log('[Client] Disconnected from server:', reason); - setGameStatusMessage(`Отключено от сервера: ${reason}. Попробуйте обновить страницу.`, true); - hideGameOverModal(); - }); - + // Обработка registerResponse - теперь включает включение кнопок форм socket.on('registerResponse', (data) => { setAuthMessage(data.message, !data.success); if (data.success && registerForm) registerForm.reset(); + // Включаем кнопки форм обратно + if(registerForm) registerForm.querySelector('button').disabled = false; + if(loginForm) loginForm.querySelector('button').disabled = false; }); + // Обработка loginResponse - Ключевое изменение здесь socket.on('loginResponse', (data) => { setAuthMessage(data.message, !data.success); if (data.success) { - isLoggedIn = true; loggedInUsername = data.username; - setAuthMessage(""); showGameSelectionScreen(data.username); + isLoggedIn = true; + loggedInUsername = data.username; + setAuthMessage(""); // Очищаем сообщение аутентификации + + // --- ИЗМЕНЕНИЕ: СРАЗУ ПОКАЗЫВАЕМ ЭКРАН ВЫБОРА ИГРЫ --- + // Не ждем gameNotFound или gameState. Сразу переходим. + showGameSelectionScreen(data.username); + // enableSetupButtons() вызывается внутри showGameSelectionScreen / updateAvailableGamesList + // --- КОНЕЦ ИЗМЕНЕНИЯ --- + } else { - isLoggedIn = false; loggedInUsername = ''; + isLoggedIn = false; + loggedInUsername = ''; + // Включаем кнопки форм обратно при ошибке логина + if(registerForm) registerForm.querySelector('button').disabled = false; + if(loginForm) loginForm.querySelector('button').disabled = false; } }); - socket.on('gameCreated', (data) => { - if (!isLoggedIn) return; - currentGameId = data.gameId; - myPlayerId = data.yourPlayerId; // Запоминаем наш технический ID слота - console.log(`[Client] Game created/joined: ${currentGameId}, Mode: ${data.mode}, You (${loggedInUsername}) are in slot: ${myPlayerId}`); - if (data.mode === 'pvp') { - if (gameIdInput) gameIdInput.value = currentGameId; - setGameStatusMessage(`PvP игра ${currentGameId.substring(0,8)}... создана. ID для друга: ${currentGameId}. Ожидание второго игрока...`); + // gameNotFound теперь обрабатывается иначе для залогиненных vs не залогиненных + socket.on('gameNotFound', (data) => { + console.log('[Client] Game not found response:', data?.message); + + // Сбрасываем игровые переменные, если они были установлены (например, после дисконнекта в игре) + resetGameVariables(); + isInGame = false; + disableGameControls(); // Убеждаемся, что игровые кнопки отключены + hideGameOverModal(); // Убеждаемся, что модалка скрыта + + if (isLoggedIn) { + // Если залогинен, и игра не найдена, это НОРМАЛЬНОЕ состояние, если он не был в игре. + // Просто показываем экран выбора игры. Сообщение может быть информационным, а не ошибкой. + showGameSelectionScreen(loggedInUsername); + // Сообщение: "Игровая сессия не найдена" может быть показано, но как статус, не ошибка. + // Можно сделать его менее тревожным или вовсе не показывать. + // setGameStatusMessage(data?.message || "Активная игровая сессия не найдена.", false); // Информационный статус + setGameStatusMessage("Выберите режим игры или присоединитесь к существующей."); // Сбрасываем на стандартное сообщение + enableSetupButtons(); // Включаем кнопки настройки игры } else { - setGameStatusMessage(`Игра против AI ${currentGameId.substring(0,8)}... создана. Ожидание начала...`); + // Если не залогинен и получил gameNotFound (что странно), сбрасываем и показываем логин + showAuthScreen(); + setAuthMessage(data?.message || "Пожалуйста, войдите, чтобы начать новую игру.", false); } }); + + socket.on('disconnect', (reason) => { + console.log('[Client] Disconnected from server:', reason); + setGameStatusMessage(`Отключено от сервера: ${reason}. Пожалуйста, обновите страницу.`, true); + + // Отключаем игровые кнопки, чтобы предотвратить отправку действий + disableGameControls(); + + // НЕ сбрасываем игровые переменные немедленно. + // Если мы были в игре (isInGame=true), возможно, сервер пришлет gameOver или gameNotFound позже. + // Если game over придет, его обработчик покажет модалку и включит кнопку "В меню". + // Если gameNotFound придет, его обработчик сбросит переменные и переключит UI. + // Если ничего не придет, страница может зависнуть. + // В продакшене тут может быть таймер на принудительный сброс и возврат в меню. + + // Если мы не были в игре (например, на экране выбора игры), просто показываем статус. + if (!isInGame) { + // Остаемся на текущем экране (выбора игры или логина) и показываем статус дисконнекта + // UI уже настроен showGameSelectionScreen или showAuthScreen + } + }); + + // Обработка gameStarted - без изменений socket.on('gameStarted', (data) => { - if (!isLoggedIn) return; + if (!isLoggedIn) { + console.warn('[Client] Ignoring gameStarted: Not logged in.'); + return; + } console.log('[Client] Event "gameStarted" received:', data); - if (window.gameUI && window.gameUI.uiElements && window.gameUI.uiElements.opponent && window.gameUI.uiElements.opponent.panel) { + if (window.gameUI?.uiElements?.opponent?.panel) { const opponentPanel = window.gameUI.uiElements.opponent.panel; - opponentPanel.classList.remove('dissolving'); - const originalTransition = opponentPanel.style.transition; - opponentPanel.style.transition = 'none'; - opponentPanel.style.opacity = '1'; - opponentPanel.style.transform = 'scale(1) translateY(0)'; - requestAnimationFrame(() => { - opponentPanel.style.transition = originalTransition || ''; - }); - console.log('[Client RESTART FIX Improved] Opponent panel styles explicitly reset for new game.'); + if (opponentPanel.classList.contains('dissolving')) { + console.log('[Client.js DEBUG] Removing .dissolving from opponent panel before new game start.'); + opponentPanel.classList.remove('dissolving'); + opponentPanel.style.opacity = '1'; + opponentPanel.style.transform = 'scale(1) translateY(0)'; + } } + // Убедимся, что игровые переменные обновлены (на случай, если игра началась сразу после логина без requestGameState) currentGameId = data.gameId; - myPlayerId = data.yourPlayerId; // Сервер присылает ID слота, который занимает ЭТОТ клиент + myPlayerId = data.yourPlayerId; currentGameState = data.initialGameState; - - // Сервер присылает playerBaseStats и opponentBaseStats ОТНОСИТЕЛЬНО этого клиента - // То есть, data.playerBaseStats - это статы персонажа, которым управляет этот клиент - // data.opponentBaseStats - это статы персонажа-оппонента для этого клиента playerBaseStatsServer = data.playerBaseStats; opponentBaseStatsServer = data.opponentBaseStats; playerAbilitiesServer = data.playerAbilities; opponentAbilitiesServer = data.opponentAbilities; - - myCharacterKey = playerBaseStatsServer?.characterKey; // Ключ персонажа этого клиента - opponentCharacterKey = opponentBaseStatsServer?.characterKey; // Ключ персонажа оппонента этого клиента - - console.log(`[Client gameStarted] My Slot ID (technical): ${myPlayerId}`); - console.log(`[Client gameStarted] My Character: ${myCharacterKey} (Name: ${playerBaseStatsServer?.name})`); - console.log(`[Client gameStarted] Opponent Character: ${opponentCharacterKey} (Name: ${opponentBaseStatsServer?.name})`); - + myCharacterKey = playerBaseStatsServer?.characterKey; + opponentCharacterKey = opponentBaseStatsServer?.characterKey; if (data.clientConfig) { window.GAME_CONFIG = { ...data.clientConfig }; + console.log('[Client.js gameStarted] Received clientConfig from server.'); } else if (!window.GAME_CONFIG) { window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' }; + console.warn('[Client.js gameStarted] No clientConfig received from server. Using fallback.'); } - // Глобальные переменные для ui.js window.gameState = currentGameState; - window.gameData = { // Эти данные используются в ui.js для отображения панелей - playerBaseStats: playerBaseStatsServer, // Статы "моего" персонажа - opponentBaseStats: opponentBaseStatsServer, // Статы "моего оппонента" - playerAbilities: playerAbilitiesServer, // Способности "моего" персонажа - opponentAbilities: opponentAbilitiesServer // Способности "моего оппонента" + window.gameData = { + playerBaseStats: playerBaseStatsServer, + opponentBaseStats: opponentBaseStatsServer, + playerAbilities: playerAbilitiesServer, + opponentAbilities: opponentAbilitiesServer }; - window.myPlayerId = myPlayerId; // Технический ID слота этого клиента + window.myPlayerId = myPlayerId; - showGameScreen(); - initializeAbilityButtons(); + showGameScreen(); // Показываем игровой экран (ставит isInGame = true) + initializeAbilityButtons(); // Инициализируем кнопки способностей - if (window.gameUI && gameUI.uiElements?.log?.list) { - gameUI.uiElements.log.list.innerHTML = ''; + if (window.gameUI?.uiElements?.log?.list) { + window.gameUI.uiElements.log.list.innerHTML = ''; } - if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) { - data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type)); + if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) { + data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type)); } requestAnimationFrame(() => { - if (window.gameUI && typeof gameUI.updateUI === 'function') { - console.log('[Client] Calling gameUI.updateUI() after style reset and rAF in gameStarted.'); - gameUI.updateUI(); + if (window.gameUI && typeof window.gameUI.updateUI === 'function') { + console.log('[Client] Calling gameUI.updateUI() after gameStarted.'); + window.gameUI.updateUI(); } }); hideGameOverModal(); - if (returnToMenuButton) { - returnToMenuButton.disabled = true; - } - setGameStatusMessage(""); + setGameStatusMessage(""); // Скрываем статус сообщение, если видим игровой экран }); + // Обработка gameStateUpdate - без изменений (проверяет isLoggedIn и isInGame) socket.on('gameStateUpdate', (data) => { - if (!isLoggedIn || !currentGameId) return; + if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) { + console.warn('[Client] Ignoring gameStateUpdate: Not logged in or not in game context.'); + return; + } currentGameState = data.gameState; - window.gameState = currentGameState; // ui.js использует это для обновления + window.gameState = currentGameState; - if (window.gameUI && typeof gameUI.updateUI === 'function') { - gameUI.updateUI(); + if (window.gameUI && typeof window.gameUI.updateUI === 'function') { + window.gameUI.updateUI(); } - if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) { - data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type)); + if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) { + data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type)); } }); + // Обработка logUpdate - без изменений (проверяет isLoggedIn и isInGame) socket.on('logUpdate', (data) => { - if (!isLoggedIn || !currentGameId) return; - if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) { - data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type)); + if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) { + console.warn('[Client] Ignoring logUpdate: Not logged in or not in game context.'); + return; + } + if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) { + data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type)); } }); + // Обработка gameOver - без изменений (сбрасывает gameState в конце для UI, но переменные игры не сбрасывает сразу) socket.on('gameOver', (data) => { - if (!isLoggedIn || !currentGameId) return; + if (!isLoggedIn || !currentGameId || !window.GAME_CONFIG) { + console.warn('[Client] Ignoring gameOver: Not logged in or currentGameId is null/stale.'); + // Если игра окончена, но состояние клиента было некорректным, попробуем сбросить его + if (!currentGameId && isLoggedIn) socket.emit('requestGameState'); // Попробуем запросить состояние + else if (!isLoggedIn) showAuthScreen(); + return; + } - console.log(`[Client gameOver] Received. My technical slot ID (myPlayerId): ${myPlayerId}, Winner's slot ID from server (data.winnerId): ${data.winnerId}`); - const playerWon = data.winnerId === myPlayerId; // Определяем, выиграл ли ЭТОТ клиент + console.log(`[Client gameOver] Received for game ${currentGameId}. My technical slot ID (myPlayerId): ${myPlayerId}, Winner's slot ID from server (data.winnerId): ${data.winnerId}`); + const playerWon = data.winnerId === myPlayerId; console.log(`[Client gameOver] Calculated playerWon for this client: ${playerWon}`); currentGameState = data.finalGameState; window.gameState = currentGameState; - // Логи для отладки имен, которые будут использоваться в ui.js + console.log('[Client gameOver] Final GameState:', currentGameState); + if (window.gameData) { - console.log(`[Client gameOver] For ui.js, myName will be: ${window.gameData.playerBaseStats?.name}, opponentName will be: ${window.gameData.opponentBaseStats?.name}`); + console.log(`[Client gameOver] For ui.js, myName: ${window.gameData.playerBaseStats?.name}, opponentName: ${window.gameData.opponentBaseStats?.name}`); } - - if (window.gameUI && typeof gameUI.updateUI === 'function') gameUI.updateUI(); - if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) { - data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type)); + if (window.gameUI && typeof window.gameUI.updateUI === 'function') { + window.gameUI.updateUI(); } - if (window.gameUI && typeof gameUI.showGameOver === 'function') { - // opponentCharacterKeyFromClient передается, чтобы ui.js знал, какой персонаж был оппонентом - // и мог применить, например, анимацию .dissolving к правильному типу оппонента (Балард/Альмагест) + if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) { + data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type)); + } + + if (window.gameUI && typeof window.gameUI.showGameOver === 'function') { const opponentKeyForModal = window.gameData?.opponentBaseStats?.characterKey; - gameUI.showGameOver(playerWon, data.reason, opponentKeyForModal); + window.gameUI.showGameOver(playerWon, data.reason, opponentKeyForModal, data); + } - if (returnToMenuButton) { - returnToMenuButton.disabled = false; - } + if (returnToMenuButton) { + returnToMenuButton.disabled = false; // Включаем кнопку "В меню" в модалке } setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли.")); + + // isInGame остается true, пока не нажмут "В меню" + // disableGameControls() уже вызвано через updateUI из-за isGameOver }); + // Обработка waitingForOpponent - без изменений socket.on('waitingForOpponent', () => { if (!isLoggedIn) return; setGameStatusMessage("Ожидание присоединения оппонента..."); + disableGameControls(); // Отключаем кнопки, пока ждем + // Включаем кнопки настройки игры после попытки создания/присоединения к ожидающей игре + // чтобы игрок мог отменить или попробовать другое + enableSetupButtons(); + // Однако, если игрок создал игру, кнопки "Создать" должны быть отключены, + // а если он искал и создал, то тоже. + // Возможно, лучше отключать кнопки создания/поиска, оставляя только "Присоединиться" по ID или отмену. + // Для простоты пока включаем все, кроме кнопок боя. + // disableSetupButtons(); // Лучше оставить их отключенными до gameStarted или gameNotFound }); + // Обработка opponentDisconnected - без изменений (проверяет isLoggedIn и isInGame) socket.on('opponentDisconnected', (data) => { - if (!isLoggedIn || !currentGameId) return; - const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system'; - if (window.gameUI && typeof gameUI.addToLog === 'function') { - gameUI.addToLog(`Противник (${data.disconnectedCharacterName || 'Игрок'}) отключился.`, systemLogType); + if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) { + console.warn('[Client] Ignoring opponentDisconnected: Not logged in or not in game context.'); + return; } + const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system'; + const disconnectedCharacterName = data.disconnectedCharacterName || 'Противник'; + const disconnectedCharacterKey = data.disconnectedCharacterKey || 'unknown'; + + if (window.gameUI && typeof window.gameUI.addToLog === 'function') { + window.gameUI.addToLog(`🔌 Противник (${disconnectedCharacterName}) отключился.`, systemLogType); + } + if (currentGameState && !currentGameState.isGameOver) { - setGameStatusMessage("Противник отключился. Игра может быть завершена сервером.", true); - // Сервер должен прислать 'gameOver', если игра действительно завершается + setGameStatusMessage(`Противник (${disconnectedCharacterName}) отключился. Ожидание завершения игры сервером...`, true); + disableGameControls(); // Отключаем кнопки немедленно } }); + // Обработка gameError - без изменений socket.on('gameError', (data) => { console.error('[Client] Server error:', data.message); const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system'; - if (isLoggedIn && currentGameId && currentGameState && !currentGameState.isGameOver && window.gameUI && typeof gameUI.addToLog === 'function') { - gameUI.addToLog(`Ошибка: ${data.message}`, systemLogType); + + // Если в игре, добавляем в лог и отключаем кнопки + if (isLoggedIn && isInGame && currentGameId && currentGameState && !currentGameState.isGameOver && window.gameUI && typeof window.gameUI.addToLog === 'function') { + window.gameUI.addToLog(`❌ Ошибка игры: ${data.message}`, systemLogType); + disableGameControls(); // Отключаем кнопки при ошибке + setGameStatusMessage(`Ошибка в игре: ${data.message}.`, true); + // Возможно, тут нужно вернуть игрока в меню после небольшой задержки? + // setTimeout(() => { + // if (isLoggedIn && isInGame) { // Проверяем, что все еще в игре после задержки + // alert("Произошла ошибка. Вы будете возвращены в меню выбора игры."); // Сообщение пользователю + // // Симулируем нажатие кнопки "В меню" + // if (returnToMenuButton && !returnToMenuButton.disabled) { + // returnToMenuButton.click(); + // } else { + // // Если кнопка "В меню" отключена или не найдена, сбрасываем вручную + // resetGameVariables(); isInGame = false; showGameSelectionScreen(loggedInUsername); + // } + // } + // }, 3000); // Задержка перед возвратом + } else { + // Ошибка вне контекста игры + setGameStatusMessage(`❌ Ошибка игры: ${data.message}`, true); + // Сбрасываем состояние, если ошибка пришла не в игре + resetGameVariables(); + isInGame = false; + disableGameControls(); + + if(isLoggedIn && loggedInUsername) { + showGameSelectionScreen(loggedInUsername); // Возвращаемся на экран выбора игры + } else { + showAuthScreen(); // Возвращаемся на экран логина + } + } + // Включаем кнопки форм/настройки игры после обработки ошибки + if (!isLoggedIn) { // Если на экране логина + if(registerForm) registerForm.querySelector('button').disabled = false; + if(loginForm) loginForm.querySelector('button').disabled = false; + } else if (!isInGame) { // Если на экране выбора игры + enableSetupButtons(); } - setGameStatusMessage(`Ошибка: ${data.message}`, true); }); + socket.on('availablePvPGamesList', (games) => { if (!isLoggedIn) return; - updateAvailableGamesList(games); + updateAvailableGamesList(games); // updateAvailableGamesList включает кнопки Join и основные кнопки создания }); socket.on('noPendingGamesFound', (data) => { if (!isLoggedIn) return; + // Это информационное сообщение, когда игрок искал игру и создал новую + // currentGameId и myPlayerId должны быть установлены событием 'gameCreated' setGameStatusMessage(data.message || "Свободных игр не найдено. Создана новая для вас, ожидайте оппонента."); - updateAvailableGamesList([]); - if (data.gameId) { - currentGameId = data.gameId; - myPlayerId = data.yourPlayerId; // Запоминаем наш технический ID слота - if (gameIdInput) gameIdInput.value = currentGameId; - console.log(`[Client] New game ${currentGameId} created after no pending games found. My slot: ${myPlayerId}`); - } + updateAvailableGamesList([]); // Очищаем список, т.к. мы теперь в ожидающей игре + isInGame = false; // Пока ждем, не в активной игре + disableGameControls(); // Кнопки боя отключены + // Кнопки настройки игры должны оставаться отключенными, пока ждем игрока + disableSetupButtons(); }); - // --- Начальное состояние UI --- + + // --- Изначальное состояние UI при загрузке страницы --- + // При загрузке страницы всегда начинаем с Auth. showAuthScreen(); }); \ No newline at end of file diff --git a/public/js/ui.js b/public/js/ui.js index 7896d02..bf2149d 100644 --- a/public/js/ui.js +++ b/public/js/ui.js @@ -56,7 +56,7 @@ 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); @@ -68,12 +68,13 @@ 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)) { + if (elements && elements.panel && elements.panel.style.display !== 'none') { // console.warn(`updateFighterPanelUI: Нет данных для видимой панели ${panelRole}.`); - // elements.panel.style.opacity = '0.3'; // Пример: сделать полупрозрачной + // elements.panel.style.opacity = '0.5'; // Пример: сделать полупрозрачной, если нет данных } return; } @@ -86,23 +87,34 @@ // Обновление имени и иконки персонажа if (elements.name) { let iconClass = 'fa-question'; // Иконка по умолчанию - let accentColor = 'var(--text-muted)'; // Цвет по умолчанию + // let accentColor = 'var(--text-muted)'; // Цвет по умолчанию - теперь берется из CSS через классы иконок 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}" для иконки/цвета имени.`); */ } + // Определяем класс иконки в зависимости от персонажа + if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-player'; } // icon-player имеет цвет через CSS + else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; } // icon-almagest имеет цвет через CSS + else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-opponent'; } // icon-opponent имеет цвет через CSS + else { /* console.warn(`updateFighterPanelUI: Неизвестный characterKey "${characterKey}" для иконки имени.`); */ } + // Обновляем innerHTML имени, включая иконку и текст. Добавляем "(Вы)" для управляемого персонажа. let nameHtml = ` ${fighterBaseStats.name || 'Неизвестно'}`; if (isControlledByThisClient) nameHtml += " (Вы)"; elements.name.innerHTML = nameHtml; - elements.name.style.color = accentColor; + // Цвет имени теперь задается CSS через классы icon-player/opponent/almagest, примененные к самой иконке + // elements.name.style.color = accentColor; // Эту строку можно удалить, если цвет задан через CSS } // Обновление аватара - if (elements.avatar && fighterBaseStats.avatarPath) elements.avatar.src = fighterBaseStats.avatarPath; - else if (elements.avatar) elements.avatar.src = 'images/default_avatar.png'; // Запасной аватар + 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); // Избегаем деления на ноль @@ -112,8 +124,10 @@ 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}`; + elements.resourceText.textContent = `${currentRes} / ${fighterBaseStats.maxResource}`; // <-- ИСПРАВЛЕНО + // Обновление типа ресурса и иконки (mana/stamina/dark-energy) const resourceBarContainerToUpdate = (panelRole === 'player') ? uiElements.playerResourceBarContainer : uiElements.opponentResourceBarContainer; @@ -121,30 +135,39 @@ if (resourceBarContainerToUpdate && resourceIconElementToUpdate) { resourceBarContainerToUpdate.classList.remove('mana', 'stamina', 'dark-energy'); // Сначала удаляем все классы ресурсов - let resourceClass = 'mana'; let iconClass = 'fa-flask'; // Значения по умолчанию (для Елены) + 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'; } + else if (fighterBaseStats.resourceName === 'Темная Энергия') { resourceClass = 'dark-energy'; iconClass = 'fa-skull'; } // Или другую иконку для темной энергии resourceBarContainerToUpdate.classList.add(resourceClass); - resourceIconElementToUpdate.className = `fas ${iconClass}`; + 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); + 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)'; } // Фоллбэк + 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)'; } // Цвет рамки через CSS переменную + else if (fighterBaseStats.characterKey === 'almagest') { elements.panel.classList.add('panel-almagest'); borderColorVar = 'var(--accent-almagest)'; } // Цвет рамки через CSS переменную + else if (fighterBaseStats.characterKey === 'balard') { elements.panel.classList.add('panel-balard'); borderColorVar = 'var(--accent-opponent)'; } // Цвет рамки через CSS переменную + + // Обновляем тень (свечение). Цвет свечения тоже может быть переменной. + let glowColorVar = 'rgba(0, 0, 0, 0.4)'; // Базовая тень + if (fighterBaseStats.characterKey === 'elena') glowColorVar = 'var(--panel-glow-player)'; + else if (fighterBaseStats.characterKey === 'almagest' || fighterBaseStats.characterKey === 'balard') glowColorVar = 'var(--panel-glow-opponent)'; // Используем одну тень для всех оппонентов (Балард/Альмагест) + + // Устанавливаем рамку и тень 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)`; + // Используем переменную для свечения. Базовая тень inset оставлена как есть. + elements.panel.style.boxShadow = `0 0 15px ${glowColorVar}, inset 0 0 10px rgba(0, 0, 0, 0.3)`; } } @@ -152,47 +175,115 @@ const config = window.GAME_CONFIG || {}; if (!effectsArray || effectsArray.length === 0) return 'Нет'; - return effectsArray.map(eff => { - let effectClasses = config.CSS_CLASS_EFFECT || 'effect'; + // Сортируем эффекты: сначала положительные (buff, block, heal), затем отрицательные (debuff, disable) + // иконка для стана/безмолвия, иконка для ослабления, иконка для усиления + const sortedEffects = [...effectsArray].sort((a, b) => { + const typeOrder = { + [config.ACTION_TYPE_BUFF]: 1, + grantsBlock: 2, + [config.ACTION_TYPE_HEAL]: 3, // HoT эффекты + [config.ACTION_TYPE_DEBUFF]: 4, // DoT, ресурсные дебаффы + [config.ACTION_TYPE_DISABLE]: 5 // Silence, Stun + }; + // Определяем порядок для эффекта A + let orderA = typeOrder[a.type]; + if (a.grantsBlock) orderA = typeOrder.grantsBlock; + if (a.isFullSilence || a.id.startsWith('playerSilencedOn_')) orderA = typeOrder[config.ACTION_TYPE_DISABLE]; + + // Определяем порядок для эффекта B + let orderB = typeOrder[b.type]; + if (b.grantsBlock) orderB = typeOrder.grantsBlock; + if (b.isFullSilence || b.id.startsWith('playerSilencedOn_')) orderB = typeOrder[config.ACTION_TYPE_DISABLE]; + + return (orderA || 99) - (orderB || 99); // Сортируем по порядку, неизвестные типы в конец + }); + + + return sortedEffects.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'; + // Добавляем специфичные классы для стилизации по типу эффекта + 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) { // Явные дебаффы (например, сжигание ресурса, DoT) + effectClasses += ' effect-debuff'; // Класс для ослаблений + } else if (eff.type === config.ACTION_TYPE_BUFF || eff.type === config.ACTION_TYPE_HEAL) { // Явные баффы или эффекты HoT + effectClasses += ' effect-buff'; // Класс для усилений + } else { + // console.warn(`generateEffectsHTML: Неизвестный тип эффекта для стилизации: ${eff.type} (ID: ${eff.id})`); + effectClasses += ' effect-info'; // Класс по умолчанию или информационный } + return `${displayText}`; - }).join(' '); + }).join(' '); // Объединяем все HTML-строки эффектов в одну } function updateEffectsUI(currentGameState) { if (!currentGameState || !window.GAME_CONFIG) { return; } const mySlotId = window.myPlayerId; // Технический ID слота этого клиента + const config = window.GAME_CONFIG; if (!mySlotId) { return; } - const opponentSlotId = mySlotId === window.GAME_CONFIG.PLAYER_ID ? window.GAME_CONFIG.OPPONENT_ID : window.GAME_CONFIG.PLAYER_ID; + const opponentSlotId = mySlotId === config.PLAYER_ID ? config.OPPONENT_ID : 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) )); + // Разделяем эффекты на баффы и дебаффы для отображения + // Критерии разделения могут быть специфичны для игры: + // Баффы: тип BUFF, эффекты дающие блок (grantsBlock), эффекты лечения (HoT) + // Дебаффы: тип DEBUFF, тип DISABLE (кроме grantsBlock), эффекты урона (DoT) + // Включаем isFullSilence и playerSilencedOn_X в "дебаффы" для отображения (можно сделать отдельную категорию) + const myBuffs = myState.activeEffects.filter(e => + e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || (e.type === config.ACTION_TYPE_HEAL) // HoT как бафф + ); + const myDebuffs = myState.activeEffects.filter(e => + e.type === config.ACTION_TYPE_DEBUFF || + e.type === config.ACTION_TYPE_DISABLE // Disable как дебафф + // || (e.type === config.ACTION_TYPE_DAMAGE) // DoT как дебафф, если есть + ); + // Специально добавляем полные безмолвия и заглушения абилок в дебаффы, даже если их тип не DEBUFF/DISABLE + myDebuffs.push(...myState.activeEffects.filter(e => e.isFullSilence || e.id.startsWith('playerSilencedOn_') && !myDebuffs.some(d => d.id === e.id))); // Избегаем дублирования + + 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 = 'Нет'; } + 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) )); + const opponentBuffs = opponentState.activeEffects.filter(e => + e.type === config.ACTION_TYPE_BUFF || e.grantsBlock || (e.type === config.ACTION_TYPE_HEAL) // HoT как бафф + ); + const opponentDebuffs = opponentState.activeEffects.filter(e => + e.type === config.ACTION_TYPE_DEBUFF || + e.type === config.ACTION_TYPE_DISABLE // Disable как дебафф + // || (e.type === config.ACTION_TYPE_DAMAGE) // DoT как дебафф, если есть + ); + // Специально добавляем полные безмолвия и заглушения абилок в дебаффы оппонента, даже если их тип не DEBUFF/DISABLE + opponentDebuffs.push(...opponentState.activeEffects.filter(e => e.isFullSilence || e.id.startsWith('playerSilencedOn_') && !opponentDebuffs.some(d => d.id === e.id))); // Избегаем дублирования + + 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 updateUI() { const currentGameState = window.gameState; // Глобальное состояние игры - const gameDataGlobal = window.gameData; // Глобальные данные (статы, абилки) для этого клиента + const gameDataGlobal = window.gameData; // Глобальные данные ( статы, абилки ) для этого клиента const configGlobal = window.GAME_CONFIG; // Глобальный конфиг const myActualPlayerId = window.myPlayerId; // Технический ID слота этого клиента @@ -207,77 +298,88 @@ // Определяем, чей сейчас ход по ID слота const actorSlotWhoseTurnItIs = currentGameState.isPlayerTurn ? configGlobal.PLAYER_ID : configGlobal.OPPONENT_ID; - // Определяем 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); + // Обновление панели "моего" персонажа ( которым управляет этот клиент) + const myStateInGameState = currentGameState[myActualPlayerId]; + const myBaseStatsForUI = gameDataGlobal.playerBaseStats; // playerBaseStats в gameData - это всегда статы персонажа этого клиента + if (myStateInGameState && myBaseStatsForUI) { + if (uiElements.player.panel) uiElements.player.panel.style.opacity = '1'; // Делаем панель полностью видимой, если есть данные + updateFighterPanelUI('player', myStateInGameState, myBaseStatsForUI, true); } else { - if (uiElements.player.panel) uiElements.player.panel.style.opacity = '0.5'; + 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 opponentStateInGameState = currentGameState[opponentActualSlotId]; + const opponentBaseStatsForUI = gameDataGlobal.opponentBaseStats; // opponentBaseStats в gameData - это всегда статы оппонента этого клиента + if (opponentStateInGameState && opponentBaseStatsForUI) { + // Если игра не окончена, а панель оппонента "тает" или не полностью видна, восстанавливаем это + if (uiElements.opponent.panel && (uiElements.opponent.panel.style.opacity !== '1' || uiElements.opponent.panel.classList.contains('dissolving')) && currentGameState.isGameOver === false ) { + console.log("[UI UPDATE DEBUG] Opponent panel not fully visible/dissolving but game not over. Restoring opacity/transform."); 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 || ''; }); + // Force reflow before applying instant style change + panel.offsetHeight; // Trigger reflow + panel.style.opacity = '1'; + panel.style.transform = 'scale(1) translateY(0)'; + } else if (uiElements.opponent.panel) { + uiElements.opponent.panel.style.opacity = '1'; // Убеждаемся, что видна, если есть данные } - updateFighterPanelUI('opponent', currentGameState[opponentActualSlotId], gameDataGlobal.opponentBaseStats, false); + updateFighterPanelUI('opponent', opponentStateInGameState, opponentBaseStatsForUI, false); } else { - if (uiElements.opponent.panel) uiElements.opponent.panel.style.opacity = '0.5'; + // Нет данных оппонента ( например, PvP игра ожидает игрока). Затемняем панель. + if (uiElements.opponent.panel) { + uiElements.opponent.panel.style.opacity = '0.5'; + // Очищаем панель, если данных нет + if(uiElements.opponent.name) uiElements.opponent.name.innerHTML = ' Ожидание игрока...'; + if(uiElements.opponent.hpText) uiElements.opponent.hpText.textContent = 'N/A'; + if(uiElements.opponent.resourceText) uiElements.opponent.resourceText.textContent = 'N/A'; + if(uiElements.opponent.status) uiElements.opponent.status.textContent = 'Не готов'; + if(uiElements.opponent.buffsList) uiElements.opponent.buffsList.innerHTML = 'Нет'; + if(uiElements.opponent.debuffsList) uiElements.opponent.debuffsList.innerHTML = 'Нет'; + if(uiElements.opponent.avatar) uiElements.opponent.avatar.src = 'images/default_avatar.png'; + if(uiElements.opponentResourceTypeIcon) uiElements.opponentResourceTypeIcon.className = 'fas fa-question'; + if(uiElements.opponentResourceBarContainer) uiElements.opponentResourceBarContainer.classList.remove('mana', 'stamina', 'dark-energy'); + } } + updateEffectsUI(currentGameState); - // Обновление заголовка игры (Имя1 vs Имя2) + // Обновление заголовка игры ( Имя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 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'; + 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 = `${myName} ${opponentName}`; } - // Обновление индикатора хода - 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; // Может ли ЭТОТ клиент ходить + // Этот клиент может действовать только если его технический слот ('player' или 'opponent') + // соответствует слоту, чей сейчас ход ( actorSlotWhoseTurnItIs). + 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; @@ -285,7 +387,9 @@ 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); + // Проверяем, есть ли активный бафф атаки И он не только что наложен в этом ходу + const isAttackBuffReady = myStateForAttackBuff.activeEffects.some(eff => eff.id === attackBuffId && !eff.justCast && eff.turnsLeft > 0); + // Подсветка активна, если бафф готов И это ход этого клиента И игра активна 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'); @@ -295,39 +399,74 @@ // Кнопки способностей const actingPlayerState = currentGameState[myActualPlayerId]; // Состояние моего персонажа - const actingPlayerAbilities = gameDataGlobal.playerAbilities; // Способности моего персонажа - const actingPlayerResourceName = gameDataGlobal.playerBaseStats?.resourceName; + 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) || !actingPlayerState || !actingPlayerAbilities || !actingPlayerResourceName || !isGameActive) { + // Если нет необходимых данных или игра неактивна, дизейблим все кнопки способностей 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 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); + if (!ability) { + 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; } - button.disabled = !(canThisClientAct && isGameActive) || !hasEnoughResource || isBuffAlreadyActive || isSilenced || isOnCooldown || isDisabledByDebuffOnTarget; + // Проверяем условия доступности способности + 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 = isSpecificallySilenced + ? (specificSilenceEffect?.turnsLeft || 0) + : (isGenerallySilenced ? (actingPlayerState.activeEffects.find(eff => eff.isFullSilence)?.turnsLeft || 0) : 0); + + + // Нельзя кастовать дебафф на цель, если он уже на ней (для определенных дебаффов) + // Нужна проверка состояния ОППОНЕНТА этого клиента (т.е. цели) + const opponentStateForDebuffCheck = currentGameState[opponentActualSlotId]; + let isDebuffAlreadyOnTarget = false; + const isTargetedDebuffAbility = ability.id === configGlobal.ABILITY_ID_SEAL_OF_WEAKNESS || ability.id === configGlobal.ABILITY_ID_ALMAGEST_DEBUFF; + if (isTargetedDebuffAbility && opponentStateForDebuffCheck && opponentStateForDebuffCheck.activeEffects) { + const effectIdForDebuff = 'effect_' + ability.id; // Ищем эффект с префиксом effect_ на цели + isDebuffAlreadyOnTarget = opponentStateForDebuffCheck.activeEffects.some(e => e.id === effectIdForDebuff); + } + + + // Кнопка активна, если: + // - Это ход этого клиента + // - Игра активна + // - Достаточно ресурса + // - Бафф не активен (если это бафф) + // - Не на кулдауне + // - Не под безмолвием (полным или специфическим) + // - Дебафф не активен на цели (если это такой дебафф) + button.disabled = !(canThisClientAct && isGameActive) || + !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'); @@ -336,39 +475,71 @@ 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'; } + if (cooldownDisplay) { + const icon = isGenerallySilenced ? '🔕' : '🔇'; // Иконка для полного/частичного безмолвия + cooldownDisplay.textContent = `${icon} ${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); + if (cooldownDisplay) cooldownDisplay.style.display = 'none'; // Скрываем, если нет ни КД, ни безмолвия + // Добавляем классы, если действие возможно, но есть ограничения (недостаточно ресурса, бафф активен, дебафф на цели) + // Эти классы используются для визуальной обратной связи, когда кнопка *не* задизейблена по КД или безмолвию. + // Если кнопка disabled из-за !hasEnoughResource, классы not-enough-resource и buff-is-active все равно могут быть добавлены. + button.classList.toggle(configGlobal.CSS_CLASS_NOT_ENOUGH_RESOURCE||'not-enough-resource', !hasEnoughResource && !isBuffAlreadyActive && !isDebuffAlreadyOnTarget); + button.classList.toggle(configGlobal.CSS_CLASS_BUFF_IS_ACTIVE||'buff-is-active', isBuffAlreadyActive && !isDebuffAlreadyOnTarget); + // Если дебафф уже на цели, но кнопка не задизейблена по другим причинам, можно добавить отдельный класс + // button.classList.toggle('debuff-on-target', isDebuffAlreadyOnTarget && !button.disabled); } - // Обновление title (всплывающей подсказки) + // Обновление 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} х.)`; + let descriptionTextFull = ability.description; // Используем описание, пришедшее с сервера - 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} х.)` : ''}`; + if (descriptionTextFull) titleText += ` - ${descriptionTextFull}`; + + let abilityBaseCooldown = ability.cooldown; // Исходный КД из данных + // Учитываем внутренние КД Баларда, если это способность Баларда (хотя игроку AI способности не отображаются, но для полноты) + // if (actingPlayerState.characterKey === 'balard') { + // if (ability.id === configGlobal.ABILITY_ID_BALARD_SILENCE) abilityBaseCooldown = configGlobal.BALARD_SILENCE_INTERNAL_COOLDOWN; + // else if (ability.id === configGlobal.ABILITY_ID_BALARD_MANA_DRAIN && typeof ability.internalCooldownValue === 'number') abilityBaseCooldown = ability.internalCooldownValue; + // } + + if (typeof abilityBaseCooldown === 'number' && abilityBaseCooldown > 0) { + titleText += ` (КД: ${abilityBaseCooldown} х.)`; } + + // Добавляем информацию о текущем состоянии (КД, безмолвие, активный бафф/debuff) в тултип, если применимо + if (isOnCooldown) { + titleText += ` | На перезарядке! Осталось: ${actingPlayerState.abilityCooldowns[ability.id]} х.`; + } + if (isSilenced) { + titleText += ` | Под безмолвием! Осталось: ${silenceTurnsLeft} х.`; + } + if (isBuffAlreadyActive) { + const activeEffect = actingPlayerState.activeEffects?.find(eff => eff.id === abilityId); + titleText += ` | Эффект уже активен${activeEffect ? ` (${activeEffect.turnsLeft} х.)` : ''}.`; + } + if (isDebuffAlreadyOnTarget && opponentStateForDebuffCheck) { + const activeDebuff = opponentStateForDebuffCheck.activeEffects?.find(e => e.id === 'effect_' + abilityId); + titleText += ` | Эффект уже наложен на ${opponentBaseStatsForUI?.name || 'противника'}${activeDebuff ? ` (${activeDebuff.turnsLeft} х.)` : ''}.`; + } + if (!hasEnoughResource) { + titleText += ` | Недостаточно ${actingPlayerResourceName} (${actingPlayerState.currentResource}/${ability.cost})`; + } + + button.setAttribute('title', titleText); }); } - function showGameOver(playerWon, reason = "", opponentCharacterKeyFromClient = null) { + /** + * Показывает модальное окно конца игры. + * @param {boolean} playerWon - Флаг, выиграл ли игрок, управляющий этим клиентом. + * @param {string} [reason=""] - Причина завершения игры. + * @param {string|null} opponentCharacterKeyFromClient - Ключ персонажа оппонента с т.з. клиента. + * @param {object} [data=null] - Полный объект данных из события gameOver (включает disconnectedCharacterName и т.д.) + */ + function showGameOver(playerWon, reason = "", opponentCharacterKeyFromClient = null, data = null) { // ИСПРАВЛЕНО: Добавлен аргумент data const config = window.GAME_CONFIG || {}; // Используем gameData, сохраненное в client.js, так как оно отражает перспективу этого клиента const clientSpecificGameData = window.gameData; @@ -380,6 +551,7 @@ 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}`); + console.log(`[UI.JS DEBUG] Full game over data received:`, data); // Добавьте этот лог if (!gameOverScreenElement) { @@ -396,7 +568,16 @@ let winText = `Победа! ${myNameForResult} празднует!`; let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`; if (reason === 'opponent_disconnected') { - winText = `${opponentNameForResult} покинул(а) игру. Победа присуждается вам!`; + // Определяем, кто отключился, по данным из события gameOver + let disconnectedName = "Противник"; + if (data && data.disconnectedCharacterName) { + disconnectedName = data.disconnectedCharacterName; + } else { + // Фоллбэк на имя оппонента с точки зрения клиента + disconnectedName = opponentNameForResult; + } + + winText = `${disconnectedName} покинул(а) игру. Победа присуждается вам!`; // Если оппонент отключился, а мы проиграли (технически такое возможно, если сервер так решит) // То текст поражения можно оставить стандартным или специфичным. // Пока оставим стандартный, если playerWon = false и reason = opponent_disconnected. @@ -407,54 +588,78 @@ const opponentPanelElement = uiElements.opponent.panel; if (opponentPanelElement) { + // Сначала убеждаемся, что анимация растворения снята, если она была активна от предыдущей попытки + // console.log(`[UI.JS DEBUG] Opponent panel classList before potential dissolve: ${opponentPanelElement.className}`); opponentPanelElement.classList.remove('dissolving'); - console.log(`[UI.JS DEBUG] Opponent panel classList after initial remove .dissolving: ${opponentPanelElement.className}`); + opponentPanelElement.offsetHeight; // Trigger reflow to reset state instantly - // Используем opponentCharacterKeyFromClient, так как это ключ реального персонажа оппонента + // Используем 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}`); + console.log(`[UI.JS DEBUG] ADDING .dissolving to opponent panel. Conditions met.`); opponentPanelElement.classList.add('dissolving'); + // Убеждаемся, что панель станет полностью прозрачной и сместится после анимации + opponentPanelElement.style.opacity = '0'; + // opponentPanelElement.style.transform = 'scale(0.9) translateY(20px)'; // Трансформация уже в CSS анимации } 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'; + console.log(`[UI.JS DEBUG] NOT adding .dissolving (opponent key mismatch for dissolve effect: ${keyForDissolveEffect} or reason: ${reason}).`); + // Если анимация не применяется, убеждаемся, что панель видна opponentPanelElement.style.opacity = '1'; opponentPanelElement.style.transform = 'scale(1) translateY(0)'; - requestAnimationFrame(() => { - opponentPanelElement.style.transition = originalTransition || ''; - }); } + } else { + console.log(`[UI.JS DEBUG] NOT adding .dissolving. Conditions NOT met: isGameOver=${currentActualGameState?.isGameOver}, playerWon=${playerWon}, reason=${reason}.`); + // Если игра не окончена или игрок проиграл/оппонент отключился, убеждаемся, что панель видна + opponentPanelElement.style.opacity = '1'; + opponentPanelElement.style.transform = 'scale(1) translateY(0)'; } - console.log(`[UI.JS DEBUG] Opponent panel classList FINAL in showGameOver: ${opponentPanelElement.className}`); + // console.log(`[UI.JS DEBUG] Opponent panel classList FINAL in showGameOver (before timeout): ${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'); + // Показываем модальное окно конца игры с небольшой задержкой + // ИСПРАВЛЕНО: Передаем аргументы в колбэк, чтобы не полагаться на глобальный gameState + setTimeout((finalState, won, gameOverReason, opponentKeyForModalParam, eventData) => { // Названия аргументов, чтобы избежать путаницы + console.log("[UI.JS DEBUG] Timeout callback fired."); + console.log("[UI.JS DEBUG] State in timeout:", finalState); + console.log("[UI.JS DEBUG] isGameOver in state:", finalState?.isGameOver); + + // Перепроверяем состояние перед показом на случай быстрых обновлений + if (finalState && finalState.isGameOver === true) { // Используем переданный state + console.log(`[UI.JS DEBUG] Condition (finalState && finalState.isGameOver === true) IS TRUE. Attempting to show modal.`); + // Убеждаемся, что modal не имеет display: none перед запуском transition opacity + // display: none полностью убирает элемент из потока и не позволяет анимировать opacity + // Переводим display в flex, если он был hidden (display: none !important в CSS) + if (gameOverScreenElement.classList.contains(config.CSS_CLASS_HIDDEN || 'hidden')) { + gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden'); + } + // Убеждаемся, что opacity 0 для начала анимации + gameOverScreenElement.style.opacity = '0'; + // Убеждаемся, что display корректен + gameOverScreenElement.style.display = 'flex'; // Или какой там display в CSS для .modal + 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); + // Применяем opacity 1 после display flex для анимации + gameOverScreenElement.style.opacity = '1'; + // Запускаем анимацию контента модального окна (scale/translate) + if (uiElements.gameOver.modalContent) { + uiElements.gameOver.modalContent.style.transform = 'scale(1) translateY(0)'; + uiElements.gameOver.modalContent.style.opacity = '1'; + } }); } else { - console.log(`[UI.JS DEBUG] NOT showing gameOverScreen modal (isGameOver is now false or gameState missing).`); + console.log(`[UI.JS DEBUG] Condition (finalState && finalState.isGameOver === true) IS FALSE. Modal will NOT be shown.`); + // Убеждаемся, что модалка скрыта, если условия больше не выполняются + gameOverScreenElement.classList.add(config.CSS_CLASS_HIDDEN || 'hidden'); + gameOverScreenElement.style.opacity = '0'; // Убеждаемся, что opacity сброшен } - }, config.DELAY_BEFORE_VICTORY_MODAL || 1500); + }, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState, playerWon, reason, opponentCharacterKeyFromClient, data); // ИСПРАВЛЕНО: Передаем аргументы } + + // Экспортируем функции UI для использования в client.js window.gameUI = { uiElements, addToLog, updateUI, showGameOver }; })(); \ No newline at end of file