bc/public/js/main.js

474 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /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') {
// console.warn("[Main.js parseJwtPayloadForValidation] Token is not a string:", token);
return null;
}
const parts = token.split('.');
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) {
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) {
// 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.');
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,
myCharacterKey: null,
opponentCharacterKey: null,
playerBaseStatsServer: null,
opponentBaseStatsServer: null,
playerAbilitiesServer: null,
opponentAbilitiesServer: null,
};
// Проверяем валидность initialToken перед установкой clientState
if (initialToken && isTokenValid(initialToken)) { // Используем нашу новую функцию
const decodedToken = parseJwtPayloadForValidation(initialToken); // Повторно парсим, т.к. isTokenValid не возвращает payload
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;
clientState.loggedInUsername = decodedToken.username;
} else {
// Этого не должно случиться, если 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] Initial clientState after token check:', JSON.parse(JSON.stringify(clientState)));
console.log('[Main.js] Initializing Socket.IO client...');
const socket = io({
path:base_path,
autoConnect: false,
auth: { token: localStorage.getItem('jwtToken') } // Передаем токен (может быть null, если был очищен)
});
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');
const turnTimerContainer = document.getElementById('turn-timer-container');
const turnTimerSpan = document.getElementById('turn-timer');
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() {
window.gameState = clientState.currentGameState;
window.gameData = {
playerBaseStats: clientState.playerBaseStatsServer,
opponentBaseStats: clientState.opponentBaseStatsServer,
playerAbilities: clientState.playerAbilitiesServer,
opponentAbilities: clientState.opponentAbilitiesServer
};
window.myPlayerId = clientState.myPlayerId;
}
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;
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 not already done.");
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();
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.");
}
if (availableGamesDiv) availableGamesDiv.innerHTML = '<h3>Доступные PvP игры:</h3><p>Загрузка...</p>';
if (gameIdInput) gameIdInput.value = '';
const elenaRadio = document.getElementById('char-elena');
if (elenaRadio) elenaRadio.checked = true;
clientState.isInGame = 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;
}
}
function showGameScreen() {
console.log("[Main.js showGameScreen] Showing Game Screen.");
if(authSection) authSection.style.display = 'none';
if(userInfoDiv) userInfoDiv.style.display = 'block';
if(logoutButton) logoutButton.disabled = false;
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';
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';
}
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';
}
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
}
// --- НОВАЯ ФУНКЦИЯ ДЛЯ ПЕРЕНАПРАВЛЕНИЯ НА ЛОГИН ---
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...');
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,
}
},
utils: { // <-- ДОБАВЛЕН ОБЪЕКТ UTILS
isTokenValid // <-- ДОБАВЛЕНО
}
};
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'); // Получаем актуальный токен
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 && 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 {
// Если 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("Пожалуйста, войдите или зарегистрируйтесь.");
}
}
});
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 (authSection.style.display !== 'block' && !clientState.isLoggedIn) {
// Если не залогинены и не на экране логина, показать экран логина
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)
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));
// Проверка на специфичные ошибки, требующие перелогина
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();
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) { // isLoggedIn уже учитывает валидность токена при начальной загрузке
console.log('[Main.js] Client is considered logged in. Will attempt session recovery on socket connect.');
setGameStatusMessage("Подключение и восстановление сессии...");
} 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.');
});