diff --git a/public/js/gameSetup.js b/public/js/gameSetup.js index af4314d..13ead6d 100644 --- a/public/js/gameSetup.js +++ b/public/js/gameSetup.js @@ -1,12 +1,57 @@ // /public/js/gameSetup.js +// ПРИМЕРНЫЕ РЕАЛИЗАЦИИ ВСПОМОГАТЕЛЬНЫХ ФУНКЦИЙ (лучше передавать из main.js) +/* +function parseJwtPayloadForValidation(token) { + try { + if (typeof token !== 'string') return null; + const base64Url = token.split('.')[1]; + if (!base64Url) return null; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + return JSON.parse(jsonPayload); + } catch (e) { + return null; + } +} + +function isTokenValid(token) { + if (!token) { + return false; + } + const decodedToken = parseJwtPayloadForValidation(token); + if (!decodedToken || !decodedToken.exp) { + localStorage.removeItem('jwtToken'); + return false; + } + const currentTimeInSeconds = Math.floor(Date.now() / 1000); + if (decodedToken.exp < currentTimeInSeconds) { + localStorage.removeItem('jwtToken'); + return false; + } + return true; +} +*/ +// Конец примерных реализаций + export function initGameSetup(dependencies) { - const { socket, clientState, ui } = dependencies; + const { socket, clientState, ui, utils } = dependencies; // Предполагаем, что utils.isTokenValid передается const { createAIGameButton, createPvPGameButton, joinPvPGameButton, findRandomPvPGameButton, gameIdInput, availableGamesDiv, pvpCharacterRadios } = ui.elements; + // Получаем функцию isTokenValid либо из utils, либо используем локальную, если она определена выше + const checkTokenValidity = utils?.isTokenValid || window.isTokenValidFunction; // window.isTokenValidFunction - если вы определили ее глобально/локально + + if (typeof checkTokenValidity !== 'function') { + console.error("[GameSetup.js] CRITICAL: isTokenValid function is not available. Auth checks will fail."); + // Можно добавить фоллбэк или аварийное поведение + } + + // --- Вспомогательные функции --- function getSelectedCharacterKey() { let selectedKey = 'elena'; // Значение по умолчанию @@ -28,14 +73,12 @@ export function initGameSetup(dependencies) { games.forEach(game => { if (game && game.id) { const li = document.createElement('li'); - // Отображаем только часть ID для краткости li.textContent = `ID: ${game.id.substring(0, 8)}... - ${game.status || 'Ожидает игрока'}`; const joinBtn = document.createElement('button'); joinBtn.textContent = 'Присоединиться'; joinBtn.dataset.gameId = game.id; - // Деактивация кнопки "Присоединиться" для своих игр if (clientState.isLoggedIn && clientState.myUserId && game.ownerIdentifier === clientState.myUserId) { joinBtn.disabled = true; joinBtn.title = "Вы не можете присоединиться к своей же ожидающей игре."; @@ -44,13 +87,21 @@ export function initGameSetup(dependencies) { } joinBtn.addEventListener('click', (e) => { - if (!clientState.isLoggedIn) { - ui.setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true); + // --- ПРОВЕРКА ТОКЕНА ПЕРЕД ДЕЙСТВИЕМ --- + if (typeof checkTokenValidity === 'function' && (!clientState.isLoggedIn || !checkTokenValidity(localStorage.getItem('jwtToken')))) { + if (typeof ui.redirectToLogin === 'function') { + ui.redirectToLogin('Для присоединения к игре необходимо войти или обновить сессию.'); + } else { + alert('Для присоединения к игре необходимо войти или обновить сессию.'); + window.location.href = '/'; // Фоллбэк + } return; } - if (e.target.disabled) return; // Не обрабатывать клик по отключенной кнопке + // --- КОНЕЦ ПРОВЕРКИ ТОКЕНА --- - ui.disableSetupButtons(); // Блокируем все кнопки выбора игры + if (e.target.disabled) return; + + ui.disableSetupButtons(); socket.emit('joinGame', { gameId: e.target.dataset.gameId }); ui.setGameStatusMessage(`Присоединение к игре ${e.target.dataset.gameId.substring(0, 8)}...`); }); @@ -62,30 +113,44 @@ export function initGameSetup(dependencies) { } else { availableGamesDiv.innerHTML += '

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

