bc/public/js/main.js

482 lines
28 KiB
JavaScript
Raw Permalink 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') {
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 = '<h3>Доступные PvP игры:</h3><p>Загрузка...</p>';
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.');
});