Ошибка загрузки способностей.
'; - console.error('[Client.js] initializeAbilityButtons failed: abilitiesGrid, gameUI, or GAME_CONFIG not found.'); return; } abilitiesGrid.innerHTML = ''; @@ -363,78 +295,104 @@ document.addEventListener('DOMContentLoaded', () => { const li = document.createElement('li'); li.textContent = `ID: ${game.id.substring(0, 8)}... - ${game.status || 'Ожидает игрока'}`; const joinBtn = document.createElement('button'); - joinBtn.textContent = 'Присоединиться'; joinBtn.dataset.gameId = game.id; + joinBtn.textContent = 'Присоединиться'; + joinBtn.dataset.gameId = game.id; + + // === ИЗМЕНЕНИЕ: Деактивация кнопки "Присоединиться" для своих игр === + if (isLoggedIn && myUserId && game.ownerIdentifier === myUserId) { + joinBtn.disabled = true; + joinBtn.title = "Вы не можете присоединиться к своей же ожидающей игре."; + } else { + joinBtn.disabled = false; + } + // === КОНЕЦ ИЗМЕНЕНИЯ === + joinBtn.addEventListener('click', (e) => { - if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true); return; } + if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; } + if (e.target.disabled) return; // Не обрабатывать клик по отключенной кнопке disableSetupButtons(); socket.emit('joinGame', { gameId: e.target.dataset.gameId }); }); - li.appendChild(joinBtn); ul.appendChild(li); + li.appendChild(joinBtn); + ul.appendChild(li); } }); availableGamesDiv.appendChild(ul); - availableGamesDiv.querySelectorAll('button').forEach(btn => btn.disabled = false); - } else { availableGamesDiv.innerHTML += 'Нет доступных игр. Создайте свою!
'; } - enableSetupButtons(); + } else { + availableGamesDiv.innerHTML += 'Нет доступных игр. Создайте свою!
'; + } + enableSetupButtons(); // Включаем основные кнопки создания/поиска } + // --- Обработчики событий Socket.IO --- socket.on('connect', () => { - console.log('[Client] Socket connected to server! Socket ID:', socket.id); - if (isLoggedIn) { - console.log(`[Client] Reconnected as ${loggedInUsername}. Requesting state.`); - socket.emit('requestGameState'); - } else { showAuthScreen(); } + console.log('[Client] Socket connected:', socket.id); + if (isLoggedIn && myUserId) { // Проверяем и isLoggedIn и myUserId + socket.emit('requestGameState'); // Запрашиваем состояние, если были залогинены + } else { + showAuthScreen(); // Иначе показываем экран логина + } }); 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; + if(registerForm) registerForm.querySelector('button').disabled = false; + if(loginForm) loginForm.querySelector('button').disabled = false; }); socket.on('loginResponse', (data) => { setAuthMessage(data.message, !data.success); if (data.success) { - isLoggedIn = true; loggedInUsername = data.username; setAuthMessage(""); + isLoggedIn = true; + loggedInUsername = data.username; + myUserId = data.userId; // === ИЗМЕНЕНИЕ: Сохраняем ID пользователя === + setAuthMessage(""); showGameSelectionScreen(data.username); } else { - isLoggedIn = false; loggedInUsername = ''; - if (registerForm) registerForm.querySelector('button').disabled = false; - if (loginForm) loginForm.querySelector('button').disabled = false; + isLoggedIn = false; loggedInUsername = ''; myUserId = null; + if(registerForm) registerForm.querySelector('button').disabled = false; + if(loginForm) loginForm.querySelector('button').disabled = false; } }); socket.on('gameNotFound', (data) => { - console.log('[Client] Game not found response:', data?.message); + console.log('[Client] Game not found/ended:', data?.message); resetGameVariables(); isInGame = false; disableGameControls(); hideGameOverModal(); - if (turnTimerContainer) turnTimerContainer.style.display = 'none'; // Скрываем таймер + if (turnTimerContainer) turnTimerContainer.style.display = 'none'; + if (turnTimerSpan) turnTimerSpan.textContent = '--'; if (isLoggedIn) { showGameSelectionScreen(loggedInUsername); - setGameStatusMessage("Выберите режим игры или присоединитесь к существующей."); - enableSetupButtons(); + setGameStatusMessage(data?.message || "Активная игровая сессия не найдена."); } else { showAuthScreen(); - setAuthMessage(data?.message || "Пожалуйста, войдите, чтобы начать новую игру.", false); + setAuthMessage(data?.message || "Пожалуйста, войдите."); } }); socket.on('disconnect', (reason) => { - console.log('[Client] Disconnected from server:', reason); - setGameStatusMessage(`Отключено от сервера: ${reason}. Пожалуйста, обновите страницу.`, true); + console.log('[Client] Disconnected:', reason); + setGameStatusMessage(`Отключено: ${reason}. Обновите страницу.`, true); disableGameControls(); - // === ИЗМЕНЕНИЕ: При дисконнекте останавливаем таймер (если он виден) === - if (turnTimerSpan) turnTimerSpan.textContent = 'Отключено'; - // Не скрываем контейнер, чтобы было видно сообщение "Отключено" - // === КОНЕЦ ИЗМЕНЕНИЯ === + if (turnTimerSpan) turnTimerSpan.textContent = 'Откл.'; + // Не сбрасываем isLoggedIn, чтобы при переподключении можно было восстановить сессию }); - socket.on('gameStarted', (data) => { - if (!isLoggedIn) { console.warn('[Client] Ignoring gameStarted: Not logged in.'); return; } - console.log('[Client] Event "gameStarted" received:', data); + socket.on('gameCreated', (data) => { // Сервер присылает это после успешного createGame + console.log('[Client] Game created by this client:', data); + currentGameId = data.gameId; + myPlayerId = data.yourPlayerId; // Сервер должен прислать роль создателя + // Остальные данные (gameState, baseStats) придут с gameStarted или gameState (если это PvP ожидание) + // Если это PvP и игра ожидает, сервер может прислать waitingForOpponent + }); + + socket.on('gameStarted', (data) => { + if (!isLoggedIn) return; + console.log('[Client] Game started:', data); + // ... (остальной код gameStarted без изменений, как был) if (window.gameUI?.uiElements?.opponent?.panel) { const opponentPanel = window.gameUI.uiElements.opponent.panel; if (opponentPanel.classList.contains('dissolving')) { @@ -450,7 +408,6 @@ document.addEventListener('DOMContentLoaded', () => { if (data.clientConfig) window.GAME_CONFIG = { ...data.clientConfig }; 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.'); } window.gameState = currentGameState; window.gameData = { playerBaseStats: playerBaseStatsServer, opponentBaseStats: opponentBaseStatsServer, playerAbilities: playerAbilitiesServer, opponentAbilities: opponentAbilitiesServer }; @@ -463,108 +420,131 @@ document.addEventListener('DOMContentLoaded', () => { } requestAnimationFrame(() => { if (window.gameUI && typeof window.gameUI.updateUI === 'function') { - console.log('[Client] Calling gameUI.updateUI() after gameStarted.'); window.gameUI.updateUI(); } }); hideGameOverModal(); setGameStatusMessage(""); }); - socket.on('gameStateUpdate', (data) => { - if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) { - console.warn('[Client] Ignoring gameStateUpdate: Not logged in or not in game context.'); - return; + // Используется для восстановления состояния уже идущей игры + socket.on('gameState', (data) => { + if (!isLoggedIn) return; + console.log('[Client] Received full gameState (e.g. on reconnect):', data); + // Это событие теперь может дублировать 'gameStarted' для переподключения. + // Убедимся, что логика похожа на gameStarted. + currentGameId = data.gameId; + myPlayerId = data.yourPlayerId; + currentGameState = data.gameState; // Используем gameState вместо initialGameState + playerBaseStatsServer = data.playerBaseStats; + opponentBaseStatsServer = data.opponentBaseStats; + playerAbilitiesServer = data.playerAbilities; + opponentAbilitiesServer = data.opponentAbilities; + myCharacterKey = playerBaseStatsServer?.characterKey; + opponentCharacterKey = opponentBaseStatsServer?.characterKey; + + if (data.clientConfig) window.GAME_CONFIG = { ...data.clientConfig }; + else if (!window.GAME_CONFIG) { + window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' }; + } + window.gameState = currentGameState; + window.gameData = { playerBaseStats: playerBaseStatsServer, opponentBaseStats: opponentBaseStatsServer, playerAbilities: playerAbilitiesServer, opponentAbilities: opponentAbilitiesServer }; + window.myPlayerId = myPlayerId; + + if (!isInGame) showGameScreen(); // Показываем экран игры, если еще не там + initializeAbilityButtons(); // Переинициализируем кнопки + + // Лог при 'gameState' может быть уже накопленным, добавляем его + if (window.gameUI?.uiElements?.log?.list && data.log) { // Очищаем лог перед добавлением нового при полном обновлении + window.gameUI.uiElements.log.list.innerHTML = ''; } - currentGameState = data.gameState; window.gameState = currentGameState; - if (window.gameUI && typeof window.gameUI.updateUI === 'function') window.gameUI.updateUI(); 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 window.gameUI.updateUI === 'function') { + window.gameUI.updateUI(); + } + }); + hideGameOverModal(); + // Таймер будет обновлен следующим событием 'turnTimerUpdate' + }); + + + socket.on('gameStateUpdate', (data) => { + if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) return; + currentGameState = data.gameState; window.gameState = currentGameState; + if (window.gameUI?.updateUI) window.gameUI.updateUI(); + if (window.gameUI?.addToLog && data.log) { + data.log.forEach(log => window.gameUI.addToLog(log.message, log.type)); + } }); socket.on('logUpdate', (data) => { - 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)); + if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) return; + if (window.gameUI?.addToLog && data.log) { + data.log.forEach(log => window.gameUI.addToLog(log.message, log.type)); } }); socket.on('gameOver', (data) => { + // ... (код без изменений, как был) 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 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; - console.log('[Client gameOver] Final GameState:', currentGameState); - if (window.gameData) console.log(`[Client gameOver] For ui.js, myName: ${window.gameData.playerBaseStats?.name}, opponentName: ${window.gameData.opponentBaseStats?.name}`); - if (window.gameUI && typeof window.gameUI.updateUI === 'function') window.gameUI.updateUI(); - if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) { - data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type)); + if (window.gameUI?.updateUI) window.gameUI.updateUI(); + if (window.gameUI?.addToLog && data.log) { + data.log.forEach(log => window.gameUI.addToLog(log.message, log.type)); } - if (window.gameUI && typeof window.gameUI.showGameOver === 'function') { - const opponentKeyForModal = window.gameData?.opponentBaseStats?.characterKey; - window.gameUI.showGameOver(playerWon, data.reason, opponentKeyForModal, data); + if (window.gameUI?.showGameOver) { + const oppKey = window.gameData?.opponentBaseStats?.characterKey; + window.gameUI.showGameOver(playerWon, data.reason, oppKey, data); } if (returnToMenuButton) returnToMenuButton.disabled = false; setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли.")); - // === ИЗМЕНЕНИЕ: При gameOver скрываем таймер или показываем "Игра окончена" === - if (turnTimerContainer) turnTimerContainer.style.display = 'block'; // Оставляем видимым - if (turnTimerSpan) turnTimerSpan.textContent = 'Конец'; - // === КОНЕЦ ИЗМЕНЕНИЯ === + if (window.gameUI?.updateTurnTimerDisplay) { // Обновляем UI таймера + window.gameUI.updateTurnTimerDisplay(null, false, currentGameState?.gameMode); // Передаем null, чтобы показать "Конец" или скрыть + } }); socket.on('waitingForOpponent', () => { if (!isLoggedIn) return; setGameStatusMessage("Ожидание присоединения оппонента..."); - disableGameControls(); - enableSetupButtons(); // Можно оставить возможность отменить, если долго ждет - // === ИЗМЕНЕНИЕ: При ожидании оппонента таймер неактивен === - if (turnTimerContainer) turnTimerContainer.style.display = 'none'; - if (turnTimerSpan) turnTimerSpan.textContent = '--'; - // === КОНЕЦ ИЗМЕНЕНИЯ === + disableGameControls(); // Боевые кнопки неактивны + disableSetupButtons(); // Кнопки создания/присоединения тоже, пока ждем + if (createPvPGameButton) createPvPGameButton.disabled = false; // Оставляем активной "Создать PvP" для отмены + if (window.gameUI?.updateTurnTimerDisplay) { + window.gameUI.updateTurnTimerDisplay(null, false, 'pvp'); // Таймер неактивен + } }); socket.on('opponentDisconnected', (data) => { - 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 || 'Противник'; - if (window.gameUI && typeof window.gameUI.addToLog === 'function') { - window.gameUI.addToLog(`🔌 Противник (${disconnectedCharacterName}) отключился.`, systemLogType); - } + if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) return; + const name = data.disconnectedCharacterName || 'Противник'; + if (window.gameUI?.addToLog) window.gameUI.addToLog(`🔌 Противник (${name}) отключился.`, 'system'); if (currentGameState && !currentGameState.isGameOver) { - setGameStatusMessage(`Противник (${disconnectedCharacterName}) отключился. Ожидание завершения игры сервером...`, true); + setGameStatusMessage(`Противник (${name}) отключился. Ожидание...`, true); disableGameControls(); } }); socket.on('gameError', (data) => { console.error('[Client] Server error:', data.message); - const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system'; - 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); + if (isLoggedIn && isInGame && currentGameState && !currentGameState.isGameOver && window.gameUI?.addToLog) { + window.gameUI.addToLog(`❌ Ошибка игры: ${data.message}`, 'system'); + disableGameControls(); setGameStatusMessage(`Ошибка: ${data.message}.`, true); } else { - setGameStatusMessage(`❌ Ошибка игры: ${data.message}`, true); - resetGameVariables(); isInGame = false; disableGameControls(); - if (isLoggedIn && loggedInUsername) showGameSelectionScreen(loggedInUsername); - else showAuthScreen(); + setGameStatusMessage(`❌ Ошибка: ${data.message}`, true); + if (isLoggedIn) enableSetupButtons(); // Если на экране выбора игры, включаем кнопки + else { // Если на экране логина + if(registerForm) registerForm.querySelector('button').disabled = false; + if(loginForm) loginForm.querySelector('button').disabled = false; + } } - if (!isLoggedIn) { - if (registerForm) registerForm.querySelector('button').disabled = false; - if (loginForm) loginForm.querySelector('button').disabled = false; - } else if (!isInGame) { enableSetupButtons(); } }); socket.on('availablePvPGamesList', (games) => { @@ -572,58 +552,32 @@ document.addEventListener('DOMContentLoaded', () => { updateAvailableGamesList(games); }); - socket.on('noPendingGamesFound', (data) => { + socket.on('noPendingGamesFound', (data) => { // Вызывается, когда создается новая игра после поиска if (!isLoggedIn) return; - setGameStatusMessage(data.message || "Свободных игр не найдено. Создана новая для вас, ожидайте оппонента."); - updateAvailableGamesList([]); - isInGame = false; disableGameControls(); disableSetupButtons(); - // === ИЗМЕНЕНИЕ: При ожидании оппонента (создана новая игра) таймер неактивен === - if (turnTimerContainer) turnTimerContainer.style.display = 'none'; - if (turnTimerSpan) turnTimerSpan.textContent = '--'; - // === КОНЕЦ ИЗМЕНЕНИЯ === + setGameStatusMessage(data.message || "Свободных игр не найдено. Создана новая для вас."); + updateAvailableGamesList([]); // Очищаем список + // currentGameId и myPlayerId должны были прийти с gameCreated + isInGame = false; // Еще не в активной фазе боя + disableGameControls(); + disableSetupButtons(); // Мы в ожидающей игре + if (window.gameUI?.updateTurnTimerDisplay) { + window.gameUI.updateTurnTimerDisplay(null, false, 'pvp'); + } }); - // === ИЗМЕНЕНИЕ: Обработчик события обновления таймера === socket.on('turnTimerUpdate', (data) => { if (!isInGame || !currentGameState || currentGameState.isGameOver) { - // Если игра не активна, или уже завершена, или нет состояния, игнорируем обновление таймера - if (turnTimerContainer && !currentGameState?.isGameOver) turnTimerContainer.style.display = 'none'; // Скрываем, если не game over - if (turnTimerSpan && !currentGameState?.isGameOver) turnTimerSpan.textContent = '--'; + if (window.gameUI?.updateTurnTimerDisplay && !currentGameState?.isGameOver) { // Только если не game over + window.gameUI.updateTurnTimerDisplay(null, false, currentGameState?.gameMode); + } return; } - - if (turnTimerSpan && turnTimerContainer) { - if (data.remainingTime === null || data.remainingTime === undefined) { - // Сервер сигнализирует, что таймер неактивен (например, ход AI) - turnTimerContainer.style.display = 'block'; // Контейнер может быть видимым - // Определяем, чей ход, чтобы показать соответствующее сообщение - const isMyActualTurn = myPlayerId && currentGameState.isPlayerTurn === (myPlayerId === GAME_CONFIG.PLAYER_ID); - - if (!data.isPlayerTurn && currentGameState.gameMode === 'ai') { // Ход AI - turnTimerSpan.textContent = 'Ход ИИ'; - turnTimerSpan.classList.remove('low-time'); - } else if (!isMyActualTurn && currentGameState.gameMode === 'pvp' && !data.isPlayerTurn !== (myPlayerId === GAME_CONFIG.PLAYER_ID)) { // Ход оппонента в PvP - turnTimerSpan.textContent = 'Ход оппонента'; - turnTimerSpan.classList.remove('low-time'); - } else { // Ход текущего игрока, но сервер прислал null - странно, но покажем '--' - turnTimerSpan.textContent = '--'; - turnTimerSpan.classList.remove('low-time'); - } - } else { - turnTimerContainer.style.display = 'block'; // Убедимся, что контейнер виден - const seconds = Math.ceil(data.remainingTime / 1000); - turnTimerSpan.textContent = `0:${seconds < 10 ? '0' : ''}${seconds}`; - - // Добавляем/удаляем класс для предупреждения, если времени мало - if (seconds <= 10) { // Например, 10 секунд - порог - turnTimerSpan.classList.add('low-time'); - } else { - turnTimerSpan.classList.remove('low-time'); - } - } + if (window.gameUI && typeof window.gameUI.updateTurnTimerDisplay === 'function') { + // Определяем, является ли текущий ход ходом этого клиента + const isMyActualTurn = myPlayerId && currentGameState.isPlayerTurn === (myPlayerId === GAME_CONFIG.PLAYER_ID); + window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyActualTurn, currentGameState.gameMode); } }); - // === КОНЕЦ ИЗМЕНЕНИЯ === - showAuthScreen(); + showAuthScreen(); // Начальный экран }); \ No newline at end of file diff --git a/public/js/ui.js b/public/js/ui.js index a96d57e..e7e7922 100644 --- a/public/js/ui.js +++ b/public/js/ui.js @@ -32,10 +32,8 @@ buttonAttack: document.getElementById('button-attack'), buttonBlock: document.getElementById('button-block'), abilitiesGrid: document.getElementById('abilities-grid'), - // === ИЗМЕНЕНИЕ: Добавлены элементы таймера === turnTimerContainer: document.getElementById('turn-timer-container'), turnTimerSpan: document.getElementById('turn-timer') - // === КОНЕЦ ИЗМЕНЕНИЯ === }, log: { list: document.getElementById('log-list'), @@ -51,6 +49,15 @@ opponentResourceTypeIcon: document.getElementById('opponent-resource-bar')?.closest('.stat-bar-container')?.querySelector('.bar-icon i'), playerResourceBarContainer: document.getElementById('player-resource-bar')?.closest('.stat-bar-container'), opponentResourceBarContainer: document.getElementById('opponent-resource-bar')?.closest('.stat-bar-container'), + + // === НОВЫЕ ЭЛЕМЕНТЫ для переключателя панелей === + panelSwitcher: { + controlsContainer: document.querySelector('.panel-switcher-controls'), + showPlayerBtn: document.getElementById('show-player-panel-btn'), + showOpponentBtn: document.getElementById('show-opponent-panel-btn') + }, + battleArenaContainer: document.querySelector('.battle-arena-container') + // === КОНЕЦ НОВЫХ ЭЛЕМЕНТОВ === }; function addToLog(message, type = 'info') { @@ -91,9 +98,9 @@ if (elements.name) { let iconClass = 'fa-question'; const characterKey = fighterBaseStats.characterKey; - if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-elena'; } // Используем специфичный класс для цвета + if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-elena'; } else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; } - else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-balard'; } // Для Баларда тоже специфичный + else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-balard'; } let nameHtml = ` ${fighterBaseStats.name || 'Неизвестно'}`; if (isControlledByThisClient) nameHtml += " (Вы)"; elements.name.innerHTML = nameHtml; @@ -140,7 +147,7 @@ else if (fighterBaseStats.characterKey === 'balard') { elements.panel.classList.add('panel-balard'); borderColorVar = 'var(--accent-opponent)'; } let glowColorVar = 'rgba(0, 0, 0, 0.4)'; if (fighterBaseStats.characterKey === 'elena') glowColorVar = 'var(--panel-glow-player)'; - else if (fighterBaseStats.characterKey === 'almagest') glowColorVar = 'var(--panel-glow-almagest)'; // Отдельный цвет для Альмагест + else if (fighterBaseStats.characterKey === 'almagest') glowColorVar = 'var(--panel-glow-almagest)'; else if (fighterBaseStats.characterKey === 'balard') glowColorVar = 'var(--panel-glow-opponent)'; elements.panel.style.borderColor = borderColorVar; elements.panel.style.boxShadow = `0 0 15px ${glowColorVar}, inset 0 0 10px rgba(0, 0, 0, 0.3)`; @@ -207,22 +214,14 @@ } } - // === ИЗМЕНЕНИЕ: Новая функция для обновления таймера === - /** - * Обновляет отображение таймера хода. - * @param {number|null} remainingTimeMs - Оставшееся время в миллисекундах, или null если таймер неактивен. - * @param {boolean} isCurrentPlayerActualTurn - Флаг, является ли текущий ход ходом этого клиента. - * @param {string} gameMode - Режим игры ('ai' или 'pvp'). - */ function updateTurnTimerDisplay(remainingTimeMs, isCurrentPlayerActualTurn, gameMode) { const timerSpan = uiElements.controls.turnTimerSpan; const timerContainer = uiElements.controls.turnTimerContainer; - const config = window.GAME_CONFIG || {}; if (!timerSpan || !timerContainer) return; if (window.gameState && window.gameState.isGameOver) { - timerContainer.style.display = 'block'; // Может быть 'flex' или другой, в зависимости от CSS + timerContainer.style.display = 'block'; timerSpan.textContent = 'Конец'; timerSpan.classList.remove('low-time'); return; @@ -231,11 +230,11 @@ if (remainingTimeMs === null || remainingTimeMs === undefined) { timerContainer.style.display = 'block'; timerSpan.classList.remove('low-time'); - if (gameMode === 'ai' && !isCurrentPlayerActualTurn) { // Предполагаем, что если не ход игрока в AI, то ход AI + if (gameMode === 'ai' && !isCurrentPlayerActualTurn) { timerSpan.textContent = 'Ход ИИ'; } else if (gameMode === 'pvp' && !isCurrentPlayerActualTurn) { timerSpan.textContent = 'Ход оппонента'; - } else { // Ход текущего игрока, но нет времени (например, ожидание первого хода) + } else { timerSpan.textContent = '--'; } } else { @@ -243,14 +242,13 @@ const seconds = Math.ceil(remainingTimeMs / 1000); timerSpan.textContent = `0:${seconds < 10 ? '0' : ''}${seconds}`; - if (seconds <= 10 && isCurrentPlayerActualTurn) { // Предупреждение только если это мой ход + if (seconds <= 10 && isCurrentPlayerActualTurn) { timerSpan.classList.add('low-time'); } else { timerSpan.classList.remove('low-time'); } } } - // === КОНЕЦ ИЗМЕНЕНИЯ === function updateUI() { @@ -267,13 +265,11 @@ if(uiElements.controls.buttonAttack) uiElements.controls.buttonAttack.disabled = true; if(uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true; if(uiElements.controls.abilitiesGrid) uiElements.controls.abilitiesGrid.innerHTML = 'Загрузка способностей...
'; - // === ИЗМЕНЕНИЕ: Сбрасываем таймер, если нет данных === if (uiElements.controls.turnTimerContainer) uiElements.controls.turnTimerContainer.style.display = 'none'; if (uiElements.controls.turnTimerSpan) { uiElements.controls.turnTimerSpan.textContent = '--'; uiElements.controls.turnTimerSpan.classList.remove('low-time'); } - // === КОНЕЦ ИЗМЕНЕНИЯ === return; } if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) { @@ -423,8 +419,7 @@ const currentActualGameState = window.gameState; const gameOverScreenElement = uiElements.gameOver.screen; - console.log(`[UI.JS DEBUG] showGameOver CALLED. PlayerWon: ${playerWon}, Reason: ${reason}`); - if (!gameOverScreenElement) { console.warn("[UI.JS DEBUG] showGameOver: gameOverScreenElement not found."); return; } + if (!gameOverScreenElement) { return; } const resultMsgElement = uiElements.gameOver.message; const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок"; @@ -433,19 +428,16 @@ if (resultMsgElement) { let winText = `Победа! ${myNameForResult} празднует!`; let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`; - // === ИЗМЕНЕНИЕ: Добавляем обработку причины 'turn_timeout' === if (reason === 'opponent_disconnected') { let disconnectedName = data?.disconnectedCharacterName || opponentNameForResult; winText = `${disconnectedName} покинул(а) игру. Победа присуждается вам!`; } else if (reason === 'turn_timeout') { - // Если текущий игрок (чей ход был) проиграл по таймауту - if (!playerWon) { // playerWon здесь будет false, если победил оппонент (т.е. мой таймаут) + if (!playerWon) { loseText = `Время на ход истекло! Поражение. ${opponentNameForResult} побеждает!`; - } else { // Если я победил, потому что у оппонента истекло время + } else { winText = `Время на ход у ${opponentNameForResult} истекло! Победа!`; } } - // === КОНЕЦ ИЗМЕНЕНИЯ === resultMsgElement.textContent = playerWon ? winText : loseText; resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)'; } @@ -468,7 +460,7 @@ opponentPanelElement.style.transition = ''; } - setTimeout((finalStateInTimeout, wonInTimeout, reasonInTimeout, keyInTimeout, dataInTimeout) => { + setTimeout((finalStateInTimeout) => { if (gameOverScreenElement && finalStateInTimeout && finalStateInTimeout.isGameOver === true) { if (gameOverScreenElement.classList.contains(config.CSS_CLASS_HIDDEN || 'hidden')) { gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden'); @@ -496,16 +488,47 @@ gameOverScreenElement.offsetHeight; } } - }, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState, playerWon, reason, opponentCharacterKeyFromClient, data); + }, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState); } + // === НОВАЯ ФУНКЦИЯ для настройки переключателя панелей === + function setupPanelSwitcher() { + const { showPlayerBtn, showOpponentBtn } = uiElements.panelSwitcher; + const battleArena = uiElements.battleArenaContainer; + + if (showPlayerBtn && showOpponentBtn && battleArena) { + showPlayerBtn.addEventListener('click', () => { + battleArena.classList.remove('show-opponent-panel'); + showPlayerBtn.classList.add('active'); + showOpponentBtn.classList.remove('active'); + }); + + showOpponentBtn.addEventListener('click', () => { + battleArena.classList.add('show-opponent-panel'); + showOpponentBtn.classList.add('active'); + showPlayerBtn.classList.remove('active'); + }); + + // По умолчанию при загрузке (если кнопки видимы) панель игрока активна + // CSS уже должен это обеспечивать, но для надежности можно убедиться + if (window.getComputedStyle(uiElements.panelSwitcher.controlsContainer).display !== 'none') { + battleArena.classList.remove('show-opponent-panel'); + showPlayerBtn.classList.add('active'); + showOpponentBtn.classList.remove('active'); + } + } + } + // === КОНЕЦ НОВОЙ ФУНКЦИИ === + window.gameUI = { uiElements, addToLog, updateUI, showGameOver, - // === ИЗМЕНЕНИЕ: Экспортируем функцию обновления таймера === updateTurnTimerDisplay - // === КОНЕЦ ИЗМЕНЕНИЯ === }; + + // Настраиваем переключатель панелей при загрузке скрипта + setupPanelSwitcher(); + })(); \ No newline at end of file diff --git a/public/style_alt.css b/public/style_alt.css index 531a3b7..ba69ff7 100644 --- a/public/style_alt.css +++ b/public/style_alt.css @@ -3,7 +3,7 @@ @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css'); :root { - /* --- Переменные цветов и шрифтов --- */ + /* --- Переменные цветов и шрифтов (из локальной версии) --- */ --font-main: 'Roboto', sans-serif; --font-fancy: 'MedievalSharp', cursive; @@ -68,6 +68,14 @@ --timer-text-color: var(--turn-color); --timer-icon-color: #b0c4de; --timer-low-time-color: var(--damage-color); + + /* === Переменные для переключателя панелей (мобильный вид) - ИЗ СЕРВЕРНОЙ ВЕРСИИ === */ + --panel-switcher-bg: rgba(10, 12, 20, 0.9); + --panel-switcher-border: var(--panel-border); + --panel-switcher-button-bg: var(--button-bg); + --panel-switcher-button-text: var(--button-text); + --panel-switcher-button-active-bg: var(--accent-player); + --panel-switcher-button-active-text: #fff; } /* --- Базовые Стили и Сброс --- */ @@ -134,7 +142,7 @@ i { } -/* === Стили для Экранов Аутентификации и Настройки Игры === */ +/* === Стили для Экранов Аутентификации и Настройки Игры (из локальной версии) === */ .auth-game-setup-wrapper { width: 100%; max-width: 700px; @@ -146,8 +154,8 @@ i { color: var(--text-light); text-align: center; max-height: calc(100vh - 40px); - overflow-y: hidden; - position: relative; /* <<< Добавлено для позиционирования #user-info */ + overflow-y: hidden; /* Сохраняем из локальной */ + position: relative; /* <<< Добавлено для позиционирования #user-info (из локальной) */ } .auth-game-setup-wrapper h2, @@ -235,7 +243,7 @@ i { margin-top: 20px; text-align: left; max-height: 250px; - height: 100px; + height: 100px; /* Сохраняем из локальной */ overflow-y: scroll; padding: 10px 15px; background-color: rgba(0, 0, 0, 0.25); @@ -277,7 +285,7 @@ i { } #status-container { - height: 40px; + height: 40px; /* Сохраняем из локальной */ } #auth-message, @@ -309,7 +317,7 @@ i { margin-bottom: 20px; } -/* === ИЗМЕНЕНИЕ: Стили для #user-info === */ +/* === ИЗМЕНЕНИЕ: Стили для #user-info (из локальной версии) === */ #user-info { position: absolute; top: 10px; /* Отступ сверху */ @@ -423,7 +431,6 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } - /* --- Основная Структура Игры (.game-wrapper) --- */ .game-wrapper { width: 100%; @@ -437,16 +444,51 @@ label[for="char-almagest"] i { overflow: hidden; } -/* === ИЗМЕНЕНИЕ: .game-header удален, стили для него больше не нужны === */ +/* === ИЗМЕНЕНИЕ: .game-header удален, стили для него больше не нужны (из локальной версии) === */ +/* Глобальные стили для кнопок переключения панелей - ИЗ СЕРВЕРНОЙ ВЕРСИИ */ +.panel-switcher-controls { + display: none; /* Скрыт по умолчанию для десктопа */ + flex-shrink: 0; + padding: 8px 5px; + background: var(--panel-switcher-bg); + border-bottom: 1px solid var(--panel-switcher-border); + gap: 10px; +} +.panel-switch-button { + flex: 1; + padding: 8px 10px; + font-size: 0.9em; + font-weight: bold; + text-transform: uppercase; + background: var(--panel-switcher-button-bg); + color: var(--panel-switcher-button-text); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s, color 0.2s, transform 0.1s; + display: flex; + align-items: center; + justify-content: center; +} +.panel-switch-button i { margin-right: 8px; } +.panel-switch-button:hover { filter: brightness(1.1); } +.panel-switch-button.active { + background: var(--panel-switcher-button-active-bg); + color: var(--panel-switcher-button-active-text); + box-shadow: 0 0 8px rgba(255,255,255,0.3); +} .battle-arena-container { flex-grow: 1; display: flex; gap: 10px; overflow: hidden; - /* === ИЗМЕНЕНИЕ: Добавляем верхний отступ, если .game-header был убран, а .game-wrapper виден === */ + /* === ИЗМЕНЕНИЕ: Добавляем верхний отступ, если .game-header был убран, а .game-wrapper виден (из локальной версии) === */ /* margin-top: 10px; /* или padding-top: 10px; на .game-wrapper, если нужно */ + /* === Изменения из серверной для работы переключения панелей === */ + position: relative; + min-height: 0; } .player-column, @@ -459,6 +501,7 @@ label[for="char-almagest"] i { overflow: hidden; } +/* Остальные стили панелей, кнопок, лога и т.д. из локальной версии */ .fighter-panel, .controls-panel-new, .battle-log-new { @@ -531,6 +574,7 @@ label[for="char-almagest"] i { padding-right: 5px; display: flex; flex-direction: column; + gap: 10px; /* Добавлено из серверной версии для консистентности */ min-height: 0; padding-top: 10px; margin-top: 0; @@ -547,7 +591,6 @@ label[for="char-almagest"] i { flex-shrink: 0; font-size: 1.4em; } - .stat-bar-container.health .bar-icon { color: var(--hp-color); } .stat-bar-container.mana .bar-icon { color: var(--mana-color); } .stat-bar-container.stamina .bar-icon { color: var(--stamina-color); } @@ -595,7 +638,6 @@ label[for="char-almagest"] i { white-space: nowrap; pointer-events: none; } - .health .bar-fill { background-color: var(--hp-color); } .mana .bar-fill { background-color: var(--mana-color); } .stamina .bar-fill { background-color: var(--stamina-color); } @@ -637,6 +679,7 @@ label[for="char-almagest"] i { font-size: 0.9em; display: flex; flex-direction: column; + gap: 8px; /* Добавлено из серверной версии для консистентности */ flex-shrink: 0; min-height: 3em; } @@ -688,7 +731,6 @@ label[for="char-almagest"] i { white-space: nowrap; vertical-align: baseline; } - .effect-buff { border-color: var(--heal-color); color: var(--heal-color); } .effect-debuff { border-color: var(--damage-color); color: var(--damage-color); } .effect-stun { border-color: var(--turn-color); color: var(--turn-color); } @@ -920,7 +962,6 @@ label[for="char-almagest"] i { border: 2px dashed var(--damage-color); animation: pulse-red-border 1s infinite ease-in-out; } - .ability-button.not-enough-resource:disabled { border-color: var(--damage-color); box-shadow: inset 0 0 8px rgba(255, 80, 80, 0.2), 0 3px 6px rgba(0, 0, 0, 0.2), inset 0 1px 3px rgba(0, 0, 0, 0.4); @@ -1023,7 +1064,6 @@ label[for="char-almagest"] i { #log-list li:hover { background-color: rgba(255, 255, 255, 0.03); } - .log-damage { color: var(--damage-color); font-weight: 500; } .log-heal { color: var(--heal-color); font-weight: 500; } .log-block { color: var(--block-color); font-style: italic; } @@ -1211,35 +1251,79 @@ label[for="char-almagest"] i { animation: shake-short 0.3s ease-in-out; } +/* --- Отзывчивость (Медиа-запросы) --- */ @media (max-width: 900px) { body { - height: auto; overflow-y: auto; + height: auto; min-height: 100vh; /* Из серверной, чтобы обеспечить высоту */ + overflow-y: auto; padding: 5px 0; font-size: 15px; justify-content: flex-start; } - .auth-game-setup-wrapper { max-height: none; padding-top: 60px; /* Отступ для #user-info */ } - /* === ИЗМЕНЕНИЕ: Адаптация #user-info === */ + .auth-game-setup-wrapper { + max-height: none; + padding-top: 60px; /* Отступ для #user-info из локальной */ + } + /* === ИЗМЕНЕНИЕ: Адаптация #user-info (из локальной версии) === */ #user-info { top: 5px; right: 10px; } #user-info p { font-size: 0.85em; } #logout-button { padding: 5px 10px !important; font-size: 0.75em !important; } /* === КОНЕЦ ИЗМЕНЕНИЯ === */ - .game-wrapper { padding: 5px; gap: 5px; height: auto; } - /* === ИЗМЕНЕНИЕ: game-header удален === */ - .battle-arena-container { flex-direction: column; height: auto; overflow: visible; /* margin-top: 0; /* Если ранее добавляли */ } - .player-column, .opponent-column { width: 100%; height: auto; overflow: visible; } - .fighter-panel, .controls-panel-new, .battle-log-new { - min-height: auto; height: auto; padding: 10px; - flex-grow: 0; flex-shrink: 1; + .game-wrapper { padding: 5px; gap: 5px; height: auto; min-height: calc(100vh - 10px); width: 100%; } /* min-height и width из серверной */ + /* === ИЗМЕНЕНИЕ: game-header удален (из локальной версии) === */ + + /* Показываем кнопки переключения на мобильных - ИЗ СЕРВЕРНОЙ ВЕРСИИ */ + .panel-switcher-controls { + display: flex; } - .controls-panel-new { min-height: 200px; } - .battle-log-new { height: auto; min-height: 150px; } + + .battle-arena-container { + /* flex-direction: column; height: auto; overflow: visible; - из локальной версии заменяется логикой ниже */ + gap: 0; /* Убираем отступ между колонками, т.к. они будут накладываться - ИЗ СЕРВЕРНОЙ ВЕРСИИ */ + /* position: relative; overflow: hidden; flex-grow: 1; min-height: 350px; - Эти стили уже есть глобально, но тут подтверждаем */ + } + + /* Стили для колонок при переключении - ИЗ СЕРВЕРНОЙ ВЕРСИИ */ + .player-column, + .opponent-column { + /* width: 100%; height: auto; overflow: visible; - из локальной версии заменяется логикой ниже */ + position: absolute; /* Для наложения */ + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow-y: auto; /* Прокрутка содержимого колонки */ + transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; + padding: 5px; /* Добавлено для отступов внутри колонок на мобильных */ + gap: 8px; /* Добавлено для отступов между панелями внутри колонок */ + } + + .player-column { transform: translateX(0); opacity: 1; z-index: 10; pointer-events: auto; } + .opponent-column { transform: translateX(100%); opacity: 0; z-index: 5; pointer-events: none; } + + .battle-arena-container.show-opponent-panel .player-column { transform: translateX(-100%); opacity: 0; z-index: 5; pointer-events: none; } + .battle-arena-container.show-opponent-panel .opponent-column { transform: translateX(0); opacity: 1; z-index: 10; pointer-events: auto; } + + + .fighter-panel, .controls-panel-new, .battle-log-new { + min-height: auto; /* Высота по контенту */ + height: auto; + padding: 10px; + flex-grow: 0; /* Локальное */ + flex-shrink: 1; /* Локальное */ + } + .fighter-panel { flex-shrink: 0; } /* Из серверной для panel-switcher */ + .fighter-panel .panel-content { flex-grow: 1; min-height: 0; } /* Из серверной для panel-switcher */ + + .controls-panel-new { min-height: 200px; flex-shrink: 0; } /* flex-shrink из серверной */ + .battle-log-new { height: auto; min-height: 150px; flex-shrink: 0; } /* flex-shrink из серверной */ + #log-list { max-height: 200px; } .abilities-grid { max-height: none; overflow-y: visible; padding-bottom: 8px; } .abilities-grid::after { display: none; } - .ability-list, .controls-layout { overflow: visible; } + .ability-list, .controls-layout { overflow: visible; } /* Локальное */ .fighter-name { font-size: 1.3em; } - .panel-content { margin-top: 10px; } + .panel-content { margin-top: 10px; /* Локальное, но теперь panel-content изменен для серверного panel-switcher, возможно, не нужно */ } .stat-bar-container .bar-icon { font-size: 1.2em; } .bar { height: 18px; } .effects-area, .effect { font-size: 0.85em; } @@ -1265,7 +1349,7 @@ label[for="char-almagest"] i { @media (max-width: 480px) { body { font-size: 14px; } - /* === ИЗМЕНЕНИЕ: Адаптация #user-info для мобильных === */ + /* === ИЗМЕНЕНИЕ: Адаптация #user-info для мобильных (из локальной версии) === */ .auth-game-setup-wrapper { padding-top: 50px; /* Еще немного места сверху */ } #user-info { top: 5px; @@ -1280,8 +1364,14 @@ label[for="char-almagest"] i { #logout-button i { margin-right: 3px; } /* === КОНЕЦ ИЗМЕНЕНИЯ === */ - /* === ИЗМЕНЕНИЕ: game-header удален === */ + /* Стили для panel-switcher на очень маленьких экранах - ИЗ СЕРВЕРНОЙ ВЕРСИИ */ + .panel-switch-button .button-text { display: none; } /* Скрываем текст, оставляем иконки */ + .panel-switch-button i { margin-right: 0; font-size: 1.2em; } + .panel-switch-button { padding: 6px 8px; } + + /* Локальные изменения */ .fighter-name { font-size: 1.2em; } + .avatar-image { max-width: 40px; } /* Из серверной, но не противоречит */ .abilities-grid { grid-template-columns: repeat(auto-fit, minmax(65px, 1fr)); gap: 5px; padding: 5px; padding-bottom: 10px; } .ability-button { font-size: 0.7em; padding: 4px; } .ability-button .ability-name { margin-bottom: 1px; } diff --git a/server/auth/authService.js b/server/auth/authService.js new file mode 100644 index 0000000..f0a6e79 --- /dev/null +++ b/server/auth/authService.js @@ -0,0 +1,133 @@ +// /server/auth/authService.js +const bcrypt = require('bcryptjs'); // Для хеширования паролей +const db = require('../core/db'); // Путь к вашему модулю для работы с базой данных (в папке core) + +const SALT_ROUNDS = 10; // Количество раундов для генерации соли bcrypt + +/** + * Регистрирует нового пользователя. + * @param {string} username - Имя пользователя. + * @param {string} password - Пароль пользователя. + * @returns {Promise