// /public/js/main.js import { initAuth } from './auth.js'; import { initGameSetup } from './gameSetup.js'; import { initGameplay } from './gameplay.js'; // ui.js загружен глобально и ожидает window.* переменных // --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ РАБОТЫ С JWT (для isTokenValid) --- function parseJwtPayloadForValidation(token) { try { if (typeof token !== 'string') { return null; } const parts = token.split('.'); if (parts.length !== 3) { return null; } const base64Url = parts[1]; 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) { console.error("[Main.js parseJwtPayloadForValidation] Error parsing JWT payload:", e, "Token:", token); return null; } } function isTokenValid(token) { if (!token) { return false; } const decodedToken = parseJwtPayloadForValidation(token); if (!decodedToken || typeof decodedToken.exp !== 'number') { localStorage.removeItem('jwtToken'); return false; } const currentTimeInSeconds = Math.floor(Date.now() / 1000); if (decodedToken.exp < currentTimeInSeconds) { localStorage.removeItem('jwtToken'); return false; } return true; } // --- КОНЕЦ ВСПОМОГАТЕЛЬНЫХ ФУНКЦИЙ ДЛЯ JWT --- document.addEventListener('DOMContentLoaded', () => { console.log('[Main.js] DOMContentLoaded event fired.'); const initialToken = localStorage.getItem('jwtToken'); console.log('[Main.js] Initial token from localStorage:', initialToken ? 'Exists' : 'Not found'); let clientState = { isLoggedIn: false, loggedInUsername: '', myUserId: null, isInGame: false, currentGameId: null, currentGameState: null, myPlayerId: null, // Роль в текущей игре (player/opponent) myCharacterKey: null, opponentCharacterKey: null, playerBaseStatsServer: null, opponentBaseStatsServer: null, playerAbilitiesServer: null, opponentAbilitiesServer: null, isActionInProgress: false, // <--- ВАЖНО: Флаг для предотвращения двойных действий }; if (initialToken && isTokenValid(initialToken)) { const decodedToken = parseJwtPayloadForValidation(initialToken); if (decodedToken && decodedToken.userId && decodedToken.username) { console.log("[Main.js] Token found and confirmed valid, pre-populating clientState:", decodedToken); clientState.isLoggedIn = true; clientState.myUserId = decodedToken.userId; // Это ID пользователя из БД clientState.loggedInUsername = decodedToken.username; } else { console.warn("[Main.js] Token deemed valid by isTokenValid, but payload incomplete. Clearing."); localStorage.removeItem('jwtToken'); } } else if (initialToken) { console.warn("[Main.js] Initial token was present but invalid/expired. It has been cleared."); } else { 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] Initializing Socket.IO client...'); const socket = io({ path:base_path + "/socket.io", // base_path определяется в HTML autoConnect: false, // Подключаемся вручную после инициализации всего auth: { token: localStorage.getItem('jwtToken') } }); console.log('[Main.js] Socket.IO client initialized.'); // --- DOM Элементы --- 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'); 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'); 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 findRandomPvPGameButton = document.getElementById('find-random-pvp-game'); const gameIdInput = document.getElementById('game-id-input'); const availableGamesDiv = document.getElementById('available-games-list'); const gameStatusMessage = document.getElementById('game-status-message'); // Сообщение на экране выбора игры const pvpCharacterRadios = document.querySelectorAll('input[name="pvp-character"]'); const gameWrapper = document.querySelector('.game-wrapper'); const returnToMenuButton = document.getElementById('return-to-menu-button'); // Кнопка в gameOver модальном окне const turnTimerContainer = document.getElementById('turn-timer-container'); const turnTimerSpan = document.getElementById('turn-timer'); // --- Функции обновления UI и состояния --- function updateGlobalWindowVariablesForUI() { window.gameState = clientState.currentGameState; window.gameData = { playerBaseStats: clientState.playerBaseStatsServer, opponentBaseStats: clientState.opponentBaseStatsServer, playerAbilities: clientState.playerAbilitiesServer, opponentAbilities: clientState.opponentAbilitiesServer }; window.myPlayerId = clientState.myPlayerId; // Роль игрока (player/opponent) } function resetGameVariables() { console.log("[Main.js resetGameVariables] Resetting game variables. State BEFORE:", JSON.parse(JSON.stringify(clientState))); clientState.currentGameId = null; clientState.currentGameState = null; clientState.myPlayerId = null; clientState.myCharacterKey = null; clientState.opponentCharacterKey = null; clientState.playerBaseStatsServer = null; clientState.opponentBaseStatsServer = null; clientState.playerAbilitiesServer = null; clientState.opponentAbilitiesServer = null; clientState.isActionInProgress = false; // <--- Сброс флага updateGlobalWindowVariablesForUI(); console.log("[Main.js resetGameVariables] Game variables reset. State AFTER:", JSON.parse(JSON.stringify(clientState))); } function explicitlyHideGameOverModal() { if (window.gameUI?.uiElements?.gameOver?.screen && window.GAME_CONFIG) { const gameOverScreenElement = window.gameUI.uiElements.gameOver.screen; const modalContentElement = window.gameUI.uiElements.gameOver.modalContent; const messageElement = window.gameUI.uiElements.gameOver.message; const hiddenClass = window.GAME_CONFIG.CSS_CLASS_HIDDEN || 'hidden'; if (gameOverScreenElement && !gameOverScreenElement.classList.contains(hiddenClass)) { gameOverScreenElement.classList.add(hiddenClass); gameOverScreenElement.style.opacity = '0'; if (modalContentElement) { modalContentElement.style.transform = 'scale(0.8) translateY(30px)'; modalContentElement.style.opacity = '0'; } } if (messageElement) messageElement.textContent = ''; } } function showAuthScreen() { console.log("[Main.js showAuthScreen] Showing Auth Screen. Resetting game state."); 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(); if(statusContainer) statusContainer.style.display = 'block'; // Показываем общий контейнер для сообщений clientState.isInGame = false; resetGameVariables(); // Включает сброс isActionInProgress 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; // Кнопка выхода неактивна на экране логина } function showGameSelectionScreen(username) { console.log(`[Main.js showGameSelectionScreen] Showing Game Selection Screen for ${username}.`); if(authSection) authSection.style.display = 'none'; if(userInfoDiv) userInfoDiv.style.display = 'block'; if(loggedInUsernameSpan) loggedInUsernameSpan.textContent = username; if(logoutButton) logoutButton.disabled = false; if(gameSetupDiv) gameSetupDiv.style.display = 'block'; if(gameWrapper) gameWrapper.style.display = 'none'; explicitlyHideGameOverModal(); setGameStatusMessage("Выберите режим игры или присоединитесь к существующей."); if(statusContainer) statusContainer.style.display = 'block'; if (socket.connected) { 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."); // Можно попробовать подключить сокет, если он не подключен // socket.connect(); // Или дождаться авто-реконнекта } if (availableGamesDiv) availableGamesDiv.innerHTML = '
Загрузка...
'; if (gameIdInput) gameIdInput.value = ''; const elenaRadio = document.getElementById('char-elena'); if (elenaRadio) elenaRadio.checked = true; // Персонаж по умолчанию clientState.isInGame = false; clientState.isActionInProgress = false; // <--- Сброс флага при переходе в меню if (turnTimerContainer) turnTimerContainer.style.display = 'none'; if (turnTimerSpan) turnTimerSpan.textContent = '--'; enableSetupButtons(); // Включаем кнопки настройки игры if (window.gameUI?.uiElements?.gameOver?.returnToMenuButton) { window.gameUI.uiElements.gameOver.returnToMenuButton.disabled = false; // Убедимся, что кнопка в модалке gameOver активна } } function showGameScreen() { console.log("[Main.js showGameScreen] Showing Game Screen."); if(authSection) authSection.style.display = 'none'; if(userInfoDiv) userInfoDiv.style.display = 'block'; // userInfo (имя, выход) остается видимым if(logoutButton) logoutButton.disabled = false; if(gameSetupDiv) gameSetupDiv.style.display = 'none'; if(gameWrapper) gameWrapper.style.display = 'flex'; // Используем flex для game-wrapper setGameStatusMessage(""); // Очищаем сообщение статуса игры при входе на экран игры if(statusContainer) statusContainer.style.display = 'none'; // Скрываем общий статус-контейнер на игровом экране clientState.isInGame = true; // clientState.isActionInProgress остается false до первого действия игрока updateGlobalWindowVariablesForUI(); if (turnTimerContainer) turnTimerContainer.style.display = 'block'; if (turnTimerSpan) turnTimerSpan.textContent = '--'; // Таймер обновится по событию } function setAuthMessage(message, isError = false) { console.log(`[Main.js setAuthMessage] Message: "${message}", isError: ${isError}`); if (authMessage) { authMessage.textContent = message; authMessage.className = isError ? 'error' : 'success'; authMessage.style.display = message ? 'block' : 'none'; } // Если показываем authMessage, скрываем gameStatusMessage if (message && gameStatusMessage && gameStatusMessage.style.display !== 'none') { gameStatusMessage.style.display = 'none'; } } function setGameStatusMessage(message, isError = false) { console.log(`[Main.js setGameStatusMessage] Message: "${message}", isError: ${isError}`); if (gameStatusMessage) { 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'; } // Если показываем gameStatusMessage, скрываем authMessage if (message && authMessage && authMessage.style.display !== 'none') { authMessage.style.display = 'none'; } } 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; // Кнопки в списке доступных игр управляются в updateAvailableGamesList в gameSetup.js } 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(); // Сбрасываем все игровые переменные, включая isActionInProgress if (socket.auth) socket.auth.token = null; // Обновляем auth объект сокета 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...'); const dependencies = { socket, clientState, ui: { showAuthScreen, showGameSelectionScreen, showGameScreen, setAuthMessage, setGameStatusMessage, resetGameVariables, updateGlobalWindowVariablesForUI, disableSetupButtons, enableSetupButtons, redirectToLogin, elements: { loginForm, registerForm, logoutButton, createAIGameButton, createPvPGameButton, joinPvPGameButton, findRandomPvPGameButton, gameIdInput, availableGamesDiv, pvpCharacterRadios, returnToMenuButton, // Не передаем сюда все элементы из ui.js, так как ui.js сам их менеджит. // Если какой-то модуль должен напрямую менять что-то из ui.js.uiElements, // то можно передать ui.js.uiElements целиком или конкретные элементы. } }, utils: { isTokenValid, parseJwtPayloadForValidation // На всякий случай, если понадобится где-то еще } }; console.log('[Main.js] Initializing auth module...'); initAuth(dependencies); console.log('[Main.js] Initializing gameSetup module...'); initGameSetup(dependencies); console.log('[Main.js] Initializing gameplay module...'); initGameplay(dependencies); console.log('[Main.js] All modules initialized.'); // --- Обработчики событий Socket.IO --- socket.on('connect', () => { const currentToken = localStorage.getItem('jwtToken'); if (socket.auth) socket.auth.token = currentToken; // Убедимся, что auth объект сокета обновлен else socket.auth = { token: currentToken }; // Если auth объекта не было 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 && 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 && (gameSetupDiv.style.display === 'block' || authSection.style.display === 'block')) { setGameStatusMessage("Восстановление игровой сессии..."); } socket.emit('requestGameState'); } else { 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("Пожалуйста, войдите или зарегистрируйтесь."); // Сообщение по умолчанию для экрана логина } } }); socket.on('connect_error', (err) => { 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 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) { // Если залогинен, но не в игре (на экране выбора) currentScreenMessageFunc = setGameStatusMessage; } currentScreenMessageFunc(`Ошибка подключения: ${err.message}. Попытка переподключения...`, true); // Если не залогинены и не на экране авторизации, показываем его if (!clientState.isLoggedIn && authSection.style.display !== 'block') { showAuthScreen(); } } if (turnTimerSpan) turnTimerSpan.textContent = 'Ошибка'; }); socket.on('disconnect', (reason) => { console.warn('[Main.js Socket.IO] Event: disconnect. Reason:', reason); let messageFunc = setAuthMessage; // По умолчанию сообщение для экрана авторизации if (clientState.isInGame) { messageFunc = setGameStatusMessage; } else if (clientState.isLoggedIn && gameSetupDiv.style.display === 'block') { messageFunc = setGameStatusMessage; } if (reason === 'io server disconnect') { messageFunc("Соединение разорвано сервером. Пожалуйста, попробуйте войти снова.", true); redirectToLogin("Соединение разорвано сервером. Пожалуйста, войдите снова."); } else if (reason === 'io client disconnect') { // Это преднамеренный дисконнект (например, при logout или смене токена). // Сообщение уже должно быть установлено функцией, вызвавшей дисконнект. // Ничего не делаем здесь, чтобы не перезаписать его. console.log('[Main.js Socket.IO] Disconnect was intentional (io client disconnect). No additional message needed.'); } else { // Другие причины (например, проблемы с сетью) messageFunc(`Потеряно соединение: ${reason}. Попытка переподключения...`, true); } if (turnTimerSpan) turnTimerSpan.textContent = 'Откл.'; clientState.isActionInProgress = false; // На всякий случай сбрасываем флаг при дисконнекте }); socket.on('gameError', (data) => { console.error('[Main.js Socket.IO] Event: gameError. Message:', data.message, 'Data:', JSON.stringify(data)); clientState.isActionInProgress = false; // Сбрасываем флаг при ошибке сервера 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(); // Разблокируем кнопки, если ошибка на экране выбора игры } else { setAuthMessage(`❌ Ошибка: ${data.message}`, true); if(registerForm && registerForm.querySelector('button')) registerForm.querySelector('button').disabled = false; if(loginForm && loginForm.querySelector('button')) loginForm.querySelector('button').disabled = false; } }); socket.on('gameNotFound', (data) => { console.log('[Main.js Socket.IO] Event: gameNotFound. Message:', data?.message, 'Data:', JSON.stringify(data)); clientState.isInGame = false; resetGameVariables(); // Включает сброс isActionInProgress explicitlyHideGameOverModal(); if (turnTimerContainer) turnTimerContainer.style.display = 'none'; if (turnTimerSpan) turnTimerSpan.textContent = '--'; if (clientState.isLoggedIn && isTokenValid(localStorage.getItem('jwtToken'))) { if (gameSetupDiv.style.display !== 'block') { // Если мы не на экране выбора игры, показываем его showGameSelectionScreen(clientState.loggedInUsername); } setGameStatusMessage(data?.message || "Активная игровая сессия не найдена. Выберите новую игру."); } else { redirectToLogin(data?.message || "Пожалуйста, войдите для продолжения."); } }); // --- Инициализация UI --- 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) { console.log('[Main.js] Client is considered logged in. Will attempt session recovery on socket connect.'); // Не показываем экран выбора игры сразу, дожидаемся 'connect' и 'requestGameState' setAuthMessage("Подключение и восстановление сессии..."); // Используем authMessage для начального сообщения } else { console.log('[Main.js] Client is NOT considered logged in. Showing auth screen.'); showAuthScreen(); setAuthMessage("Подключение к серверу..."); } console.log('[Main.js] Attempting to connect socket...'); socket.connect(); // Подключаемся здесь, после всей инициализации console.log('[Main.js] socket.connect() called.'); });