bc/public/js/main.js

417 lines
23 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 загружен глобально
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 = '<h3>Доступные PvP игры:</h3><p>Загрузка...</p>';
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();
});