// /public/js/main.js import { initAuth } from './auth.js'; import { initGameSetup } from './gameSetup.js'; import { initGameplay } from './gameplay.js'; // ui.js загружен глобально function parseJwtPayload(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("[parseJwtPayload] Error parsing JWT payload:", e); return null; } } document.addEventListener('DOMContentLoaded', () => { const SERVER_URL = 'https://81.177.140.16:3200' //'http://127.0.0.1:3200'; const API_BASE_URL = SERVER_URL; const initialToken = localStorage.getItem('jwtToken'); let clientState = { isLoggedIn: false, loggedInUsername: '', myUserId: null, isInGame: false, currentGameId: null, currentGameState: null, // Будет объектом или null myPlayerId: null, myCharacterKey: null, opponentCharacterKey: null, playerBaseStatsServer: null, opponentBaseStatsServer: null, playerAbilitiesServer: null, opponentAbilitiesServer: null, }; if (initialToken) { const decodedToken = parseJwtPayload(initialToken); if (decodedToken && decodedToken.userId && decodedToken.username) { const nowInSeconds = Math.floor(Date.now() / 1000); if (decodedToken.exp && decodedToken.exp > nowInSeconds) { console.log("[Client Init] Token found, pre-populating clientState."); clientState.isLoggedIn = true; clientState.myUserId = decodedToken.userId; clientState.loggedInUsername = decodedToken.username; } else { console.warn("[Client Init] Token expired or invalid 'exp'. Clearing."); localStorage.removeItem('jwtToken'); } } else { console.warn("[Client Init] Token invalid or missing data. Clearing."); localStorage.removeItem('jwtToken'); } } const socket = io(SERVER_URL, { autoConnect: false, auth: { token: localStorage.getItem('jwtToken') } }); 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'); // Он же в ui.elements.gameOver.returnToMenuButton const turnTimerContainer = document.getElementById('turn-timer-container'); const turnTimerSpan = document.getElementById('turn-timer'); function updateGlobalWindowVariablesForUI() { // console.log("[Main] Updating global window variables. currentGameState:", clientState.currentGameState ? JSON.parse(JSON.stringify(clientState.currentGameState)) : null); window.gameState = clientState.currentGameState; // Может быть null window.gameData = { playerBaseStats: clientState.playerBaseStatsServer, opponentBaseStats: clientState.opponentBaseStatsServer, playerAbilities: clientState.playerAbilitiesServer, opponentAbilities: clientState.opponentAbilitiesServer }; window.myPlayerId = clientState.myPlayerId; // window.GAME_CONFIG устанавливается при gameStarted/gameState из gameplay.js } function resetGameVariables() { console.log("[Main:resetGameVariables] Resetting game variables. State BEFORE:", JSON.parse(JSON.stringify(clientState))); clientState.currentGameId = null; // ВАЖНО: currentGameState должен быть сброшен в состояние "нет игры" // Либо null, либо объект, который ui.js интерпретирует как "нет игры" clientState.currentGameState = null; // Можно также так, если ui.js лучше работает с объектом: // clientState.currentGameState = { isGameOver: false, player: null, opponent: null, turnNumber: 0 }; clientState.myPlayerId = null; clientState.myCharacterKey = null; clientState.opponentCharacterKey = null; clientState.playerBaseStatsServer = null; clientState.opponentBaseStatsServer = null; clientState.playerAbilitiesServer = null; clientState.opponentAbilitiesServer = null; // clientState.isInGame будет установлено в вызывающей функции (showAuthScreen/showGameSelectionScreen) updateGlobalWindowVariablesForUI(); // Обновляем глобальные переменные СРАЗУ после сброса console.log("[Main:resetGameVariables] Game variables reset. State AFTER:", JSON.parse(JSON.stringify(clientState))); } function explicitlyHideGameOverModal() { console.log("[Main: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; 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'; } console.log("[Main:explicitlyHideGameOverModal] Game Over screen explicitly hidden."); } else if (gameOverScreenElement) { console.log("[Main:explicitlyHideGameOverModal] Game Over screen was already hidden or not found."); } if (messageElement) messageElement.textContent = ''; // Очищаем сообщение } else { console.warn("[Main:explicitlyHideGameOverModal] Cannot hide Game Over modal: gameUI or GAME_CONFIG not available."); } } function showAuthScreen() { console.log("[Main:showAuthScreen] Showing Auth Screen. Resetting game state."); authSection.style.display = 'block'; userInfoDiv.style.display = 'none'; gameSetupDiv.style.display = 'none'; gameWrapper.style.display = 'none'; explicitlyHideGameOverModal(); // <-- ЯВНО СКРЫВАЕМ МОДАЛКУ statusContainer.style.display = 'block'; clientState.isInGame = false; // Важно resetGameVariables(); // Сбрасываем все переменные предыдущей игры if (turnTimerContainer) turnTimerContainer.style.display = 'none'; if (turnTimerSpan) turnTimerSpan.textContent = '--'; if(registerForm) registerForm.querySelector('button').disabled = false; if(loginForm) loginForm.querySelector('button').disabled = false; if(logoutButton) logoutButton.disabled = true; // Кнопка Logout должна быть недоступна на экране логина } function showGameSelectionScreen(username) { console.log(`[Main:showGameSelectionScreen] Showing Game Selection Screen for ${username}. Resetting game state.`); authSection.style.display = 'none'; userInfoDiv.style.display = 'block'; if(loggedInUsernameSpan) loggedInUsernameSpan.textContent = username; if(logoutButton) logoutButton.disabled = false; // Logout доступен gameSetupDiv.style.display = 'block'; gameWrapper.style.display = 'none'; explicitlyHideGameOverModal(); // <-- ЯВНО СКРЫВАЕМ МОДАЛКУ setGameStatusMessage("Выберите режим игры или присоединитесь к существующей."); statusContainer.style.display = 'block'; if (socket.connected) { socket.emit('requestPvPGameList'); } else { console.warn("[Main:showGameSelectionScreen] Socket not connected, cannot request PvP game list yet."); } if (availableGamesDiv) availableGamesDiv.innerHTML = '
Загрузка...
'; if (gameIdInput) gameIdInput.value = ''; 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(); // Убедимся, что кнопка "Вернуться в меню" на gameOver модалке (если она вдруг видима) активна, // хотя сама модалка должна быть скрыта. if (window.gameUI?.uiElements?.gameOver?.returnToMenuButton) { window.gameUI.uiElements.gameOver.returnToMenuButton.disabled = false; } } function showGameScreen() { console.log("[Main:showGameScreen] Showing Game Screen."); // Не нужно здесь вызывать explicitlyHideGameOverModal, так как если игра начинается, // а модалка была видима, это ошибка логики где-то еще. // GameStarted/GameState должно само приводить UI в порядок. authSection.style.display = 'none'; userInfoDiv.style.display = 'block'; if(logoutButton) logoutButton.disabled = false; gameSetupDiv.style.display = 'none'; gameWrapper.style.display = 'flex'; setGameStatusMessage(""); statusContainer.style.display = 'none'; clientState.isInGame = true; // Важно updateGlobalWindowVariablesForUI(); // Обновляем перед тем, как UI начнет рендерить игровой экран if (turnTimerContainer) turnTimerContainer.style.display = 'block'; if (turnTimerSpan) turnTimerSpan.textContent = '--'; } function setAuthMessage(message, isError = false) { if (authMessage) { authMessage.textContent = message; authMessage.className = isError ? 'error' : 'success'; authMessage.style.display = message ? 'block' : 'none'; } if (message && gameStatusMessage && gameStatusMessage.style.display !== 'none') gameStatusMessage.style.display = 'none'; } function setGameStatusMessage(message, isError = false) { 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'; } 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; // Кнопки в списке доступных игр управляются в gameSetup.js -> updateAvailableGamesList } const dependencies = { socket, clientState, ui: { showAuthScreen, showGameSelectionScreen, showGameScreen, setAuthMessage, setGameStatusMessage, resetGameVariables, // Передаем, чтобы другие модули могли вызвать при необходимости (хотя лучше избегать) updateGlobalWindowVariablesForUI, disableSetupButtons, enableSetupButtons, elements: { loginForm, registerForm, logoutButton, createAIGameButton, createPvPGameButton, joinPvPGameButton, findRandomPvPGameButton, gameIdInput, availableGamesDiv, pvpCharacterRadios, returnToMenuButton, // returnToMenuButton из gameplay.js, но здесь тоже может быть полезен } }, API_BASE_URL: API_BASE_URL }; initAuth(dependencies); initGameSetup(dependencies); initGameplay(dependencies); socket.on('connect', () => { const currentToken = socket.auth.token || localStorage.getItem('jwtToken'); console.log('[Main:SocketConnect] Socket connected:', socket.id, 'Auth token sent:', !!currentToken); if (clientState.isLoggedIn && clientState.myUserId) { console.log(`[Main:SocketConnect] Client state indicates logged in as ${clientState.loggedInUsername}. Requesting game state.`); if (authSection.style.display === 'block' || gameSetupDiv.style.display === 'block') { // Если мы на экране логина или выбора игры, но считаем себя залогиненными, // покажем сообщение о восстановлении. setGameStatusMessage("Восстановление игровой сессии..."); } // Не очищаем здесь resetGameVariables, так как gameplay.js ожидает, что clientState может содержать // предыдущие данные, которые он перезапишет при получении gameState или gameStarted. // Если придет gameNotFound, то там уже будет reset. socket.emit('requestGameState'); } else { console.log('[Main:SocketConnect] Client state indicates NOT logged in. Showing auth screen.'); showAuthScreen(); // Убеждаемся, что все сброшено и показан экран логина setAuthMessage("Пожалуйста, войдите или зарегистрируйтесь."); } }); socket.on('connect_error', (err) => { console.error('[Main:SocketConnectError] Socket connection error:', err.message, err.data ? 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:SocketConnectError] Authentication error. 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; showAuthScreen(); // Это вызовет resetGameVariables и скроет модалку setAuthMessage("Ошибка аутентификации. Пожалуйста, войдите снова.", true); } else { if (clientState.isLoggedIn && clientState.isInGame) { setGameStatusMessage(`Ошибка подключения: ${err.message}. Попытка переподключения...`, true); } else if (clientState.isLoggedIn) { setGameStatusMessage(`Ошибка подключения к серверу: ${err.message}. Попытка переподключения...`, true); } else { setAuthMessage(`Ошибка подключения к серверу: ${err.message}. Попытка переподключения...`, true); if (authSection.style.display !== 'block') { showAuthScreen(); // Если не на экране логина, но ошибка не auth, все равно показываем его } } } if (turnTimerSpan) turnTimerSpan.textContent = 'Ошибка'; }); socket.on('disconnect', (reason) => { console.warn('[Main:SocketDisconnect] Disconnected from server:', reason); // Сообщения в зависимости от текущего состояния if (clientState.isInGame) { setGameStatusMessage(`Потеряно соединение: ${reason}. Попытка переподключения...`, true); } else if (clientState.isLoggedIn) { // Уже должен быть на экране выбора игры или восстановления, setGameStatusMessage там уместно if (gameSetupDiv.style.display === 'block') { setGameStatusMessage(`Потеряно соединение с сервером: ${reason}. Попытка переподключения...`, true); } else { // Если где-то между экранами, но залогинен setAuthMessage(`Потеряно соединение: ${reason}. Попытка переподключения...`, true); // Используем authMessage для общего случая } } else { setAuthMessage(`Потеряно соединение с сервером: ${reason}. Попытка переподключения...`, true); } if (turnTimerSpan) turnTimerSpan.textContent = 'Откл.'; // Не сбрасываем clientState.isLoggedIn здесь, чтобы socket.connect мог попытаться восстановить сессию }); socket.on('gameError', (data) => { console.error('[Main:SocketGameError] Received gameError from server:', data.message); if (clientState.isInGame && window.gameUI?.addToLog) { window.gameUI.addToLog(`❌ Ошибка сервера: ${data.message}`, 'system'); // Можно добавить setGameStatusMessage и здесь, если ошибка критическая для игры } else if (clientState.isLoggedIn) { // На экране выбора игры setGameStatusMessage(`❌ Ошибка: ${data.message}`, true); enableSetupButtons(); // Разблокировать кнопки, если ошибка при создании/присоединении } else { // На экране логина setAuthMessage(`❌ Ошибка: ${data.message}`, true); if(registerForm) registerForm.querySelector('button').disabled = false; if(loginForm) loginForm.querySelector('button').disabled = false; } }); socket.on('gameNotFound', (data) => { console.log('[Main:SocketGameNotFound] Game not found/ended after request:', data?.message); // Важно: gameNotFound означает, что активной игры нет. // Сбрасываем состояние и показываем экран выбора игры, если залогинены. clientState.isInGame = false; // Явно выходим из игры resetGameVariables(); // Полный сброс игровых переменных explicitlyHideGameOverModal(); // Убеждаемся, что модалка скрыта if (turnTimerContainer) turnTimerContainer.style.display = 'none'; if (turnTimerSpan) turnTimerSpan.textContent = '--'; if (clientState.isLoggedIn && clientState.myUserId) { showGameSelectionScreen(clientState.loggedInUsername); // Переходим на выбор игры (он вызовет resetGameVariables еще раз, но это не страшно) setGameStatusMessage(data?.message || "Активная игровая сессия не найдена. Выберите новую игру."); } else { // Если по какой-то причине мы не залогинены (например, токен истек и connect_error сбросил isLoggedIn) showAuthScreen(); setAuthMessage(data?.message || "Пожалуйста, войдите."); } }); // Инициализация UI authSection.style.display = 'none'; gameSetupDiv.style.display = 'none'; gameWrapper.style.display = 'none'; userInfoDiv.style.display = 'none'; statusContainer.style.display = 'block'; if (clientState.isLoggedIn) { setGameStatusMessage("Подключение и восстановление сессии..."); // Или setAuthMessage, если statusContainer не виден сразу } else { setAuthMessage("Подключение к серверу..."); } socket.connect(); });