'; } - ui.enableSetupButtons(); // Включаем основные кнопки создания/поиска после обновления списка + ui.enableSetupButtons(); } // --- Обработчики событий DOM --- if (createAIGameButton) { createAIGameButton.addEventListener('click', () => { - if (!clientState.isLoggedIn) { - ui.setGameStatusMessage("Пожалуйста, войдите, чтобы создать игру.", true); + // --- ПРОВЕРКА ТОКЕНА ПЕРЕД ДЕЙСТВИЕМ --- + if (typeof checkTokenValidity === 'function' && (!clientState.isLoggedIn || !checkTokenValidity(localStorage.getItem('jwtToken')))) { + if (typeof ui.redirectToLogin === 'function') { + ui.redirectToLogin('Для создания игры необходимо войти или обновить сессию.'); + } else { + alert('Для создания игры необходимо войти или обновить сессию.'); + window.location.href = '/'; // Фоллбэк + } return; } + // --- КОНЕЦ ПРОВЕРКИ ТОКЕНА --- + ui.disableSetupButtons(); - // Для AI игры персонаж может быть фиксированным или выбираемым - // В вашем client.js был 'elena', оставим так - socket.emit('createGame', { mode: 'ai', characterKey: 'elena' }); + socket.emit('createGame', { mode: 'ai', characterKey: 'elena' }); // Персонаж для AI может быть фиксированным ui.setGameStatusMessage("Создание игры против AI..."); }); } if (createPvPGameButton) { createPvPGameButton.addEventListener('click', () => { - if (!clientState.isLoggedIn) { - ui.setGameStatusMessage("Пожалуйста, войдите, чтобы создать игру.", true); + // --- ПРОВЕРКА ТОКЕНА ПЕРЕД ДЕЙСТВИЕМ --- + if (typeof checkTokenValidity === 'function' && (!clientState.isLoggedIn || !checkTokenValidity(localStorage.getItem('jwtToken')))) { + if (typeof ui.redirectToLogin === 'function') { + ui.redirectToLogin('Для создания PvP игры необходимо войти или обновить сессию.'); + } else { + alert('Для создания PvP игры необходимо войти или обновить сессию.'); + window.location.href = '/'; // Фоллбэк + } return; } + // --- КОНЕЦ ПРОВЕРКИ ТОКЕНА --- + ui.disableSetupButtons(); const characterKey = getSelectedCharacterKey(); socket.emit('createGame', { mode: 'pvp', characterKey: characterKey }); @@ -95,10 +160,18 @@ export function initGameSetup(dependencies) { if (joinPvPGameButton) { joinPvPGameButton.addEventListener('click', () => { - if (!clientState.isLoggedIn) { - ui.setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true); + // --- ПРОВЕРКА ТОКЕНА ПЕРЕД ДЕЙСТВИЕМ --- + if (typeof checkTokenValidity === 'function' && (!clientState.isLoggedIn || !checkTokenValidity(localStorage.getItem('jwtToken')))) { + if (typeof ui.redirectToLogin === 'function') { + ui.redirectToLogin('Для присоединения к игре необходимо войти или обновить сессию.'); + } else { + alert('Для присоединения к игре необходимо войти или обновить сессию.'); + window.location.href = '/'; // Фоллбэк + } return; } + // --- КОНЕЦ ПРОВЕРКИ ТОКЕНА --- + const gameId = gameIdInput ? gameIdInput.value.trim() : ''; if (gameId) { ui.disableSetupButtons(); @@ -112,10 +185,18 @@ export function initGameSetup(dependencies) { if (findRandomPvPGameButton) { findRandomPvPGameButton.addEventListener('click', () => { - if (!clientState.isLoggedIn) { - ui.setGameStatusMessage("Пожалуйста, войдите, чтобы найти игру.", true); + // --- ПРОВЕРКА ТОКЕНА ПЕРЕД ДЕЙСТВИЕМ --- + if (typeof checkTokenValidity === 'function' && (!clientState.isLoggedIn || !checkTokenValidity(localStorage.getItem('jwtToken')))) { + if (typeof ui.redirectToLogin === 'function') { + ui.redirectToLogin('Для поиска игры необходимо войти или обновить сессию.'); + } else { + alert('Для поиска игры необходимо войти или обновить сессию.'); + window.location.href = '/'; // Фоллбэк + } return; } + // --- КОНЕЦ ПРОВЕРКИ ТОКЕНА --- + ui.disableSetupButtons(); const characterKey = getSelectedCharacterKey(); socket.emit('findRandomGame', { characterKey: characterKey }); @@ -125,75 +206,51 @@ export function initGameSetup(dependencies) { // --- Обработчики событий Socket.IO --- - // gameCreated: Сервер присылает это после успешного createGame - // Это событие может быть важным для установки currentGameId и myPlayerId - // перед тем, как придет gameStarted или waitingForOpponent. socket.on('gameCreated', (data) => { - if (!clientState.isLoggedIn) return; // Игнорируем, если не залогинены + if (!clientState.isLoggedIn) return; console.log('[GameSetup] Game created by this client:', data); clientState.currentGameId = data.gameId; - clientState.myPlayerId = data.yourPlayerId; // Сервер должен прислать роль создателя - ui.updateGlobalWindowVariablesForUI(); // Обновляем глобальные переменные для ui.js - - // Если это PvP игра, обычно сервер следом пришлет 'waitingForOpponent' - // Если AI, то сразу 'gameStarted' - // На этом этапе UI не меняем кардинально, ждем следующего события. - // ui.setGameStatusMessage(`Игра ${data.gameId.substring(0,8)} создана. Ожидание...`); - // Кнопки уже должны быть заблокированы ui.disableSetupButtons() + clientState.myPlayerId = data.yourPlayerId; + ui.updateGlobalWindowVariablesForUI(); }); socket.on('availablePvPGamesList', (games) => { - if (!clientState.isLoggedIn) return; // Только для залогиненных пользователей + // Проверяем, залогинен ли пользователь, ПЕРЕД обновлением списка. + // Если пользователь разлогинился, а список пришел, его не нужно показывать на экране логина. + if (!clientState.isLoggedIn) { + if (availableGamesDiv) availableGamesDiv.innerHTML = ''; // Очищаем, если пользователь не залогинен + return; + } updateAvailableGamesList(games); }); - // Это событие приходит, когда игрок искал случайную игру, но свободных не было, - // и сервер создал новую игру для этого игрока. socket.on('noPendingGamesFound', (data) => { if (!clientState.isLoggedIn) return; ui.setGameStatusMessage(data.message || "Свободных игр не найдено. Создана новая для вас. Ожидание оппонента..."); - updateAvailableGamesList([]); // Очищаем список доступных игр, так как мы уже в созданной + updateAvailableGamesList([]); - // clientState.currentGameId и clientState.myPlayerId должны были быть установлены - // через событие 'gameCreated', которое сервер должен прислать перед 'noPendingGamesFound'. - // Если 'gameCreated' не присылается в этом сценарии, нужно будет получать gameId и yourPlayerId из data 'noPendingGamesFound'. if (data.gameId) clientState.currentGameId = data.gameId; if (data.yourPlayerId) clientState.myPlayerId = data.yourPlayerId; ui.updateGlobalWindowVariablesForUI(); - clientState.isInGame = false; // Мы еще не в активной фазе боя, а в ожидании - // ui.disableGameControls(); // Будет вызвано из gameplay.js, если он уже был инициализирован - // или неактуально, так как мы не на игровом экране - ui.disableSetupButtons(); // Мы в ожидающей игре, кнопки выбора не нужны - // Можно оставить кнопку "Создать PvP" активной для возможности "отменить" и создать другую, - // но это усложнит логику. Пока блокируем все. - - // Если есть таймер, его нужно сбросить или показать "Ожидание" - if (window.gameUI?.updateTurnTimerDisplay) { - window.gameUI.updateTurnTimerDisplay(null, false, 'pvp'); // Таймер неактивен в ожидании - } - }); - - // waitingForOpponent: Когда PvP игра создана и ожидает второго игрока - socket.on('waitingForOpponent', () => { - if (!clientState.isLoggedIn) return; - - ui.setGameStatusMessage("Ожидание присоединения оппонента..."); - // clientState.isInGame = false; // Уже должно быть false или будет установлено при gameStarted - // ui.disableGameControls(); // не на игровом экране - ui.disableSetupButtons(); // Блокируем кнопки создания/присоединения - - // Можно оставить кнопку "Создать PvP" или добавить кнопку "Отменить ожидание", - // но это требует дополнительной логики на сервере и клиенте. - // if (ui.elements.createPvPGameButton) ui.elements.createPvPGameButton.disabled = false; + clientState.isInGame = false; + ui.disableSetupButtons(); if (window.gameUI?.updateTurnTimerDisplay) { window.gameUI.updateTurnTimerDisplay(null, false, 'pvp'); } }); - // Примечание: gameNotFound обрабатывается в main.js, так как он может сбросить - // игрока на экран выбора игры или даже на экран логина. + socket.on('waitingForOpponent', () => { + if (!clientState.isLoggedIn) return; + + ui.setGameStatusMessage("Ожидание присоединения оппонента..."); + ui.disableSetupButtons(); + + if (window.gameUI?.updateTurnTimerDisplay) { + window.gameUI.updateTurnTimerDisplay(null, false, 'pvp'); + } + }); } \ No newline at end of file diff --git a/public/js/main.js b/public/js/main.js index cba300d..96f6008 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -5,11 +5,18 @@ import { initGameSetup } from './gameSetup.js'; import { initGameplay } from './gameplay.js'; // ui.js загружен глобально и ожидает window.* переменных -function parseJwtPayload(token) { +// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ РАБОТЫ С JWT (для isTokenValid) --- +function parseJwtPayloadForValidation(token) { try { - if (typeof token !== 'string') { return null; } + if (typeof token !== 'string') { + // console.warn("[Main.js parseJwtPayloadForValidation] Token is not a string:", token); + return null; + } const parts = token.split('.'); - if (parts.length !== 3) { return null; } + if (parts.length !== 3) { + // console.warn("[Main.js parseJwtPayloadForValidation] Token does not have 3 parts:", token); + return null; + } const base64Url = parts[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { @@ -17,16 +24,39 @@ function parseJwtPayload(token) { }).join('')); return JSON.parse(jsonPayload); } catch (e) { - console.error("[Main.js parseJwtPayload] Error parsing JWT payload:", e); + console.error("[Main.js parseJwtPayloadForValidation] Error parsing JWT payload:", e, "Token:", token); return null; } } +function isTokenValid(token) { + if (!token) { + // console.log("[Main.js isTokenValid] No token provided."); + return false; + } + const decodedToken = parseJwtPayloadForValidation(token); + if (!decodedToken || typeof decodedToken.exp !== 'number') { + // console.warn("[Main.js isTokenValid] Token invalid or no 'exp' field. Clearing token from storage."); + localStorage.removeItem('jwtToken'); // Удаляем невалидный токен + return false; + } + const currentTimeInSeconds = Math.floor(Date.now() / 1000); + if (decodedToken.exp < currentTimeInSeconds) { + // console.warn("[Main.js isTokenValid] Token expired. Clearing token from storage."); + localStorage.removeItem('jwtToken'); // Удаляем истекший токен + return false; + } + // console.log("[Main.js isTokenValid] Token is valid."); + return true; +} +// --- КОНЕЦ ВСПОМОГАТЕЛЬНЫХ ФУНКЦИЙ ДЛЯ JWT --- + + document.addEventListener('DOMContentLoaded', () => { - console.log('[Main.js] DOMContentLoaded event fired.'); // <--- ДОБАВЛЕНО + console.log('[Main.js] DOMContentLoaded event fired.'); const initialToken = localStorage.getItem('jwtToken'); - console.log('[Main.js] Initial token from localStorage:', initialToken); // <--- ДОБАВЛЕНО + console.log('[Main.js] Initial token from localStorage:', initialToken ? 'Exists' : 'Not found'); let clientState = { isLoggedIn: false, @@ -44,38 +74,37 @@ document.addEventListener('DOMContentLoaded', () => { opponentAbilitiesServer: null, }; - if (initialToken) { - const decodedToken = parseJwtPayload(initialToken); + // Проверяем валидность initialToken перед установкой clientState + if (initialToken && isTokenValid(initialToken)) { // Используем нашу новую функцию + const decodedToken = parseJwtPayloadForValidation(initialToken); // Повторно парсим, т.к. isTokenValid не возвращает payload if (decodedToken && decodedToken.userId && decodedToken.username) { - const nowInSeconds = Math.floor(Date.now() / 1000); - if (decodedToken.exp && decodedToken.exp > nowInSeconds) { - console.log("[Main.js] Token found and valid, pre-populating clientState:", decodedToken); // <--- ДОБАВЛЕНО - clientState.isLoggedIn = true; - clientState.myUserId = decodedToken.userId; - clientState.loggedInUsername = decodedToken.username; - } else { - console.warn("[Main.js] Token expired or invalid 'exp'. Clearing."); - localStorage.removeItem('jwtToken'); - } + console.log("[Main.js] Token found and confirmed valid, pre-populating clientState:", decodedToken); + clientState.isLoggedIn = true; + clientState.myUserId = decodedToken.userId; + clientState.loggedInUsername = decodedToken.username; } else { - console.warn("[Main.js] Token invalid or missing data. Clearing."); + // Этого не должно случиться, если isTokenValid прошла, но на всякий случай + console.warn("[Main.js] Token deemed valid by isTokenValid, but payload incomplete. Clearing."); localStorage.removeItem('jwtToken'); } + } else if (initialToken) { // Токен был, но isTokenValid его отверг (и удалил) + console.warn("[Main.js] Initial token was present but invalid/expired. It has been cleared."); + // clientState остается по умолчанию (isLoggedIn: false) } else { - console.log("[Main.js] No initial token found in localStorage."); // <--- ДОБАВЛЕНО + console.log("[Main.js] No initial token found in localStorage."); } - console.log('[Main.js] Initial clientState after token check:', JSON.parse(JSON.stringify(clientState))); // <--- ДОБАВЛЕНО + console.log('[Main.js] Initial clientState after token check:', JSON.parse(JSON.stringify(clientState))); - console.log('[Main.js] Initializing Socket.IO client...'); // <--- ДОБАВЛЕНО + console.log('[Main.js] Initializing Socket.IO client...'); const socket = io({ autoConnect: false, - auth: { token: localStorage.getItem('jwtToken') } // Передаем токен (может быть null) + auth: { token: localStorage.getItem('jwtToken') } // Передаем токен (может быть null, если был очищен) }); - console.log('[Main.js] Socket.IO client initialized. Socket ID (if connected):', socket.id); // <--- ДОБАВЛЕНО (ID будет при autoConnect:true или после .connect()) + console.log('[Main.js] Socket.IO client initialized.'); // --- DOM Элементы --- - console.log('[Main.js] Getting DOM elements...'); // <--- ДОБАВЛЕНО + console.log('[Main.js] Getting DOM elements...'); const authSection = document.getElementById('auth-section'); const loginForm = document.getElementById('login-form'); const registerForm = document.getElementById('register-form'); @@ -98,17 +127,13 @@ document.addEventListener('DOMContentLoaded', () => { const turnTimerContainer = document.getElementById('turn-timer-container'); const turnTimerSpan = document.getElementById('turn-timer'); - // --- ДОБАВЛЕНО: Логирование найденных DOM элементов --- - console.log('[Main.js DOM Check] authSection:', authSection); - console.log('[Main.js DOM Check] loginForm:', loginForm); - console.log('[Main.js DOM Check] registerForm:', registerForm); - console.log('[Main.js DOM Check] logoutButton:', logoutButton); - // --- КОНЕЦ: Логирование найденных DOM элементов --- - + console.log('[Main.js DOM Check] authSection:', !!authSection); + console.log('[Main.js DOM Check] loginForm:', !!loginForm); + console.log('[Main.js DOM Check] registerForm:', !!registerForm); + console.log('[Main.js DOM Check] logoutButton:', !!logoutButton); // --- Функции обновления UI и состояния --- function updateGlobalWindowVariablesForUI() { - // console.log("[Main.js updateGlobalWindowVariablesForUI] Updating window variables."); // Можно раскомментировать для очень детальной отладки window.gameState = clientState.currentGameState; window.gameData = { playerBaseStats: clientState.playerBaseStatsServer, @@ -135,7 +160,6 @@ document.addEventListener('DOMContentLoaded', () => { } function explicitlyHideGameOverModal() { - // console.log("[Main.js explicitlyHideGameOverModal] Attempting to hide Game Over modal."); // Можно раскомментировать if (window.gameUI?.uiElements?.gameOver?.screen && window.GAME_CONFIG) { const gameOverScreenElement = window.gameUI.uiElements.gameOver.screen; const modalContentElement = window.gameUI.uiElements.gameOver.modalContent; @@ -149,47 +173,42 @@ document.addEventListener('DOMContentLoaded', () => { modalContentElement.style.transform = 'scale(0.8) translateY(30px)'; modalContentElement.style.opacity = '0'; } - // console.log("[Main.js explicitlyHideGameOverModal] Game Over screen explicitly hidden."); - } else if (gameOverScreenElement) { - // console.log("[Main.js explicitlyHideGameOverModal] Game Over screen was already hidden or not found."); } if (messageElement) messageElement.textContent = ''; - } else { - // console.warn("[Main.js explicitlyHideGameOverModal] Cannot hide Game Over modal: gameUI or GAME_CONFIG not available."); } } function showAuthScreen() { console.log("[Main.js showAuthScreen] Showing Auth Screen. Resetting game state if not already done."); - authSection.style.display = 'block'; - userInfoDiv.style.display = 'none'; - gameSetupDiv.style.display = 'none'; - gameWrapper.style.display = 'none'; + if(authSection) authSection.style.display = 'block'; + if(userInfoDiv) userInfoDiv.style.display = 'none'; + if(gameSetupDiv) gameSetupDiv.style.display = 'none'; + if(gameWrapper) gameWrapper.style.display = 'none'; explicitlyHideGameOverModal(); - statusContainer.style.display = 'block'; // Убедимся, что контейнер статуса виден - clientState.isInGame = false; // Сбрасываем флаг игры - resetGameVariables(); // Сбрасываем игровые переменные + if(statusContainer) statusContainer.style.display = 'block'; + clientState.isInGame = false; + resetGameVariables(); if (turnTimerContainer) turnTimerContainer.style.display = 'none'; if (turnTimerSpan) turnTimerSpan.textContent = '--'; if(registerForm && registerForm.querySelector('button')) registerForm.querySelector('button').disabled = false; if(loginForm && loginForm.querySelector('button')) loginForm.querySelector('button').disabled = false; - if(logoutButton) logoutButton.disabled = true; // Кнопка выхода должна быть неактивна на экране логина + if(logoutButton) logoutButton.disabled = true; } function showGameSelectionScreen(username) { console.log(`[Main.js showGameSelectionScreen] Showing Game Selection Screen for ${username}.`); - authSection.style.display = 'none'; - userInfoDiv.style.display = 'block'; + if(authSection) authSection.style.display = 'none'; + if(userInfoDiv) userInfoDiv.style.display = 'block'; if(loggedInUsernameSpan) loggedInUsernameSpan.textContent = username; if(logoutButton) logoutButton.disabled = false; - gameSetupDiv.style.display = 'block'; - gameWrapper.style.display = 'none'; + if(gameSetupDiv) gameSetupDiv.style.display = 'block'; + if(gameWrapper) gameWrapper.style.display = 'none'; explicitlyHideGameOverModal(); setGameStatusMessage("Выберите режим игры или присоединитесь к существующей."); - statusContainer.style.display = 'block'; + if(statusContainer) statusContainer.style.display = 'block'; if (socket.connected) { - console.log("[Main.js showGameSelectionScreen] Socket connected, requesting PvP game list."); // <--- ДОБАВЛЕНО + console.log("[Main.js showGameSelectionScreen] Socket connected, requesting PvP game list."); socket.emit('requestPvPGameList'); } else { console.warn("[Main.js showGameSelectionScreen] Socket not connected, cannot request PvP game list yet."); @@ -200,7 +219,6 @@ document.addEventListener('DOMContentLoaded', () => { const elenaRadio = document.getElementById('char-elena'); if (elenaRadio) elenaRadio.checked = true; clientState.isInGame = false; - // resetGameVariables(); // Обычно не нужно здесь, т.к. мы только вошли, игры еще не было if (turnTimerContainer) turnTimerContainer.style.display = 'none'; if (turnTimerSpan) turnTimerSpan.textContent = '--'; enableSetupButtons(); @@ -211,13 +229,13 @@ document.addEventListener('DOMContentLoaded', () => { function showGameScreen() { console.log("[Main.js showGameScreen] Showing Game Screen."); - authSection.style.display = 'none'; - userInfoDiv.style.display = 'block'; + if(authSection) authSection.style.display = 'none'; + if(userInfoDiv) userInfoDiv.style.display = 'block'; if(logoutButton) logoutButton.disabled = false; - gameSetupDiv.style.display = 'none'; - gameWrapper.style.display = 'flex'; // Используем flex для .game-wrapper - setGameStatusMessage(""); // Очищаем статусное сообщение - statusContainer.style.display = 'none'; // Скрываем контейнер статуса, т.к. игра началась + if(gameSetupDiv) gameSetupDiv.style.display = 'none'; + if(gameWrapper) gameWrapper.style.display = 'flex'; + setGameStatusMessage(""); + if(statusContainer) statusContainer.style.display = 'none'; clientState.isInGame = true; updateGlobalWindowVariablesForUI(); if (turnTimerContainer) turnTimerContainer.style.display = 'block'; @@ -225,7 +243,7 @@ document.addEventListener('DOMContentLoaded', () => { } function setAuthMessage(message, isError = false) { - console.log(`[Main.js setAuthMessage] Message: "${message}", isError: ${isError}`); // <--- ДОБАВЛЕНО + console.log(`[Main.js setAuthMessage] Message: "${message}", isError: ${isError}`); if (authMessage) { authMessage.textContent = message; authMessage.className = isError ? 'error' : 'success'; @@ -235,7 +253,7 @@ document.addEventListener('DOMContentLoaded', () => { } function setGameStatusMessage(message, isError = false) { - console.log(`[Main.js setGameStatusMessage] Message: "${message}", isError: ${isError}`); // <--- ДОБАВЛЕНО + console.log(`[Main.js setGameStatusMessage] Message: "${message}", isError: ${isError}`); if (gameStatusMessage) { gameStatusMessage.textContent = message; gameStatusMessage.style.display = message ? 'block' : 'none'; @@ -245,9 +263,7 @@ document.addEventListener('DOMContentLoaded', () => { if (message && authMessage && authMessage.style.display !== 'none') authMessage.style.display = 'none'; } - function disableSetupButtons() { - // console.log("[Main.js] Disabling setup buttons."); // Можно раскомментировать if(createAIGameButton) createAIGameButton.disabled = true; if(createPvPGameButton) createPvPGameButton.disabled = true; if(joinPvPGameButton) joinPvPGameButton.disabled = true; @@ -255,15 +271,36 @@ document.addEventListener('DOMContentLoaded', () => { if(availableGamesDiv) availableGamesDiv.querySelectorAll('button').forEach(btn => btn.disabled = true); } function enableSetupButtons() { - // console.log("[Main.js] Enabling setup buttons."); // Можно раскомментировать if(createAIGameButton) createAIGameButton.disabled = false; if(createPvPGameButton) createPvPGameButton.disabled = false; if(joinPvPGameButton) joinPvPGameButton.disabled = false; if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = false; + // Кнопки в списке доступных игр управляются в updateAvailableGamesList } + // --- НОВАЯ ФУНКЦИЯ ДЛЯ ПЕРЕНАПРАВЛЕНИЯ НА ЛОГИН --- + function redirectToLogin(message) { + console.log(`[Main.js redirectToLogin] Redirecting to login. Message: "${message}"`); + clientState.isLoggedIn = false; + clientState.loggedInUsername = ''; + clientState.myUserId = null; + clientState.isInGame = false; // Важно сбросить, если он пытался войти в игру + localStorage.removeItem('jwtToken'); + resetGameVariables(); // Сбрасываем все игровые переменные + + if (socket.auth) socket.auth.token = null; + if (socket.connected) { + console.log("[Main.js redirectToLogin] Socket connected, disconnecting before showing auth screen."); + socket.disconnect(); // Отключаем текущий сокет, чтобы он не пытался переподключиться с невалидными данными + } + + showAuthScreen(); + setAuthMessage(message || "Для продолжения необходимо войти или обновить сессию.", true); + } + // --- КОНЕЦ НОВОЙ ФУНКЦИИ --- + // --- Сборка зависимостей для модулей --- - console.log('[Main.js] Preparing dependencies for modules...'); // <--- ДОБАВЛЕНО + console.log('[Main.js] Preparing dependencies for modules...'); const dependencies = { socket, clientState, @@ -277,73 +314,75 @@ document.addEventListener('DOMContentLoaded', () => { updateGlobalWindowVariablesForUI, disableSetupButtons, enableSetupButtons, + redirectToLogin, // <-- ДОБАВЛЕНО elements: { loginForm, registerForm, logoutButton, createAIGameButton, createPvPGameButton, joinPvPGameButton, findRandomPvPGameButton, gameIdInput, availableGamesDiv, pvpCharacterRadios, returnToMenuButton, } + }, + utils: { // <-- ДОБАВЛЕН ОБЪЕКТ UTILS + isTokenValid // <-- ДОБАВЛЕНО } }; - console.log('[Main.js] Initializing auth module...'); // <--- ДОБАВЛЕНО + console.log('[Main.js] Initializing auth module...'); initAuth(dependencies); - console.log('[Main.js] Initializing gameSetup module...'); // <--- ДОБАВЛЕНО + console.log('[Main.js] Initializing gameSetup module...'); initGameSetup(dependencies); - console.log('[Main.js] Initializing gameplay module...'); // <--- ДОБАВЛЕНО + console.log('[Main.js] Initializing gameplay module...'); initGameplay(dependencies); - console.log('[Main.js] All modules initialized.'); // <--- ДОБАВЛЕНО + console.log('[Main.js] All modules initialized.'); // --- Обработчики событий Socket.IO --- socket.on('connect', () => { - const currentToken = socket.auth.token || localStorage.getItem('jwtToken'); // Проверяем токен для лога - console.log('[Main.js Socket.IO] Event: connect. Socket ID:', socket.id, 'Auth token sent to server:', !!currentToken); // <--- ИЗМЕНЕНО + const currentToken = localStorage.getItem('jwtToken'); // Получаем актуальный токен + socket.auth.token = currentToken; // Убедимся, что сокет использует актуальный токен для этого соединения + // (хотя handshake.auth устанавливается при io(), это для ясности) + console.log('[Main.js Socket.IO] Event: connect. Socket ID:', socket.id, 'Auth token associated with this connection attempt:', !!currentToken); - if (clientState.isLoggedIn && clientState.myUserId) { - console.log(`[Main.js Socket.IO] Client state indicates logged in as ${clientState.loggedInUsername} (ID: ${clientState.myUserId}). Requesting game state.`); - // Сообщение о восстановлении сессии лучше показывать, только если мы НЕ в игре + if (clientState.isLoggedIn && clientState.myUserId && isTokenValid(currentToken)) { // Дополнительная проверка токена + console.log(`[Main.js Socket.IO] Client state indicates logged in as ${clientState.loggedInUsername} (ID: ${clientState.myUserId}) and token is valid. Requesting game state.`); if (!clientState.isInGame && (authSection.style.display === 'block' || gameSetupDiv.style.display === 'block')) { setGameStatusMessage("Восстановление игровой сессии..."); } socket.emit('requestGameState'); } else { - console.log('[Main.js Socket.IO] Client state indicates NOT logged in. Showing auth screen if not already visible.'); - if (authSection.style.display !== 'block') { // Показываем, только если еще не там - showAuthScreen(); + // Если clientState говорит, что залогинен, но токен невалиден, или если не залогинен + if (clientState.isLoggedIn && !isTokenValid(currentToken)) { + console.warn('[Main.js Socket.IO connect] Client state says logged in, but token is invalid/expired. Redirecting to login.'); + redirectToLogin("Ваша сессия истекла. Пожалуйста, войдите снова."); + } else { + console.log('[Main.js Socket.IO connect] Client state indicates NOT logged in or no valid token. Showing auth screen if not already visible.'); + if (authSection.style.display !== 'block') { + showAuthScreen(); + } + setAuthMessage("Пожалуйста, войдите или зарегистрируйтесь."); } - setAuthMessage("Пожалуйста, войдите или зарегистрируйтесь."); } }); socket.on('connect_error', (err) => { - console.error('[Main.js Socket.IO] Event: connect_error. Message:', err.message, err.data ? JSON.stringify(err.data) : ''); // <--- ИЗМЕНЕНО + console.error('[Main.js Socket.IO] Event: connect_error. Message:', err.message, err.data ? JSON.stringify(err.data) : ''); const errorMessageLower = err.message ? err.message.toLowerCase() : ""; const isAuthError = errorMessageLower.includes('auth') || errorMessageLower.includes('token') || errorMessageLower.includes('unauthorized') || err.message === 'invalid token' || err.message === 'no token' || (err.data && typeof err.data === 'string' && err.data.toLowerCase().includes('auth')); if (isAuthError) { - console.warn('[Main.js Socket.IO] Authentication error during connection. Clearing token, resetting state, showing auth screen.'); - localStorage.removeItem('jwtToken'); // Убедимся, что токен удален - clientState.isLoggedIn = false; - clientState.loggedInUsername = ''; - clientState.myUserId = null; - if (socket.auth) socket.auth.token = null; // Очищаем токен и в объекте сокета - - if (authSection.style.display !== 'block') { - showAuthScreen(); - } - setAuthMessage("Ошибка аутентификации. Пожалуйста, войдите снова.", true); + console.warn('[Main.js Socket.IO connect_error] Authentication error during connection. Redirecting to login.'); + redirectToLogin("Ошибка аутентификации. Пожалуйста, войдите снова."); } else { let currentScreenMessageFunc = setAuthMessage; if (clientState.isLoggedIn && clientState.isInGame) { currentScreenMessageFunc = setGameStatusMessage; } else if (clientState.isLoggedIn) { - // Если залогинен, но не в игре (на экране выбора игры), сообщение на gameStatusMessage currentScreenMessageFunc = setGameStatusMessage; } currentScreenMessageFunc(`Ошибка подключения: ${err.message}. Попытка переподключения...`, true); if (authSection.style.display !== 'block' && !clientState.isLoggedIn) { + // Если не залогинены и не на экране логина, показать экран логина showAuthScreen(); } } @@ -351,27 +390,41 @@ document.addEventListener('DOMContentLoaded', () => { }); socket.on('disconnect', (reason) => { - console.warn('[Main.js Socket.IO] Event: disconnect. Reason:', reason); // <--- ИЗМЕНЕНО + console.warn('[Main.js Socket.IO] Event: disconnect. Reason:', reason); let messageFunc = setAuthMessage; - if (clientState.isInGame) { + if (clientState.isInGame) { // Если были в игре, сообщение на игровом статусе messageFunc = setGameStatusMessage; - } else if (clientState.isLoggedIn && gameSetupDiv.style.display === 'block') { + } else if (clientState.isLoggedIn && gameSetupDiv.style.display === 'block') { // Если были на экране выбора игры messageFunc = setGameStatusMessage; } - // Не показываем сообщение об ошибке, если это преднамеренный дисконнект при логауте - if (reason !== 'io client disconnect') { + + if (reason === 'io server disconnect') { // Сервер принудительно отключил + messageFunc("Соединение разорвано сервером. Пожалуйста, попробуйте войти снова.", true); + // Можно сразу перенаправить на логин, если это означает проблему с сессией + redirectToLogin("Соединение разорвано сервером. Пожалуйста, войдите снова."); + } else if (reason !== 'io client disconnect') { // Если это не преднамеренный дисконнект клиента (например, при logout) messageFunc(`Потеряно соединение: ${reason}. Попытка переподключения...`, true); } if (turnTimerSpan) turnTimerSpan.textContent = 'Откл.'; + // Не вызываем redirectToLogin здесь автоматически при каждом дисконнекте, + // так как Socket.IO будет пытаться переподключиться. + // redirectToLogin будет вызван из connect_error или connect, если токен окажется невалидным. }); socket.on('gameError', (data) => { - console.error('[Main.js Socket.IO] Event: gameError. Message:', data.message, 'Data:', JSON.stringify(data)); // <--- ИЗМЕНЕНО + console.error('[Main.js Socket.IO] Event: gameError. Message:', data.message, 'Data:', JSON.stringify(data)); + + // Проверка на специфичные ошибки, требующие перелогина + if (data.message && (data.message.toLowerCase().includes("сессия истекла") || data.message.toLowerCase().includes("необходимо войти"))) { + redirectToLogin(data.message); + return; + } + if (clientState.isInGame && window.gameUI?.addToLog) { window.gameUI.addToLog(`❌ Ошибка сервера: ${data.message}`, 'system'); } else if (clientState.isLoggedIn) { setGameStatusMessage(`❌ Ошибка: ${data.message}`, true); - enableSetupButtons(); + enableSetupButtons(); // Разблокируем кнопки, если произошла ошибка на экране выбора игры } else { setAuthMessage(`❌ Ошибка: ${data.message}`, true); if(registerForm && registerForm.querySelector('button')) registerForm.querySelector('button').disabled = false; @@ -380,50 +433,41 @@ document.addEventListener('DOMContentLoaded', () => { }); socket.on('gameNotFound', (data) => { - console.log('[Main.js Socket.IO] Event: gameNotFound. Message:', data?.message, 'Data:', JSON.stringify(data)); // <--- ИЗМЕНЕНО + console.log('[Main.js Socket.IO] Event: gameNotFound. Message:', data?.message, 'Data:', JSON.stringify(data)); clientState.isInGame = false; resetGameVariables(); explicitlyHideGameOverModal(); if (turnTimerContainer) turnTimerContainer.style.display = 'none'; if (turnTimerSpan) turnTimerSpan.textContent = '--'; - if (clientState.isLoggedIn && clientState.myUserId) { - if (gameSetupDiv.style.display !== 'block') { // Показываем, только если еще не там + if (clientState.isLoggedIn && isTokenValid(localStorage.getItem('jwtToken'))) { // Проверяем, что токен еще валиден + if (gameSetupDiv.style.display !== 'block') { showGameSelectionScreen(clientState.loggedInUsername); } setGameStatusMessage(data?.message || "Активная игровая сессия не найдена. Выберите новую игру."); - } else { - if (authSection.style.display !== 'block') { - showAuthScreen(); - } - setAuthMessage(data?.message || "Пожалуйста, войдите."); + } else { // Если не залогинен или токен истек + redirectToLogin(data?.message || "Пожалуйста, войдите для продолжения."); } }); // --- Инициализация UI --- - console.log('[Main.js] Initializing UI visibility...'); // <--- ДОБАВЛЕНО - authSection.style.display = 'none'; - gameSetupDiv.style.display = 'none'; - gameWrapper.style.display = 'none'; - userInfoDiv.style.display = 'none'; - statusContainer.style.display = 'block'; // Показываем контейнер статуса по умолчанию + console.log('[Main.js] Initializing UI visibility...'); + if(authSection) authSection.style.display = 'none'; + if(gameSetupDiv) gameSetupDiv.style.display = 'none'; + if(gameWrapper) gameWrapper.style.display = 'none'; + if(userInfoDiv) userInfoDiv.style.display = 'none'; + if(statusContainer) statusContainer.style.display = 'block'; - if (clientState.isLoggedIn) { - // Если токен есть и валиден, НЕ показываем экран логина, а пытаемся восстановить сессию - // Это будет обработано в socket.on('connect') -> requestGameState - // Если requestGameState вернет gameNotFound, тогда покажется gameSelectionScreen - // Если requestGameState вернет игру, покажется gameScreen - // Сообщение "Подключение и восстановление сессии..." может быть показано здесь + if (clientState.isLoggedIn) { // isLoggedIn уже учитывает валидность токена при начальной загрузке console.log('[Main.js] Client is considered logged in. Will attempt session recovery on socket connect.'); - setGameStatusMessage("Подключение и восстановление сессии..."); // Показываем на общем статусном элементе + setGameStatusMessage("Подключение и восстановление сессии..."); } else { - // Если нет валидного токена, показываем экран аутентификации console.log('[Main.js] Client is NOT considered logged in. Showing auth screen.'); - showAuthScreen(); // Это установит правильные display для authSection и скроет другие - setAuthMessage("Подключение к серверу..."); // Начальное сообщение на экране логина + showAuthScreen(); + setAuthMessage("Подключение к серверу..."); } - console.log('[Main.js] Attempting to connect socket...'); // <--- ДОБАВЛЕНО - socket.connect(); // Начинаем подключение к серверу - console.log('[Main.js] socket.connect() called.'); // <--- ДОБАВЛЕНО + console.log('[Main.js] Attempting to connect socket...'); + socket.connect(); + console.log('[Main.js] socket.connect() called.'); }); \ No newline at end of file