diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/battle_club_git.iml b/.idea/battle_club_git.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/battle_club_git.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9f07350 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/bc.js b/bc.js deleted file mode 100644 index f0d22be..0000000 --- a/bc.js +++ /dev/null @@ -1,165 +0,0 @@ -// bc.js - Главный файл сервера Battle Club - -const express = require('express'); -const http = require('http'); // Используем HTTP, так как SSL будет на Node.js прокси (server.js) -const { Server } = require('socket.io'); -const path = require('path'); -// -// Импорт серверных модулей -const auth = require('./server_modules/auth'); -const GameManager = require('./server_modules/gameManager'); -const db = require('./server_modules/db'); // Импорт для инициализации соединения с БД (хотя пул создается при require) -const GAME_CONFIG = require('./server_modules/config'); // Конфиг игры -// gameData импортируется внутри GameInstance и GameLogic - -const app = express(); -const server = http.createServer(app); - -// Настройка Socket.IO -const io = new Server(server, { - cors: { - origin: "https://pavel-chagovsky.com:3200", // Указываем точный origin, включая порт, откуда придет запрос К ПРОКСИ - // Если доступ будет с нескольких доменов или портов, можно использовать массив: - // origin: ["https://pavel-chagovsky.com:3200", "https://oleg-okhotnikov.ru:3200"], - // Или для разработки можно временно использовать "*", но это менее безопасно: - // origin: "*", - methods: ["GET", "POST"] - } -}); - -// Раздача статических файлов из папки 'public' -app.use(express.static(path.join(__dirname, 'public'))); - -// Создаем экземпляр GameManager -const gameManager = new GameManager(io); - -// Хранилище информации о залогиненных пользователях по socket.id -const loggedInUsers = {}; // { socket.id: { userId: ..., username: ... } } - -// Обработка подключений Socket.IO -io.on('connection', (socket) => { - console.log(`[BC App HTTP] Socket.IO User connected: ${socket.id}`); - - socket.userData = null; - - socket.on('register', async (data) => { - console.log(`[BC App HTTP Socket.IO] Register attempt for username: "${data?.username}" from ${socket.id}`); - const result = await auth.registerUser(data?.username, data?.password); - if (result.success) { - console.log(`[BC App HTTP Socket.IO] Registration successful for ${result.username} (${result.userId})`); - } else { - console.warn(`[BC App HTTP Socket.IO] Registration failed for "${data?.username}": ${result.message}`); - } - socket.emit('registerResponse', result); - }); - - socket.on('login', async (data) => { - console.log(`[BC App HTTP Socket.IO] Login attempt for username: "${data?.username}" from ${socket.id}`); - const result = await auth.loginUser(data?.username, data?.password); - if (result.success) { - console.log(`[BC App HTTP Socket.IO] Login successful for ${result.username} (${result.userId}). Assigning to socket ${socket.id}.`); - socket.userData = { userId: result.userId, username: result.username }; - loggedInUsers[socket.id] = socket.userData; - gameManager.handleRequestGameState(socket, socket.userData.userId); - } else { - console.warn(`[BC App HTTP Socket.IO] Login failed for "${data?.username}": ${result.message}`); - socket.userData = null; - if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id]; - } - socket.emit('loginResponse', result); - }); - - socket.on('logout', () => { - console.log(`[BC App HTTP Socket.IO] Logout for user ${socket.userData?.username || socket.id}`); - gameManager.handleDisconnect(socket.id, socket.userData?.userId || socket.id); - socket.userData = null; - if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id]; - }); - - socket.on('createGame', (data) => { - const identifier = socket.userData?.userId || socket.id; - const mode = data?.mode || 'ai'; - if (mode === 'pvp' && !socket.userData) { - socket.emit('gameError', { message: 'Необходимо войти в систему для создания PvP игры.' }); - return; - } - console.log(`[BC App HTTP Socket.IO] Create Game request from ${socket.userData?.username || socket.id} (Identifier: ${identifier}). Mode: ${mode}, Character: ${data?.characterKey}`); - const characterKey = data?.characterKey || 'elena'; - gameManager.createGame(socket, mode, characterKey, identifier); - }); - - socket.on('joinGame', (data) => { - if (!socket.userData) { - socket.emit('gameError', { message: 'Необходимо войти в систему для присоединения к игре.' }); - return; - } - console.log(`[BC App HTTP Socket.IO] Join Game request from ${socket.userData.username} (${socket.id}). Game ID: ${data?.gameId}`); - const gameId = data?.gameId; - const identifier = socket.userData.userId; - if (gameId) { - gameManager.joinGame(socket, gameId, identifier); - } else { - socket.emit('gameError', { message: 'Не указан ID игры для присоединения.' }); - } - }); - - socket.on('findRandomGame', (data) => { - if (!socket.userData) { - socket.emit('gameError', { message: 'Необходимо войти в систему для поиска игры.' }); - return; - } - console.log(`[BC App HTTP Socket.IO] Find Random Game request from ${socket.userData.username} (${socket.id}). Preferred Character: ${data?.characterKey}`); - const characterKey = data?.characterKey || 'elena'; - const identifier = socket.userData.userId; - gameManager.findAndJoinRandomPvPGame(socket, characterKey, identifier); - }); - - socket.on('requestPvPGameList', () => { - console.log(`[BC App HTTP Socket.IO] Request PvP Game List from ${socket.userData?.username || socket.id}`); - const availableGames = gameManager.getAvailablePvPGamesListForClient(); - socket.emit('availablePvPGamesList', availableGames); - }); - - socket.on('requestGameState', () => { - if (!socket.userData) { - console.log(`[BC App HTTP Socket.IO] Request Game State from unauthenticated socket ${socket.id}.`); - socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' }); - return; - } - console.log(`[BC App HTTP Socket.IO] Request Game State from ${socket.userData.username} (${socket.id}).`); - gameManager.handleRequestGameState(socket, socket.userData.userId); - }); - - socket.on('playerAction', (actionData) => { - const identifier = socket.userData?.userId || socket.id; - gameManager.handlePlayerAction(identifier, actionData); - }); - - socket.on('disconnect', (reason) => { - const identifier = socket.userData?.userId || socket.id; - console.log(`[BC App HTTP Socket.IO] User disconnected: ${socket.id} (Причина: ${reason}). Identifier: ${identifier}`); - gameManager.handleDisconnect(socket.id, identifier); - if (loggedInUsers[socket.id]) { - delete loggedInUsers[socket.id]; - } - }); -}); - -// Запуск HTTP сервера -const PORT = process.env.BC_INTERNAL_PORT || 3200; // Внутренний порт для bc.js -const HOSTNAME = '127.0.0.1'; // Слушать ТОЛЬКО на localhost - -server.listen(PORT, HOSTNAME, () => { // Явно указываем HOSTNAME - console.log(`Battle Club HTTP Application Server running at http://${HOSTNAME}:${PORT}`); - console.log(`This server should only be accessed locally by the reverse proxy.`); - console.log(`Serving static files from: ${path.join(__dirname, 'public')}`); -}); - -// Обработка необработанных промис-ошибок -process.on('unhandledRejection', (reason, promise) => { - console.error('[BC App HTTP UNHANDLED REJECTION] Unhandled Rejection at:', promise, 'reason:', reason); -}); - -process.on('uncaughtException', (err) => { - console.error('[BC App HTTP UNCAUGHT EXCEPTION] Caught exception:', err); -}); \ No newline at end of file diff --git a/public/index.html b/public/index.html index e9b12aa..6ab467d 100644 --- a/public/index.html +++ b/public/index.html @@ -83,6 +83,18 @@ --> + + +
+ + +
+ +
diff --git a/public/js/client.js b/public/js/client.js index 3f729fd..9f85014 100644 --- a/public/js/client.js +++ b/public/js/client.js @@ -3,11 +3,13 @@ document.addEventListener('DOMContentLoaded', () => { const socket = io({ // Опции Socket.IO, если нужны + // transports: ['websocket'], // Можно попробовать для отладки, если есть проблемы с polling }); // --- Состояние клиента --- let currentGameState = null; - let myPlayerId = null; + let myPlayerId = null; // Технический ID слота в игре ('player' или 'opponent') + let myUserId = null; // ID залогиненного пользователя (из БД) let myCharacterKey = null; let opponentCharacterKey = null; let currentGameId = null; @@ -20,7 +22,6 @@ document.addEventListener('DOMContentLoaded', () => { let isInGame = false; // --- DOM Элементы --- - // Аутентификация const authSection = document.getElementById('auth-section'); const registerForm = document.getElementById('register-form'); const loginForm = document.getElementById('login-form'); @@ -30,125 +31,97 @@ document.addEventListener('DOMContentLoaded', () => { 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'); // Опечатка в ID, должно быть join-pvp-game + const joinPvPGameButton = document.getElementById('join-pvp-game'); // Убедитесь, что ID в HTML '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 attackButton = document.getElementById('button-attack'); const returnToMenuButton = document.getElementById('return-to-menu-button'); const gameOverScreen = document.getElementById('game-over-screen'); const abilitiesGrid = document.getElementById('abilities-grid'); - // === ИЗМЕНЕНИЕ: DOM элемент для таймера === - const turnTimerSpan = document.getElementById('turn-timer'); // Элемент для отображения времени - const turnTimerContainer = document.getElementById('turn-timer-container'); // Контейнер таймера для управления видимостью - // === КОНЕЦ ИЗМЕНЕНИЯ === - - console.log('Client.js DOMContentLoaded. Initializing elements...'); - + const turnTimerSpan = document.getElementById('turn-timer'); + const turnTimerContainer = document.getElementById('turn-timer-container'); // --- Функции управления UI --- function showAuthScreen() { - console.log('[UI] Showing Auth Screen'); - if (authSection) authSection.style.display = 'block'; - if (userInfoDiv) userInfoDiv.style.display = 'none'; - if (gameSetupDiv) gameSetupDiv.style.display = 'none'; - if (gameWrapper) gameWrapper.style.display = 'none'; + authSection.style.display = 'block'; + userInfoDiv.style.display = 'none'; + gameSetupDiv.style.display = 'none'; + gameWrapper.style.display = 'none'; hideGameOverModal(); setAuthMessage("Ожидание подключения к серверу..."); - if (statusContainer) statusContainer.style.display = 'block'; + statusContainer.style.display = 'block'; isInGame = false; disableGameControls(); resetGameVariables(); - // === ИЗМЕНЕНИЕ: Скрываем таймер при выходе на экран аутентификации === if (turnTimerContainer) turnTimerContainer.style.display = 'none'; if (turnTimerSpan) turnTimerSpan.textContent = '--'; - // === КОНЕЦ ИЗМЕНЕНИЯ === } function showGameSelectionScreen(username) { - console.log('[UI] Showing Game Selection Screen for:', username); - if (authSection) authSection.style.display = 'none'; - if (userInfoDiv) { - userInfoDiv.style.display = 'block'; - if (loggedInUsernameSpan) loggedInUsernameSpan.textContent = username; - } - if (gameSetupDiv) gameSetupDiv.style.display = 'block'; - if (gameWrapper) gameWrapper.style.display = 'none'; + authSection.style.display = 'none'; + userInfoDiv.style.display = 'block'; + loggedInUsernameSpan.textContent = username; + gameSetupDiv.style.display = 'block'; + gameWrapper.style.display = 'none'; hideGameOverModal(); setGameStatusMessage("Выберите режим игры или присоединитесь к существующей."); - if (statusContainer) statusContainer.style.display = 'block'; + statusContainer.style.display = 'block'; socket.emit('requestPvPGameList'); - updateAvailableGamesList([]); + updateAvailableGamesList([]); // Очищаем перед запросом if (gameIdInput) gameIdInput.value = ''; const elenaRadio = document.getElementById('char-elena'); if (elenaRadio) elenaRadio.checked = true; isInGame = false; disableGameControls(); - resetGameVariables(); - // === ИЗМЕНЕНИЕ: Скрываем таймер при выходе на экран выбора игры === + resetGameVariables(); // Сбрасываем игровые переменные при выходе в меню if (turnTimerContainer) turnTimerContainer.style.display = 'none'; if (turnTimerSpan) turnTimerSpan.textContent = '--'; - // === КОНЕЦ ИЗМЕНЕНИЯ === + enableSetupButtons(); // Включаем кнопки на экране выбора игры } function showGameScreen() { - console.log('[UI] Showing Game Screen'); hideGameOverModal(); - if (authSection) authSection.style.display = 'none'; - if (userInfoDiv) userInfoDiv.style.display = 'block'; // Оставляем видимым, чтобы видеть "Привет, username" - if (gameSetupDiv) gameSetupDiv.style.display = 'none'; - if (gameWrapper) gameWrapper.style.display = 'flex'; - setGameStatusMessage(""); - if (statusContainer) statusContainer.style.display = 'none'; + authSection.style.display = 'none'; + userInfoDiv.style.display = 'block'; // Оставляем инфо о пользователе + gameSetupDiv.style.display = 'none'; + gameWrapper.style.display = 'flex'; + setGameStatusMessage(""); // Очищаем статус, т.к. есть индикатор хода + statusContainer.style.display = 'none'; // Скрываем общий статус контейнер isInGame = true; - disableGameControls(); - // === ИЗМЕНЕНИЕ: Показываем контейнер таймера, когда игра начинается === - if (turnTimerContainer) turnTimerContainer.style.display = 'block'; + disableGameControls(); // Кнопки включатся, когда будет ход игрока + if (turnTimerContainer) turnTimerContainer.style.display = 'block'; // Показываем таймер if (turnTimerSpan) turnTimerSpan.textContent = '--'; // Начальное значение - // === КОНЕЦ ИЗМЕНЕНИЯ === } function resetGameVariables() { - currentGameId = null; - currentGameState = null; - myPlayerId = null; - myCharacterKey = null; - opponentCharacterKey = null; - playerBaseStatsServer = null; - opponentBaseStatsServer = null; - playerAbilitiesServer = null; - opponentAbilitiesServer = null; - window.gameState = null; - window.gameData = null; - window.myPlayerId = null; + currentGameId = null; currentGameState = null; myPlayerId = null; + myCharacterKey = null; opponentCharacterKey = null; + playerBaseStatsServer = null; opponentBaseStatsServer = null; + playerAbilitiesServer = null; opponentAbilitiesServer = null; + window.gameState = null; window.gameData = null; window.myPlayerId = null; } function hideGameOverModal() { - const hiddenClass = (window.GAME_CONFIG && window.GAME_CONFIG.CSS_CLASS_HIDDEN) ? window.GAME_CONFIG.CSS_CLASS_HIDDEN : 'hidden'; + const hiddenClass = window.GAME_CONFIG?.CSS_CLASS_HIDDEN || 'hidden'; if (gameOverScreen && !gameOverScreen.classList.contains(hiddenClass)) { - console.log('[Client.js DEBUG] Hiding GameOver Modal.'); gameOverScreen.classList.add(hiddenClass); if (window.gameUI?.uiElements?.gameOver?.modalContent) { window.gameUI.uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)'; window.gameUI.uiElements.gameOver.modalContent.style.opacity = '0'; } - if (window.gameUI?.uiElements?.opponent?.panel) { - const opponentPanel = window.gameUI.uiElements.opponent.panel; - if (opponentPanel.classList.contains('dissolving')) { - opponentPanel.classList.remove('dissolving'); - opponentPanel.style.opacity = '1'; - opponentPanel.style.transform = 'scale(1) translateY(0)'; - } + const opponentPanel = window.gameUI?.uiElements?.opponent?.panel; + if (opponentPanel?.classList.contains('dissolving')) { + opponentPanel.classList.remove('dissolving'); + opponentPanel.style.opacity = '1'; opponentPanel.style.transform = 'scale(1) translateY(0)'; } } } @@ -183,143 +156,102 @@ document.addEventListener('DOMContentLoaded', () => { function enableGameControls(enableAttack = true, enableAbilities = true) { if (attackButton) attackButton.disabled = !enableAttack; if (abilitiesGrid) { - const abilityButtonClass = window.GAME_CONFIG?.CSS_CLASS_ABILITY_BUTTON || 'ability-button'; - abilitiesGrid.querySelectorAll(`.${abilityButtonClass}`).forEach(button => { button.disabled = !enableAbilities; }); + const cls = window.GAME_CONFIG?.CSS_CLASS_ABILITY_BUTTON || 'ability-button'; + abilitiesGrid.querySelectorAll(`.${cls}`).forEach(b => { b.disabled = !enableAbilities; }); } if (window.gameUI?.uiElements?.controls?.buttonBlock) window.gameUI.uiElements.controls.buttonBlock.disabled = true; } + function disableGameControls() { enableGameControls(false, false); } - function disableGameControls() { - enableGameControls(false, false); - } - - // Инициализация кнопок и обработчиков - if (registerForm) { - registerForm.addEventListener('submit', (e) => { - e.preventDefault(); - const usernameInput = document.getElementById('register-username'); - const passwordInput = document.getElementById('register-password'); - if (usernameInput && passwordInput) { - registerForm.querySelector('button').disabled = true; - if (loginForm) loginForm.querySelector('button').disabled = true; - socket.emit('register', { username: usernameInput.value, password: passwordInput.value }); - } else { setAuthMessage("Ошибка: поля ввода не найдены.", true); } - }); - } - if (loginForm) { - loginForm.addEventListener('submit', (e) => { - e.preventDefault(); - const usernameInput = document.getElementById('login-username'); - const passwordInput = document.getElementById('login-password'); - if (usernameInput && passwordInput) { - if (registerForm) registerForm.querySelector('button').disabled = true; - loginForm.querySelector('button').disabled = true; - socket.emit('login', { username: usernameInput.value, password: passwordInput.value }); - } else { setAuthMessage("Ошибка: поля ввода не найдены.", true); } - }); - } - if (logoutButton) { - logoutButton.addEventListener('click', () => { - logoutButton.disabled = true; - socket.emit('logout'); - isLoggedIn = false; loggedInUsername = ''; - resetGameVariables(); isInGame = false; disableGameControls(); - showAuthScreen(); - setGameStatusMessage("Вы вышли из системы."); - logoutButton.disabled = false; - }); - } - if (createAIGameButton) { - createAIGameButton.addEventListener('click', () => { - if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы начать игру.", true); return; } - disableSetupButtons(); - socket.emit('createGame', { mode: 'ai', characterKey: 'elena' }); - setGameStatusMessage("Создание игры против AI..."); - }); - } - if (createPvPGameButton) { - createPvPGameButton.addEventListener('click', () => { - if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы начать игру.", true); return; } - disableSetupButtons(); - const selectedCharacter = getSelectedCharacterKey(); - socket.emit('createGame', { mode: 'pvp', characterKey: selectedCharacter }); - setGameStatusMessage(`Создание PvP игры за ${selectedCharacter === 'elena' ? 'Елену' : 'Альмагест'}...`); - }); - } - // Исправляем селектор для joinPvPGameButton, если ID в HTML был join-pvP-game - const actualJoinPvPGameButton = document.getElementById('join-pvp-game') || document.getElementById('join-pvP-game'); - if (actualJoinPvPGameButton && gameIdInput) { - actualJoinPvPGameButton.addEventListener('click', () => { - if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true); return; } - const gameIdToJoin = gameIdInput.value.trim(); - if (gameIdToJoin) { - disableSetupButtons(); - socket.emit('joinGame', { gameId: gameIdToJoin }); - setGameStatusMessage(`Присоединение к игре ${gameIdToJoin}...`); - } else { setGameStatusMessage("Пожалуйста, введите ID игры для присоединения.", true); } - }); - } - if (findRandomPvPGameButton) { - findRandomPvPGameButton.addEventListener('click', () => { - if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы найти игру.", true); return; } - disableSetupButtons(); - const selectedCharacter = getSelectedCharacterKey(); - socket.emit('findRandomGame', { characterKey: selectedCharacter }); - setGameStatusMessage(`Поиск случайной PvP игры (предпочтение: ${selectedCharacter === 'elena' ? 'Елена' : 'Альмагест'})...`); - }); - } function disableSetupButtons() { - if (createAIGameButton) createAIGameButton.disabled = true; - if (createPvPGameButton) createPvPGameButton.disabled = true; - if (actualJoinPvPGameButton) actualJoinPvPGameButton.disabled = true; - if (findRandomPvPGameButton) findRandomPvPGameButton.disabled = true; - if (availableGamesDiv) availableGamesDiv.querySelectorAll('button').forEach(btn => btn.disabled = true); + 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 (actualJoinPvPGameButton) actualJoinPvPGameButton.disabled = false; - if (findRandomPvPGameButton) findRandomPvPGameButton.disabled = false; + if(createAIGameButton) createAIGameButton.disabled = false; + if(createPvPGameButton) createPvPGameButton.disabled = false; + if(joinPvPGameButton) joinPvPGameButton.disabled = false; + if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = false; + // Кнопки в списке игр включаются в updateAvailableGamesList } - if (attackButton) { - attackButton.addEventListener('click', () => { - if (isLoggedIn && isInGame && currentGameId && currentGameState && !currentGameState.isGameOver) { - socket.emit('playerAction', { actionType: 'attack' }); - } else { - console.warn('[Client] Попытка действия (атака) вне допустимого состояния игры. isLogged:', isLoggedIn, 'isInGame:', isInGame); - disableGameControls(); - if (isLoggedIn && !isInGame) showGameSelectionScreen(loggedInUsername); - else if (!isLoggedIn) showAuthScreen(); - } - }); - } + // --- Инициализация обработчиков событий --- + if (registerForm) registerForm.addEventListener('submit', (e) => { + e.preventDefault(); + const u = document.getElementById('register-username').value; + const p = document.getElementById('register-password').value; + registerForm.querySelector('button').disabled = true; + if(loginForm) loginForm.querySelector('button').disabled = true; + socket.emit('register', { username: u, password: p }); + }); + if (loginForm) loginForm.addEventListener('submit', (e) => { + e.preventDefault(); + const u = document.getElementById('login-username').value; + const p = document.getElementById('login-password').value; + if(registerForm) registerForm.querySelector('button').disabled = true; + loginForm.querySelector('button').disabled = true; + socket.emit('login', { username: u, password: p }); + }); + if (logoutButton) logoutButton.addEventListener('click', () => { + logoutButton.disabled = true; socket.emit('logout'); + isLoggedIn = false; loggedInUsername = ''; myUserId = null; + resetGameVariables(); isInGame = false; disableGameControls(); + showAuthScreen(); setGameStatusMessage("Вы вышли из системы."); + logoutButton.disabled = false; + }); + if (createAIGameButton) createAIGameButton.addEventListener('click', () => { + if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; } + disableSetupButtons(); + socket.emit('createGame', { mode: 'ai', characterKey: 'elena' }); // AI всегда за Елену + setGameStatusMessage("Создание игры против AI..."); + }); + if (createPvPGameButton) createPvPGameButton.addEventListener('click', () => { + if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; } + disableSetupButtons(); + socket.emit('createGame', { mode: 'pvp', characterKey: getSelectedCharacterKey() }); + setGameStatusMessage("Создание PvP игры..."); + }); + if (joinPvPGameButton) joinPvPGameButton.addEventListener('click', () => { // Убедитесь, что ID кнопки 'join-pvp-game' + if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; } + const gameId = gameIdInput.value.trim(); + if (gameId) { + disableSetupButtons(); + socket.emit('joinGame', { gameId: gameId }); + setGameStatusMessage(`Присоединение к игре ${gameId}...`); + } else setGameStatusMessage("Введите ID игры.", true); + }); + if (findRandomPvPGameButton) findRandomPvPGameButton.addEventListener('click', () => { + if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; } + disableSetupButtons(); + socket.emit('findRandomGame', { characterKey: getSelectedCharacterKey() }); + setGameStatusMessage("Поиск случайной PvP игры..."); + }); + if (attackButton) attackButton.addEventListener('click', () => { + if (isLoggedIn && isInGame && currentGameId && currentGameState && !currentGameState.isGameOver) { + socket.emit('playerAction', { actionType: 'attack' }); + } else { /* обработка ошибки/некорректного состояния */ } + }); function handleAbilityButtonClick(event) { - const button = event.currentTarget; - const abilityId = button.dataset.abilityId; + const abilityId = event.currentTarget.dataset.abilityId; if (isLoggedIn && isInGame && currentGameId && abilityId && currentGameState && !currentGameState.isGameOver) { socket.emit('playerAction', { actionType: 'ability', abilityId: abilityId }); - } else { - console.warn('[Client] Попытка действия (способность) вне допустимого состояния игры. isLogged:', isLoggedIn, 'isInGame:', isInGame); - disableGameControls(); - if (isLoggedIn && !isInGame) showGameSelectionScreen(loggedInUsername); - else if (!isLoggedIn) showAuthScreen(); - } - } - if (returnToMenuButton) { - returnToMenuButton.addEventListener('click', () => { - if (!isLoggedIn) { showAuthScreen(); return; } - returnToMenuButton.disabled = true; - console.log('[Client] Return to menu button clicked. Resetting game state and showing selection screen.'); - resetGameVariables(); isInGame = false; disableGameControls(); hideGameOverModal(); - showGameSelectionScreen(loggedInUsername); - }); + } else { /* обработка ошибки/некорректного состояния */ } } + if (returnToMenuButton) returnToMenuButton.addEventListener('click', () => { + if (!isLoggedIn) { showAuthScreen(); return; } + returnToMenuButton.disabled = true; + resetGameVariables(); isInGame = false; disableGameControls(); hideGameOverModal(); + showGameSelectionScreen(loggedInUsername); // Возвращаемся на экран выбора + // Кнопка включится при следующем показе модалки + }); function initializeAbilityButtons() { + // ... (код без изменений, как был) if (!abilitiesGrid || !window.gameUI || !window.GAME_CONFIG) { if (abilitiesGrid) abilitiesGrid.innerHTML = '

Ошибка загрузки способностей.

'; - console.error('[Client.js] initializeAbilityButtons failed: abilitiesGrid, gameUI, or GAME_CONFIG not found.'); return; } abilitiesGrid.innerHTML = ''; @@ -363,78 +295,104 @@ document.addEventListener('DOMContentLoaded', () => { const li = document.createElement('li'); li.textContent = `ID: ${game.id.substring(0, 8)}... - ${game.status || 'Ожидает игрока'}`; const joinBtn = document.createElement('button'); - joinBtn.textContent = 'Присоединиться'; joinBtn.dataset.gameId = game.id; + joinBtn.textContent = 'Присоединиться'; + joinBtn.dataset.gameId = game.id; + + // === ИЗМЕНЕНИЕ: Деактивация кнопки "Присоединиться" для своих игр === + if (isLoggedIn && myUserId && game.ownerIdentifier === myUserId) { + joinBtn.disabled = true; + joinBtn.title = "Вы не можете присоединиться к своей же ожидающей игре."; + } else { + joinBtn.disabled = false; + } + // === КОНЕЦ ИЗМЕНЕНИЯ === + joinBtn.addEventListener('click', (e) => { - if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true); return; } + if (!isLoggedIn) { setGameStatusMessage("Пожалуйста, войдите.", true); return; } + if (e.target.disabled) return; // Не обрабатывать клик по отключенной кнопке disableSetupButtons(); socket.emit('joinGame', { gameId: e.target.dataset.gameId }); }); - li.appendChild(joinBtn); ul.appendChild(li); + li.appendChild(joinBtn); + ul.appendChild(li); } }); availableGamesDiv.appendChild(ul); - availableGamesDiv.querySelectorAll('button').forEach(btn => btn.disabled = false); - } else { availableGamesDiv.innerHTML += '

Нет доступных игр. Создайте свою!

'; } - enableSetupButtons(); + } else { + availableGamesDiv.innerHTML += '

Нет доступных игр. Создайте свою!

'; + } + enableSetupButtons(); // Включаем основные кнопки создания/поиска } + // --- Обработчики событий Socket.IO --- socket.on('connect', () => { - console.log('[Client] Socket connected to server! Socket ID:', socket.id); - if (isLoggedIn) { - console.log(`[Client] Reconnected as ${loggedInUsername}. Requesting state.`); - socket.emit('requestGameState'); - } else { showAuthScreen(); } + console.log('[Client] Socket connected:', socket.id); + if (isLoggedIn && myUserId) { // Проверяем и isLoggedIn и myUserId + socket.emit('requestGameState'); // Запрашиваем состояние, если были залогинены + } else { + showAuthScreen(); // Иначе показываем экран логина + } }); socket.on('registerResponse', (data) => { setAuthMessage(data.message, !data.success); if (data.success && registerForm) registerForm.reset(); - if (registerForm) registerForm.querySelector('button').disabled = false; - if (loginForm) loginForm.querySelector('button').disabled = false; + if(registerForm) registerForm.querySelector('button').disabled = false; + if(loginForm) loginForm.querySelector('button').disabled = false; }); socket.on('loginResponse', (data) => { setAuthMessage(data.message, !data.success); if (data.success) { - isLoggedIn = true; loggedInUsername = data.username; setAuthMessage(""); + isLoggedIn = true; + loggedInUsername = data.username; + myUserId = data.userId; // === ИЗМЕНЕНИЕ: Сохраняем ID пользователя === + setAuthMessage(""); showGameSelectionScreen(data.username); } else { - isLoggedIn = false; loggedInUsername = ''; - if (registerForm) registerForm.querySelector('button').disabled = false; - if (loginForm) loginForm.querySelector('button').disabled = false; + isLoggedIn = false; loggedInUsername = ''; myUserId = null; + if(registerForm) registerForm.querySelector('button').disabled = false; + if(loginForm) loginForm.querySelector('button').disabled = false; } }); socket.on('gameNotFound', (data) => { - console.log('[Client] Game not found response:', data?.message); + console.log('[Client] Game not found/ended:', data?.message); resetGameVariables(); isInGame = false; disableGameControls(); hideGameOverModal(); - if (turnTimerContainer) turnTimerContainer.style.display = 'none'; // Скрываем таймер + if (turnTimerContainer) turnTimerContainer.style.display = 'none'; + if (turnTimerSpan) turnTimerSpan.textContent = '--'; if (isLoggedIn) { showGameSelectionScreen(loggedInUsername); - setGameStatusMessage("Выберите режим игры или присоединитесь к существующей."); - enableSetupButtons(); + setGameStatusMessage(data?.message || "Активная игровая сессия не найдена."); } else { showAuthScreen(); - setAuthMessage(data?.message || "Пожалуйста, войдите, чтобы начать новую игру.", false); + setAuthMessage(data?.message || "Пожалуйста, войдите."); } }); socket.on('disconnect', (reason) => { - console.log('[Client] Disconnected from server:', reason); - setGameStatusMessage(`Отключено от сервера: ${reason}. Пожалуйста, обновите страницу.`, true); + console.log('[Client] Disconnected:', reason); + setGameStatusMessage(`Отключено: ${reason}. Обновите страницу.`, true); disableGameControls(); - // === ИЗМЕНЕНИЕ: При дисконнекте останавливаем таймер (если он виден) === - if (turnTimerSpan) turnTimerSpan.textContent = 'Отключено'; - // Не скрываем контейнер, чтобы было видно сообщение "Отключено" - // === КОНЕЦ ИЗМЕНЕНИЯ === + if (turnTimerSpan) turnTimerSpan.textContent = 'Откл.'; + // Не сбрасываем isLoggedIn, чтобы при переподключении можно было восстановить сессию }); - socket.on('gameStarted', (data) => { - if (!isLoggedIn) { console.warn('[Client] Ignoring gameStarted: Not logged in.'); return; } - console.log('[Client] Event "gameStarted" received:', data); + socket.on('gameCreated', (data) => { // Сервер присылает это после успешного createGame + console.log('[Client] Game created by this client:', data); + currentGameId = data.gameId; + myPlayerId = data.yourPlayerId; // Сервер должен прислать роль создателя + // Остальные данные (gameState, baseStats) придут с gameStarted или gameState (если это PvP ожидание) + // Если это PvP и игра ожидает, сервер может прислать waitingForOpponent + }); + + socket.on('gameStarted', (data) => { + if (!isLoggedIn) return; + console.log('[Client] Game started:', data); + // ... (остальной код gameStarted без изменений, как был) if (window.gameUI?.uiElements?.opponent?.panel) { const opponentPanel = window.gameUI.uiElements.opponent.panel; if (opponentPanel.classList.contains('dissolving')) { @@ -450,7 +408,6 @@ document.addEventListener('DOMContentLoaded', () => { if (data.clientConfig) window.GAME_CONFIG = { ...data.clientConfig }; else if (!window.GAME_CONFIG) { window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' }; - console.warn('[Client.js gameStarted] No clientConfig received from server. Using fallback.'); } window.gameState = currentGameState; window.gameData = { playerBaseStats: playerBaseStatsServer, opponentBaseStats: opponentBaseStatsServer, playerAbilities: playerAbilitiesServer, opponentAbilities: opponentAbilitiesServer }; @@ -463,108 +420,131 @@ document.addEventListener('DOMContentLoaded', () => { } requestAnimationFrame(() => { if (window.gameUI && typeof window.gameUI.updateUI === 'function') { - console.log('[Client] Calling gameUI.updateUI() after gameStarted.'); window.gameUI.updateUI(); } }); hideGameOverModal(); setGameStatusMessage(""); }); - socket.on('gameStateUpdate', (data) => { - if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) { - console.warn('[Client] Ignoring gameStateUpdate: Not logged in or not in game context.'); - return; + // Используется для восстановления состояния уже идущей игры + socket.on('gameState', (data) => { + if (!isLoggedIn) return; + console.log('[Client] Received full gameState (e.g. on reconnect):', data); + // Это событие теперь может дублировать 'gameStarted' для переподключения. + // Убедимся, что логика похожа на gameStarted. + currentGameId = data.gameId; + myPlayerId = data.yourPlayerId; + currentGameState = data.gameState; // Используем gameState вместо initialGameState + playerBaseStatsServer = data.playerBaseStats; + opponentBaseStatsServer = data.opponentBaseStats; + playerAbilitiesServer = data.playerAbilities; + opponentAbilitiesServer = data.opponentAbilities; + myCharacterKey = playerBaseStatsServer?.characterKey; + opponentCharacterKey = opponentBaseStatsServer?.characterKey; + + if (data.clientConfig) window.GAME_CONFIG = { ...data.clientConfig }; + else if (!window.GAME_CONFIG) { + window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' }; + } + window.gameState = currentGameState; + window.gameData = { playerBaseStats: playerBaseStatsServer, opponentBaseStats: opponentBaseStatsServer, playerAbilities: playerAbilitiesServer, opponentAbilities: opponentAbilitiesServer }; + window.myPlayerId = myPlayerId; + + if (!isInGame) showGameScreen(); // Показываем экран игры, если еще не там + initializeAbilityButtons(); // Переинициализируем кнопки + + // Лог при 'gameState' может быть уже накопленным, добавляем его + if (window.gameUI?.uiElements?.log?.list && data.log) { // Очищаем лог перед добавлением нового при полном обновлении + window.gameUI.uiElements.log.list.innerHTML = ''; } - currentGameState = data.gameState; window.gameState = currentGameState; - if (window.gameUI && typeof window.gameUI.updateUI === 'function') window.gameUI.updateUI(); if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) { data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type)); } + + requestAnimationFrame(() => { + if (window.gameUI && typeof window.gameUI.updateUI === 'function') { + window.gameUI.updateUI(); + } + }); + hideGameOverModal(); + // Таймер будет обновлен следующим событием 'turnTimerUpdate' + }); + + + socket.on('gameStateUpdate', (data) => { + if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) return; + currentGameState = data.gameState; window.gameState = currentGameState; + if (window.gameUI?.updateUI) window.gameUI.updateUI(); + if (window.gameUI?.addToLog && data.log) { + data.log.forEach(log => window.gameUI.addToLog(log.message, log.type)); + } }); socket.on('logUpdate', (data) => { - if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) { - console.warn('[Client] Ignoring logUpdate: Not logged in or not in game context.'); - return; - } - if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) { - data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type)); + if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) return; + if (window.gameUI?.addToLog && data.log) { + data.log.forEach(log => window.gameUI.addToLog(log.message, log.type)); } }); socket.on('gameOver', (data) => { + // ... (код без изменений, как был) if (!isLoggedIn || !currentGameId || !window.GAME_CONFIG) { - console.warn('[Client] Ignoring gameOver: Not logged in or currentGameId is null/stale.'); if (!currentGameId && isLoggedIn) socket.emit('requestGameState'); else if (!isLoggedIn) showAuthScreen(); return; } - console.log(`[Client gameOver] Received for game ${currentGameId}. My technical slot ID (myPlayerId): ${myPlayerId}, Winner's slot ID from server (data.winnerId): ${data.winnerId}`); const playerWon = data.winnerId === myPlayerId; - console.log(`[Client gameOver] Calculated playerWon for this client: ${playerWon}`); currentGameState = data.finalGameState; window.gameState = currentGameState; - console.log('[Client gameOver] Final GameState:', currentGameState); - if (window.gameData) console.log(`[Client gameOver] For ui.js, myName: ${window.gameData.playerBaseStats?.name}, opponentName: ${window.gameData.opponentBaseStats?.name}`); - if (window.gameUI && typeof window.gameUI.updateUI === 'function') window.gameUI.updateUI(); - if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) { - data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type)); + if (window.gameUI?.updateUI) window.gameUI.updateUI(); + if (window.gameUI?.addToLog && data.log) { + data.log.forEach(log => window.gameUI.addToLog(log.message, log.type)); } - if (window.gameUI && typeof window.gameUI.showGameOver === 'function') { - const opponentKeyForModal = window.gameData?.opponentBaseStats?.characterKey; - window.gameUI.showGameOver(playerWon, data.reason, opponentKeyForModal, data); + if (window.gameUI?.showGameOver) { + const oppKey = window.gameData?.opponentBaseStats?.characterKey; + window.gameUI.showGameOver(playerWon, data.reason, oppKey, data); } if (returnToMenuButton) returnToMenuButton.disabled = false; setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли.")); - // === ИЗМЕНЕНИЕ: При gameOver скрываем таймер или показываем "Игра окончена" === - if (turnTimerContainer) turnTimerContainer.style.display = 'block'; // Оставляем видимым - if (turnTimerSpan) turnTimerSpan.textContent = 'Конец'; - // === КОНЕЦ ИЗМЕНЕНИЯ === + if (window.gameUI?.updateTurnTimerDisplay) { // Обновляем UI таймера + window.gameUI.updateTurnTimerDisplay(null, false, currentGameState?.gameMode); // Передаем null, чтобы показать "Конец" или скрыть + } }); socket.on('waitingForOpponent', () => { if (!isLoggedIn) return; setGameStatusMessage("Ожидание присоединения оппонента..."); - disableGameControls(); - enableSetupButtons(); // Можно оставить возможность отменить, если долго ждет - // === ИЗМЕНЕНИЕ: При ожидании оппонента таймер неактивен === - if (turnTimerContainer) turnTimerContainer.style.display = 'none'; - if (turnTimerSpan) turnTimerSpan.textContent = '--'; - // === КОНЕЦ ИЗМЕНЕНИЯ === + disableGameControls(); // Боевые кнопки неактивны + disableSetupButtons(); // Кнопки создания/присоединения тоже, пока ждем + if (createPvPGameButton) createPvPGameButton.disabled = false; // Оставляем активной "Создать PvP" для отмены + if (window.gameUI?.updateTurnTimerDisplay) { + window.gameUI.updateTurnTimerDisplay(null, false, 'pvp'); // Таймер неактивен + } }); socket.on('opponentDisconnected', (data) => { - if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) { - console.warn('[Client] Ignoring opponentDisconnected: Not logged in or not in game context.'); - return; - } - const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system'; - const disconnectedCharacterName = data.disconnectedCharacterName || 'Противник'; - if (window.gameUI && typeof window.gameUI.addToLog === 'function') { - window.gameUI.addToLog(`🔌 Противник (${disconnectedCharacterName}) отключился.`, systemLogType); - } + if (!isLoggedIn || !isInGame || !currentGameId || !window.GAME_CONFIG) return; + const name = data.disconnectedCharacterName || 'Противник'; + if (window.gameUI?.addToLog) window.gameUI.addToLog(`🔌 Противник (${name}) отключился.`, 'system'); if (currentGameState && !currentGameState.isGameOver) { - setGameStatusMessage(`Противник (${disconnectedCharacterName}) отключился. Ожидание завершения игры сервером...`, true); + setGameStatusMessage(`Противник (${name}) отключился. Ожидание...`, true); disableGameControls(); } }); socket.on('gameError', (data) => { console.error('[Client] Server error:', data.message); - const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system'; - if (isLoggedIn && isInGame && currentGameId && currentGameState && !currentGameState.isGameOver && window.gameUI && typeof window.gameUI.addToLog === 'function') { - window.gameUI.addToLog(`❌ Ошибка игры: ${data.message}`, systemLogType); - disableGameControls(); - setGameStatusMessage(`Ошибка в игре: ${data.message}.`, true); + if (isLoggedIn && isInGame && currentGameState && !currentGameState.isGameOver && window.gameUI?.addToLog) { + window.gameUI.addToLog(`❌ Ошибка игры: ${data.message}`, 'system'); + disableGameControls(); setGameStatusMessage(`Ошибка: ${data.message}.`, true); } else { - setGameStatusMessage(`❌ Ошибка игры: ${data.message}`, true); - resetGameVariables(); isInGame = false; disableGameControls(); - if (isLoggedIn && loggedInUsername) showGameSelectionScreen(loggedInUsername); - else showAuthScreen(); + setGameStatusMessage(`❌ Ошибка: ${data.message}`, true); + if (isLoggedIn) enableSetupButtons(); // Если на экране выбора игры, включаем кнопки + else { // Если на экране логина + if(registerForm) registerForm.querySelector('button').disabled = false; + if(loginForm) loginForm.querySelector('button').disabled = false; + } } - if (!isLoggedIn) { - if (registerForm) registerForm.querySelector('button').disabled = false; - if (loginForm) loginForm.querySelector('button').disabled = false; - } else if (!isInGame) { enableSetupButtons(); } }); socket.on('availablePvPGamesList', (games) => { @@ -572,58 +552,32 @@ document.addEventListener('DOMContentLoaded', () => { updateAvailableGamesList(games); }); - socket.on('noPendingGamesFound', (data) => { + socket.on('noPendingGamesFound', (data) => { // Вызывается, когда создается новая игра после поиска if (!isLoggedIn) return; - setGameStatusMessage(data.message || "Свободных игр не найдено. Создана новая для вас, ожидайте оппонента."); - updateAvailableGamesList([]); - isInGame = false; disableGameControls(); disableSetupButtons(); - // === ИЗМЕНЕНИЕ: При ожидании оппонента (создана новая игра) таймер неактивен === - if (turnTimerContainer) turnTimerContainer.style.display = 'none'; - if (turnTimerSpan) turnTimerSpan.textContent = '--'; - // === КОНЕЦ ИЗМЕНЕНИЯ === + setGameStatusMessage(data.message || "Свободных игр не найдено. Создана новая для вас."); + updateAvailableGamesList([]); // Очищаем список + // currentGameId и myPlayerId должны были прийти с gameCreated + isInGame = false; // Еще не в активной фазе боя + disableGameControls(); + disableSetupButtons(); // Мы в ожидающей игре + if (window.gameUI?.updateTurnTimerDisplay) { + window.gameUI.updateTurnTimerDisplay(null, false, 'pvp'); + } }); - // === ИЗМЕНЕНИЕ: Обработчик события обновления таймера === socket.on('turnTimerUpdate', (data) => { if (!isInGame || !currentGameState || currentGameState.isGameOver) { - // Если игра не активна, или уже завершена, или нет состояния, игнорируем обновление таймера - if (turnTimerContainer && !currentGameState?.isGameOver) turnTimerContainer.style.display = 'none'; // Скрываем, если не game over - if (turnTimerSpan && !currentGameState?.isGameOver) turnTimerSpan.textContent = '--'; + if (window.gameUI?.updateTurnTimerDisplay && !currentGameState?.isGameOver) { // Только если не game over + window.gameUI.updateTurnTimerDisplay(null, false, currentGameState?.gameMode); + } return; } - - if (turnTimerSpan && turnTimerContainer) { - if (data.remainingTime === null || data.remainingTime === undefined) { - // Сервер сигнализирует, что таймер неактивен (например, ход AI) - turnTimerContainer.style.display = 'block'; // Контейнер может быть видимым - // Определяем, чей ход, чтобы показать соответствующее сообщение - const isMyActualTurn = myPlayerId && currentGameState.isPlayerTurn === (myPlayerId === GAME_CONFIG.PLAYER_ID); - - if (!data.isPlayerTurn && currentGameState.gameMode === 'ai') { // Ход AI - turnTimerSpan.textContent = 'Ход ИИ'; - turnTimerSpan.classList.remove('low-time'); - } else if (!isMyActualTurn && currentGameState.gameMode === 'pvp' && !data.isPlayerTurn !== (myPlayerId === GAME_CONFIG.PLAYER_ID)) { // Ход оппонента в PvP - turnTimerSpan.textContent = 'Ход оппонента'; - turnTimerSpan.classList.remove('low-time'); - } else { // Ход текущего игрока, но сервер прислал null - странно, но покажем '--' - turnTimerSpan.textContent = '--'; - turnTimerSpan.classList.remove('low-time'); - } - } else { - turnTimerContainer.style.display = 'block'; // Убедимся, что контейнер виден - const seconds = Math.ceil(data.remainingTime / 1000); - turnTimerSpan.textContent = `0:${seconds < 10 ? '0' : ''}${seconds}`; - - // Добавляем/удаляем класс для предупреждения, если времени мало - if (seconds <= 10) { // Например, 10 секунд - порог - turnTimerSpan.classList.add('low-time'); - } else { - turnTimerSpan.classList.remove('low-time'); - } - } + if (window.gameUI && typeof window.gameUI.updateTurnTimerDisplay === 'function') { + // Определяем, является ли текущий ход ходом этого клиента + const isMyActualTurn = myPlayerId && currentGameState.isPlayerTurn === (myPlayerId === GAME_CONFIG.PLAYER_ID); + window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyActualTurn, currentGameState.gameMode); } }); - // === КОНЕЦ ИЗМЕНЕНИЯ === - showAuthScreen(); + showAuthScreen(); // Начальный экран }); \ No newline at end of file diff --git a/public/js/ui.js b/public/js/ui.js index a96d57e..e7e7922 100644 --- a/public/js/ui.js +++ b/public/js/ui.js @@ -32,10 +32,8 @@ buttonAttack: document.getElementById('button-attack'), buttonBlock: document.getElementById('button-block'), abilitiesGrid: document.getElementById('abilities-grid'), - // === ИЗМЕНЕНИЕ: Добавлены элементы таймера === turnTimerContainer: document.getElementById('turn-timer-container'), turnTimerSpan: document.getElementById('turn-timer') - // === КОНЕЦ ИЗМЕНЕНИЯ === }, log: { list: document.getElementById('log-list'), @@ -51,6 +49,15 @@ opponentResourceTypeIcon: document.getElementById('opponent-resource-bar')?.closest('.stat-bar-container')?.querySelector('.bar-icon i'), playerResourceBarContainer: document.getElementById('player-resource-bar')?.closest('.stat-bar-container'), opponentResourceBarContainer: document.getElementById('opponent-resource-bar')?.closest('.stat-bar-container'), + + // === НОВЫЕ ЭЛЕМЕНТЫ для переключателя панелей === + panelSwitcher: { + controlsContainer: document.querySelector('.panel-switcher-controls'), + showPlayerBtn: document.getElementById('show-player-panel-btn'), + showOpponentBtn: document.getElementById('show-opponent-panel-btn') + }, + battleArenaContainer: document.querySelector('.battle-arena-container') + // === КОНЕЦ НОВЫХ ЭЛЕМЕНТОВ === }; function addToLog(message, type = 'info') { @@ -91,9 +98,9 @@ if (elements.name) { let iconClass = 'fa-question'; const characterKey = fighterBaseStats.characterKey; - if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-elena'; } // Используем специфичный класс для цвета + if (characterKey === 'elena') { iconClass = 'fa-hat-wizard icon-elena'; } else if (characterKey === 'almagest') { iconClass = 'fa-staff-aesculapius icon-almagest'; } - else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-balard'; } // Для Баларда тоже специфичный + else if (characterKey === 'balard') { iconClass = 'fa-khanda icon-balard'; } let nameHtml = ` ${fighterBaseStats.name || 'Неизвестно'}`; if (isControlledByThisClient) nameHtml += " (Вы)"; elements.name.innerHTML = nameHtml; @@ -140,7 +147,7 @@ else if (fighterBaseStats.characterKey === 'balard') { elements.panel.classList.add('panel-balard'); borderColorVar = 'var(--accent-opponent)'; } let glowColorVar = 'rgba(0, 0, 0, 0.4)'; if (fighterBaseStats.characterKey === 'elena') glowColorVar = 'var(--panel-glow-player)'; - else if (fighterBaseStats.characterKey === 'almagest') glowColorVar = 'var(--panel-glow-almagest)'; // Отдельный цвет для Альмагест + else if (fighterBaseStats.characterKey === 'almagest') glowColorVar = 'var(--panel-glow-almagest)'; else if (fighterBaseStats.characterKey === 'balard') glowColorVar = 'var(--panel-glow-opponent)'; elements.panel.style.borderColor = borderColorVar; elements.panel.style.boxShadow = `0 0 15px ${glowColorVar}, inset 0 0 10px rgba(0, 0, 0, 0.3)`; @@ -207,22 +214,14 @@ } } - // === ИЗМЕНЕНИЕ: Новая функция для обновления таймера === - /** - * Обновляет отображение таймера хода. - * @param {number|null} remainingTimeMs - Оставшееся время в миллисекундах, или null если таймер неактивен. - * @param {boolean} isCurrentPlayerActualTurn - Флаг, является ли текущий ход ходом этого клиента. - * @param {string} gameMode - Режим игры ('ai' или 'pvp'). - */ function updateTurnTimerDisplay(remainingTimeMs, isCurrentPlayerActualTurn, gameMode) { const timerSpan = uiElements.controls.turnTimerSpan; const timerContainer = uiElements.controls.turnTimerContainer; - const config = window.GAME_CONFIG || {}; if (!timerSpan || !timerContainer) return; if (window.gameState && window.gameState.isGameOver) { - timerContainer.style.display = 'block'; // Может быть 'flex' или другой, в зависимости от CSS + timerContainer.style.display = 'block'; timerSpan.textContent = 'Конец'; timerSpan.classList.remove('low-time'); return; @@ -231,11 +230,11 @@ if (remainingTimeMs === null || remainingTimeMs === undefined) { timerContainer.style.display = 'block'; timerSpan.classList.remove('low-time'); - if (gameMode === 'ai' && !isCurrentPlayerActualTurn) { // Предполагаем, что если не ход игрока в AI, то ход AI + if (gameMode === 'ai' && !isCurrentPlayerActualTurn) { timerSpan.textContent = 'Ход ИИ'; } else if (gameMode === 'pvp' && !isCurrentPlayerActualTurn) { timerSpan.textContent = 'Ход оппонента'; - } else { // Ход текущего игрока, но нет времени (например, ожидание первого хода) + } else { timerSpan.textContent = '--'; } } else { @@ -243,14 +242,13 @@ const seconds = Math.ceil(remainingTimeMs / 1000); timerSpan.textContent = `0:${seconds < 10 ? '0' : ''}${seconds}`; - if (seconds <= 10 && isCurrentPlayerActualTurn) { // Предупреждение только если это мой ход + if (seconds <= 10 && isCurrentPlayerActualTurn) { timerSpan.classList.add('low-time'); } else { timerSpan.classList.remove('low-time'); } } } - // === КОНЕЦ ИЗМЕНЕНИЯ === function updateUI() { @@ -267,13 +265,11 @@ if(uiElements.controls.buttonAttack) uiElements.controls.buttonAttack.disabled = true; if(uiElements.controls.buttonBlock) uiElements.controls.buttonBlock.disabled = true; if(uiElements.controls.abilitiesGrid) uiElements.controls.abilitiesGrid.innerHTML = '

Загрузка способностей...

'; - // === ИЗМЕНЕНИЕ: Сбрасываем таймер, если нет данных === if (uiElements.controls.turnTimerContainer) uiElements.controls.turnTimerContainer.style.display = 'none'; if (uiElements.controls.turnTimerSpan) { uiElements.controls.turnTimerSpan.textContent = '--'; uiElements.controls.turnTimerSpan.classList.remove('low-time'); } - // === КОНЕЦ ИЗМЕНЕНИЯ === return; } if (!uiElements.player.panel || !uiElements.opponent.panel || !uiElements.controls.turnIndicator || !uiElements.controls.abilitiesGrid || !uiElements.log.list) { @@ -423,8 +419,7 @@ const currentActualGameState = window.gameState; const gameOverScreenElement = uiElements.gameOver.screen; - console.log(`[UI.JS DEBUG] showGameOver CALLED. PlayerWon: ${playerWon}, Reason: ${reason}`); - if (!gameOverScreenElement) { console.warn("[UI.JS DEBUG] showGameOver: gameOverScreenElement not found."); return; } + if (!gameOverScreenElement) { return; } const resultMsgElement = uiElements.gameOver.message; const myNameForResult = clientSpecificGameData?.playerBaseStats?.name || "Игрок"; @@ -433,19 +428,16 @@ if (resultMsgElement) { let winText = `Победа! ${myNameForResult} празднует!`; let loseText = `Поражение! ${opponentNameForResult} оказался(лась) сильнее!`; - // === ИЗМЕНЕНИЕ: Добавляем обработку причины 'turn_timeout' === if (reason === 'opponent_disconnected') { let disconnectedName = data?.disconnectedCharacterName || opponentNameForResult; winText = `${disconnectedName} покинул(а) игру. Победа присуждается вам!`; } else if (reason === 'turn_timeout') { - // Если текущий игрок (чей ход был) проиграл по таймауту - if (!playerWon) { // playerWon здесь будет false, если победил оппонент (т.е. мой таймаут) + if (!playerWon) { loseText = `Время на ход истекло! Поражение. ${opponentNameForResult} побеждает!`; - } else { // Если я победил, потому что у оппонента истекло время + } else { winText = `Время на ход у ${opponentNameForResult} истекло! Победа!`; } } - // === КОНЕЦ ИЗМЕНЕНИЯ === resultMsgElement.textContent = playerWon ? winText : loseText; resultMsgElement.style.color = playerWon ? 'var(--heal-color)' : 'var(--damage-color)'; } @@ -468,7 +460,7 @@ opponentPanelElement.style.transition = ''; } - setTimeout((finalStateInTimeout, wonInTimeout, reasonInTimeout, keyInTimeout, dataInTimeout) => { + setTimeout((finalStateInTimeout) => { if (gameOverScreenElement && finalStateInTimeout && finalStateInTimeout.isGameOver === true) { if (gameOverScreenElement.classList.contains(config.CSS_CLASS_HIDDEN || 'hidden')) { gameOverScreenElement.classList.remove(config.CSS_CLASS_HIDDEN || 'hidden'); @@ -496,16 +488,47 @@ gameOverScreenElement.offsetHeight; } } - }, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState, playerWon, reason, opponentCharacterKeyFromClient, data); + }, config.DELAY_BEFORE_VICTORY_MODAL || 1500, currentActualGameState); } + // === НОВАЯ ФУНКЦИЯ для настройки переключателя панелей === + function setupPanelSwitcher() { + const { showPlayerBtn, showOpponentBtn } = uiElements.panelSwitcher; + const battleArena = uiElements.battleArenaContainer; + + if (showPlayerBtn && showOpponentBtn && battleArena) { + showPlayerBtn.addEventListener('click', () => { + battleArena.classList.remove('show-opponent-panel'); + showPlayerBtn.classList.add('active'); + showOpponentBtn.classList.remove('active'); + }); + + showOpponentBtn.addEventListener('click', () => { + battleArena.classList.add('show-opponent-panel'); + showOpponentBtn.classList.add('active'); + showPlayerBtn.classList.remove('active'); + }); + + // По умолчанию при загрузке (если кнопки видимы) панель игрока активна + // CSS уже должен это обеспечивать, но для надежности можно убедиться + if (window.getComputedStyle(uiElements.panelSwitcher.controlsContainer).display !== 'none') { + battleArena.classList.remove('show-opponent-panel'); + showPlayerBtn.classList.add('active'); + showOpponentBtn.classList.remove('active'); + } + } + } + // === КОНЕЦ НОВОЙ ФУНКЦИИ === + window.gameUI = { uiElements, addToLog, updateUI, showGameOver, - // === ИЗМЕНЕНИЕ: Экспортируем функцию обновления таймера === updateTurnTimerDisplay - // === КОНЕЦ ИЗМЕНЕНИЯ === }; + + // Настраиваем переключатель панелей при загрузке скрипта + setupPanelSwitcher(); + })(); \ No newline at end of file diff --git a/public/style_alt.css b/public/style_alt.css index 531a3b7..ba69ff7 100644 --- a/public/style_alt.css +++ b/public/style_alt.css @@ -3,7 +3,7 @@ @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css'); :root { - /* --- Переменные цветов и шрифтов --- */ + /* --- Переменные цветов и шрифтов (из локальной версии) --- */ --font-main: 'Roboto', sans-serif; --font-fancy: 'MedievalSharp', cursive; @@ -68,6 +68,14 @@ --timer-text-color: var(--turn-color); --timer-icon-color: #b0c4de; --timer-low-time-color: var(--damage-color); + + /* === Переменные для переключателя панелей (мобильный вид) - ИЗ СЕРВЕРНОЙ ВЕРСИИ === */ + --panel-switcher-bg: rgba(10, 12, 20, 0.9); + --panel-switcher-border: var(--panel-border); + --panel-switcher-button-bg: var(--button-bg); + --panel-switcher-button-text: var(--button-text); + --panel-switcher-button-active-bg: var(--accent-player); + --panel-switcher-button-active-text: #fff; } /* --- Базовые Стили и Сброс --- */ @@ -134,7 +142,7 @@ i { } -/* === Стили для Экранов Аутентификации и Настройки Игры === */ +/* === Стили для Экранов Аутентификации и Настройки Игры (из локальной версии) === */ .auth-game-setup-wrapper { width: 100%; max-width: 700px; @@ -146,8 +154,8 @@ i { color: var(--text-light); text-align: center; max-height: calc(100vh - 40px); - overflow-y: hidden; - position: relative; /* <<< Добавлено для позиционирования #user-info */ + overflow-y: hidden; /* Сохраняем из локальной */ + position: relative; /* <<< Добавлено для позиционирования #user-info (из локальной) */ } .auth-game-setup-wrapper h2, @@ -235,7 +243,7 @@ i { margin-top: 20px; text-align: left; max-height: 250px; - height: 100px; + height: 100px; /* Сохраняем из локальной */ overflow-y: scroll; padding: 10px 15px; background-color: rgba(0, 0, 0, 0.25); @@ -277,7 +285,7 @@ i { } #status-container { - height: 40px; + height: 40px; /* Сохраняем из локальной */ } #auth-message, @@ -309,7 +317,7 @@ i { margin-bottom: 20px; } -/* === ИЗМЕНЕНИЕ: Стили для #user-info === */ +/* === ИЗМЕНЕНИЕ: Стили для #user-info (из локальной версии) === */ #user-info { position: absolute; top: 10px; /* Отступ сверху */ @@ -423,7 +431,6 @@ label[for="char-almagest"] i { color: var(--accent-almagest); } - /* --- Основная Структура Игры (.game-wrapper) --- */ .game-wrapper { width: 100%; @@ -437,16 +444,51 @@ label[for="char-almagest"] i { overflow: hidden; } -/* === ИЗМЕНЕНИЕ: .game-header удален, стили для него больше не нужны === */ +/* === ИЗМЕНЕНИЕ: .game-header удален, стили для него больше не нужны (из локальной версии) === */ +/* Глобальные стили для кнопок переключения панелей - ИЗ СЕРВЕРНОЙ ВЕРСИИ */ +.panel-switcher-controls { + display: none; /* Скрыт по умолчанию для десктопа */ + flex-shrink: 0; + padding: 8px 5px; + background: var(--panel-switcher-bg); + border-bottom: 1px solid var(--panel-switcher-border); + gap: 10px; +} +.panel-switch-button { + flex: 1; + padding: 8px 10px; + font-size: 0.9em; + font-weight: bold; + text-transform: uppercase; + background: var(--panel-switcher-button-bg); + color: var(--panel-switcher-button-text); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 5px; + cursor: pointer; + transition: background-color 0.2s, color 0.2s, transform 0.1s; + display: flex; + align-items: center; + justify-content: center; +} +.panel-switch-button i { margin-right: 8px; } +.panel-switch-button:hover { filter: brightness(1.1); } +.panel-switch-button.active { + background: var(--panel-switcher-button-active-bg); + color: var(--panel-switcher-button-active-text); + box-shadow: 0 0 8px rgba(255,255,255,0.3); +} .battle-arena-container { flex-grow: 1; display: flex; gap: 10px; overflow: hidden; - /* === ИЗМЕНЕНИЕ: Добавляем верхний отступ, если .game-header был убран, а .game-wrapper виден === */ + /* === ИЗМЕНЕНИЕ: Добавляем верхний отступ, если .game-header был убран, а .game-wrapper виден (из локальной версии) === */ /* margin-top: 10px; /* или padding-top: 10px; на .game-wrapper, если нужно */ + /* === Изменения из серверной для работы переключения панелей === */ + position: relative; + min-height: 0; } .player-column, @@ -459,6 +501,7 @@ label[for="char-almagest"] i { overflow: hidden; } +/* Остальные стили панелей, кнопок, лога и т.д. из локальной версии */ .fighter-panel, .controls-panel-new, .battle-log-new { @@ -531,6 +574,7 @@ label[for="char-almagest"] i { padding-right: 5px; display: flex; flex-direction: column; + gap: 10px; /* Добавлено из серверной версии для консистентности */ min-height: 0; padding-top: 10px; margin-top: 0; @@ -547,7 +591,6 @@ label[for="char-almagest"] i { flex-shrink: 0; font-size: 1.4em; } - .stat-bar-container.health .bar-icon { color: var(--hp-color); } .stat-bar-container.mana .bar-icon { color: var(--mana-color); } .stat-bar-container.stamina .bar-icon { color: var(--stamina-color); } @@ -595,7 +638,6 @@ label[for="char-almagest"] i { white-space: nowrap; pointer-events: none; } - .health .bar-fill { background-color: var(--hp-color); } .mana .bar-fill { background-color: var(--mana-color); } .stamina .bar-fill { background-color: var(--stamina-color); } @@ -637,6 +679,7 @@ label[for="char-almagest"] i { font-size: 0.9em; display: flex; flex-direction: column; + gap: 8px; /* Добавлено из серверной версии для консистентности */ flex-shrink: 0; min-height: 3em; } @@ -688,7 +731,6 @@ label[for="char-almagest"] i { white-space: nowrap; vertical-align: baseline; } - .effect-buff { border-color: var(--heal-color); color: var(--heal-color); } .effect-debuff { border-color: var(--damage-color); color: var(--damage-color); } .effect-stun { border-color: var(--turn-color); color: var(--turn-color); } @@ -920,7 +962,6 @@ label[for="char-almagest"] i { border: 2px dashed var(--damage-color); animation: pulse-red-border 1s infinite ease-in-out; } - .ability-button.not-enough-resource:disabled { border-color: var(--damage-color); box-shadow: inset 0 0 8px rgba(255, 80, 80, 0.2), 0 3px 6px rgba(0, 0, 0, 0.2), inset 0 1px 3px rgba(0, 0, 0, 0.4); @@ -1023,7 +1064,6 @@ label[for="char-almagest"] i { #log-list li:hover { background-color: rgba(255, 255, 255, 0.03); } - .log-damage { color: var(--damage-color); font-weight: 500; } .log-heal { color: var(--heal-color); font-weight: 500; } .log-block { color: var(--block-color); font-style: italic; } @@ -1211,35 +1251,79 @@ label[for="char-almagest"] i { animation: shake-short 0.3s ease-in-out; } +/* --- Отзывчивость (Медиа-запросы) --- */ @media (max-width: 900px) { body { - height: auto; overflow-y: auto; + height: auto; min-height: 100vh; /* Из серверной, чтобы обеспечить высоту */ + overflow-y: auto; padding: 5px 0; font-size: 15px; justify-content: flex-start; } - .auth-game-setup-wrapper { max-height: none; padding-top: 60px; /* Отступ для #user-info */ } - /* === ИЗМЕНЕНИЕ: Адаптация #user-info === */ + .auth-game-setup-wrapper { + max-height: none; + padding-top: 60px; /* Отступ для #user-info из локальной */ + } + /* === ИЗМЕНЕНИЕ: Адаптация #user-info (из локальной версии) === */ #user-info { top: 5px; right: 10px; } #user-info p { font-size: 0.85em; } #logout-button { padding: 5px 10px !important; font-size: 0.75em !important; } /* === КОНЕЦ ИЗМЕНЕНИЯ === */ - .game-wrapper { padding: 5px; gap: 5px; height: auto; } - /* === ИЗМЕНЕНИЕ: game-header удален === */ - .battle-arena-container { flex-direction: column; height: auto; overflow: visible; /* margin-top: 0; /* Если ранее добавляли */ } - .player-column, .opponent-column { width: 100%; height: auto; overflow: visible; } - .fighter-panel, .controls-panel-new, .battle-log-new { - min-height: auto; height: auto; padding: 10px; - flex-grow: 0; flex-shrink: 1; + .game-wrapper { padding: 5px; gap: 5px; height: auto; min-height: calc(100vh - 10px); width: 100%; } /* min-height и width из серверной */ + /* === ИЗМЕНЕНИЕ: game-header удален (из локальной версии) === */ + + /* Показываем кнопки переключения на мобильных - ИЗ СЕРВЕРНОЙ ВЕРСИИ */ + .panel-switcher-controls { + display: flex; } - .controls-panel-new { min-height: 200px; } - .battle-log-new { height: auto; min-height: 150px; } + + .battle-arena-container { + /* flex-direction: column; height: auto; overflow: visible; - из локальной версии заменяется логикой ниже */ + gap: 0; /* Убираем отступ между колонками, т.к. они будут накладываться - ИЗ СЕРВЕРНОЙ ВЕРСИИ */ + /* position: relative; overflow: hidden; flex-grow: 1; min-height: 350px; - Эти стили уже есть глобально, но тут подтверждаем */ + } + + /* Стили для колонок при переключении - ИЗ СЕРВЕРНОЙ ВЕРСИИ */ + .player-column, + .opponent-column { + /* width: 100%; height: auto; overflow: visible; - из локальной версии заменяется логикой ниже */ + position: absolute; /* Для наложения */ + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow-y: auto; /* Прокрутка содержимого колонки */ + transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; + padding: 5px; /* Добавлено для отступов внутри колонок на мобильных */ + gap: 8px; /* Добавлено для отступов между панелями внутри колонок */ + } + + .player-column { transform: translateX(0); opacity: 1; z-index: 10; pointer-events: auto; } + .opponent-column { transform: translateX(100%); opacity: 0; z-index: 5; pointer-events: none; } + + .battle-arena-container.show-opponent-panel .player-column { transform: translateX(-100%); opacity: 0; z-index: 5; pointer-events: none; } + .battle-arena-container.show-opponent-panel .opponent-column { transform: translateX(0); opacity: 1; z-index: 10; pointer-events: auto; } + + + .fighter-panel, .controls-panel-new, .battle-log-new { + min-height: auto; /* Высота по контенту */ + height: auto; + padding: 10px; + flex-grow: 0; /* Локальное */ + flex-shrink: 1; /* Локальное */ + } + .fighter-panel { flex-shrink: 0; } /* Из серверной для panel-switcher */ + .fighter-panel .panel-content { flex-grow: 1; min-height: 0; } /* Из серверной для panel-switcher */ + + .controls-panel-new { min-height: 200px; flex-shrink: 0; } /* flex-shrink из серверной */ + .battle-log-new { height: auto; min-height: 150px; flex-shrink: 0; } /* flex-shrink из серверной */ + #log-list { max-height: 200px; } .abilities-grid { max-height: none; overflow-y: visible; padding-bottom: 8px; } .abilities-grid::after { display: none; } - .ability-list, .controls-layout { overflow: visible; } + .ability-list, .controls-layout { overflow: visible; } /* Локальное */ .fighter-name { font-size: 1.3em; } - .panel-content { margin-top: 10px; } + .panel-content { margin-top: 10px; /* Локальное, но теперь panel-content изменен для серверного panel-switcher, возможно, не нужно */ } .stat-bar-container .bar-icon { font-size: 1.2em; } .bar { height: 18px; } .effects-area, .effect { font-size: 0.85em; } @@ -1265,7 +1349,7 @@ label[for="char-almagest"] i { @media (max-width: 480px) { body { font-size: 14px; } - /* === ИЗМЕНЕНИЕ: Адаптация #user-info для мобильных === */ + /* === ИЗМЕНЕНИЕ: Адаптация #user-info для мобильных (из локальной версии) === */ .auth-game-setup-wrapper { padding-top: 50px; /* Еще немного места сверху */ } #user-info { top: 5px; @@ -1280,8 +1364,14 @@ label[for="char-almagest"] i { #logout-button i { margin-right: 3px; } /* === КОНЕЦ ИЗМЕНЕНИЯ === */ - /* === ИЗМЕНЕНИЕ: game-header удален === */ + /* Стили для panel-switcher на очень маленьких экранах - ИЗ СЕРВЕРНОЙ ВЕРСИИ */ + .panel-switch-button .button-text { display: none; } /* Скрываем текст, оставляем иконки */ + .panel-switch-button i { margin-right: 0; font-size: 1.2em; } + .panel-switch-button { padding: 6px 8px; } + + /* Локальные изменения */ .fighter-name { font-size: 1.2em; } + .avatar-image { max-width: 40px; } /* Из серверной, но не противоречит */ .abilities-grid { grid-template-columns: repeat(auto-fit, minmax(65px, 1fr)); gap: 5px; padding: 5px; padding-bottom: 10px; } .ability-button { font-size: 0.7em; padding: 4px; } .ability-button .ability-name { margin-bottom: 1px; } diff --git a/server/auth/authService.js b/server/auth/authService.js new file mode 100644 index 0000000..f0a6e79 --- /dev/null +++ b/server/auth/authService.js @@ -0,0 +1,133 @@ +// /server/auth/authService.js +const bcrypt = require('bcryptjs'); // Для хеширования паролей +const db = require('../core/db'); // Путь к вашему модулю для работы с базой данных (в папке core) + +const SALT_ROUNDS = 10; // Количество раундов для генерации соли bcrypt + +/** + * Регистрирует нового пользователя. + * @param {string} username - Имя пользователя. + * @param {string} password - Пароль пользователя. + * @returns {Promise} Объект с результатом: { success: boolean, message: string, userId?: number, username?: string } + */ +async function registerUser(username, password) { + console.log(`[AuthService DEBUG] registerUser called with username: "${username}"`); + + if (!username || !password) { + console.warn('[AuthService DEBUG] Validation failed: Username or password empty.'); + return { success: false, message: 'Имя пользователя и пароль не могут быть пустыми.' }; + } + if (password.length < 6) { + console.warn(`[AuthService DEBUG] Validation failed for "${username}": Password too short.`); + return { success: false, message: 'Пароль должен содержать не менее 6 символов.' }; + } + + try { + // Этап A: Проверка существующего пользователя + console.log(`[AuthService DEBUG] Stage A: Checking if user "${username}" exists...`); + // Предполагаем, что db.query возвращает массив, где первый элемент - это массив строк (результатов) + const [existingUsers] = await db.query('SELECT id FROM users WHERE username = ?', [username]); + console.log(`[AuthService DEBUG] Stage A: existingUsers query result length: ${existingUsers.length}`); + + if (existingUsers.length > 0) { + console.warn(`[AuthService DEBUG] Registration declined for "${username}": Username already taken.`); + return { success: false, message: 'Это имя пользователя уже занято.' }; + } + console.log(`[AuthService DEBUG] Stage A: Username "${username}" is available.`); + + // Этап B: Хеширование пароля + console.log(`[AuthService DEBUG] Stage B: Hashing password for user "${username}"...`); + const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); + console.log(`[AuthService DEBUG] Stage B: Password for "${username}" hashed successfully.`); + + // Этап C: Сохранение пользователя в БД + console.log(`[AuthService DEBUG] Stage C: Attempting to insert user "${username}" into DB...`); + // Предполагаем, что db.query для INSERT возвращает объект результата с insertId + const [result] = await db.query( + 'INSERT INTO users (username, password_hash) VALUES (?, ?)', + [username, hashedPassword] + ); + console.log(`[AuthService DEBUG] Stage C: DB insert result for "${username}":`, result); + + if (result && result.insertId) { + console.log(`[AuthService] Пользователь "${username}" успешно зарегистрирован с ID: ${result.insertId}.`); + return { + success: true, + message: 'Регистрация прошла успешно!', + userId: result.insertId, + username: username // Возвращаем и имя пользователя + }; + } else { + console.error(`[AuthService] Ошибка БД при регистрации пользователя "${username}": Запись не была вставлена или insertId отсутствует. Result:`, result); + return { success: false, message: 'Ошибка сервера при регистрации (данные не сохранены). Попробуйте позже.' }; + } + + } catch (error) { + console.error(`[AuthService] КРИТИЧЕСКАЯ ОШИБКА (catch block) при регистрации пользователя "${username}":`, error); + if (error.sqlMessage) { + console.error(`[AuthService] MySQL Error Message: ${error.sqlMessage}`); + console.error(`[AuthService] MySQL Error Code: ${error.code}`); + console.error(`[AuthService] MySQL Errno: ${error.errno}`); + } + return { success: false, message: 'Внутренняя ошибка сервера при регистрации.' }; + } +} + +/** + * Выполняет вход пользователя. + * @param {string} username - Имя пользователя. + * @param {string} password - Пароль пользователя. + * @returns {Promise} Объект с результатом: { success: boolean, message: string, userId?: number, username?: string } + */ +async function loginUser(username, password) { + console.log(`[AuthService DEBUG] loginUser called with username: "${username}"`); + + if (!username || !password) { + console.warn('[AuthService DEBUG] Login validation failed: Username or password empty.'); + return { success: false, message: 'Имя пользователя и пароль не могут быть пустыми.' }; + } + + try { + console.log(`[AuthService DEBUG] Searching for user "${username}" in DB...`); + const [users] = await db.query('SELECT id, username, password_hash FROM users WHERE username = ?', [username]); + console.log(`[AuthService DEBUG] DB query result for user "${username}" (length): ${users.length}`); + + if (users.length === 0) { + console.warn(`[AuthService DEBUG] Login failed: User "${username}" not found.`); + return { success: false, message: 'Неверное имя пользователя или пароль.' }; + } + + const user = users[0]; + console.log(`[AuthService DEBUG] User "${username}" found. ID: ${user.id}. Comparing password...`); + + const passwordMatch = await bcrypt.compare(password, user.password_hash); + console.log(`[AuthService DEBUG] Password comparison result for "${username}": ${passwordMatch}`); + + if (passwordMatch) { + console.log(`[AuthService] Пользователь "${user.username}" (ID: ${user.id}) успешно вошел в систему.`); + return { + success: true, + message: 'Вход выполнен успешно!', + userId: user.id, + username: user.username // Возвращаем имя пользователя + }; + } else { + console.warn(`[AuthService DEBUG] Login failed for user "${user.username}": Incorrect password.`); + return { success: false, message: 'Неверное имя пользователя или пароль.' }; + } + + } catch (error) { + console.error(`[AuthService] КРИТИЧЕСКАЯ ОШИБКА (catch block) при входе пользователя "${username}":`, error); + if (error.sqlMessage) { + console.error(`[AuthService] MySQL Error Message: ${error.sqlMessage}`); + console.error(`[AuthService] MySQL Error Code: ${error.code}`); + console.error(`[AuthService] MySQL Errno: ${error.errno}`); + } + return { success: false, message: 'Внутренняя ошибка сервера при входе.' }; + } +} + +module.exports = { + registerUser, + loginUser +}; \ No newline at end of file diff --git a/server/bc.js b/server/bc.js new file mode 100644 index 0000000..efcd344 --- /dev/null +++ b/server/bc.js @@ -0,0 +1,193 @@ +// /server/bc.js - Главный файл сервера Battle Club + +const express = require('express'); +const http = require('http'); +const { Server } = require('socket.io'); +const path = require('path'); + +// Импорт серверных модулей из их новых местоположений +const authService = require('./auth/authService'); // Сервис аутентификации +const GameManager = require('./game/GameManager'); // Менеджер игр +const db = require('./core/db'); // Модуль базы данных (для инициализации) +const GAME_CONFIG = require('./core/config'); // Глобальный конфиг игры +// data.js (теперь data/index.js) и gameLogic.js (теперь game/logic/index.js) +// импортируются внутри GameManager и GameInstance или их компонентов. + +const app = express(); +const server = http.createServer(app); + +// Настройка Socket.IO +const io = new Server(server, { + cors: { + origin: "https://pavel-chagovsky.com:3200", // Для разработки. В продакшене укажите домен клиента. + methods: ["GET", "POST"] + }, + // Можно настроить pingInterval и pingTimeout для более быстрого обнаружения дисконнектов + // pingInterval: 10000, // 10 секунд + // pingTimeout: 5000, // 5 секунд (клиент должен ответить в течение этого времени) +}); + +// Раздача статических файлов из папки 'public' +// __dirname будет указывать на папку server/, поэтому нужно подняться на уровень выше +app.use(express.static(path.join(__dirname, '..', 'public'))); + +// Создаем экземпляр GameManager +const gameManager = new GameManager(io); + +// Хранилище информации о залогиненных пользователях по socket.id +// (Временное решение, в продакшене лучше использовать Redis или БД для сессий) +const loggedInUsers = {}; // { socket.id: { userId: ..., username: ... } } + +// Обработка подключений Socket.IO +io.on('connection', (socket) => { + console.log(`[Socket.IO] Пользователь подключился: ${socket.id}`); + + // Привязываем user data к сокету (пока пустые, заполняются при логине) + socket.userData = null; // { userId: ..., username: ... } + + // --- Обработчики событий Аутентификации --- + socket.on('register', async (data) => { + console.log(`[Socket.IO] Register attempt for username: "${data?.username}" from ${socket.id}`); + const result = await authService.registerUser(data?.username, data?.password); + if (result.success) { + console.log(`[Socket.IO] Registration successful for ${result.username} (${result.userId})`); + } else { + console.warn(`[Socket.IO] Registration failed for "${data?.username}": ${result.message}`); + } + socket.emit('registerResponse', result); + }); + + socket.on('login', async (data) => { + console.log(`[Socket.IO] Login attempt for username: "${data?.username}" from ${socket.id}`); + const result = await authService.loginUser(data?.username, data?.password); + if (result.success && result.userId && result.username) { // Убедимся, что userId и username есть + console.log(`[Socket.IO] Login successful for ${result.username} (${result.userId}). Assigning to socket ${socket.id}.`); + socket.userData = { userId: result.userId, username: result.username }; + loggedInUsers[socket.id] = socket.userData; // Сохраняем для быстрого доступа, если нужно + + // После успешного логина, просим GameManager проверить, не был ли этот пользователь в игре + if (gameManager && typeof gameManager.handleRequestGameState === 'function') { + gameManager.handleRequestGameState(socket, result.userId); + } + } else { + console.warn(`[Socket.IO] Login failed for "${data?.username}": ${result.message}`); + socket.userData = null; + if (loggedInUsers[socket.id]) delete loggedInUsers[socket.id]; + } + socket.emit('loginResponse', result); // Отправляем результат клиенту + }); + + socket.on('logout', () => { + const username = socket.userData?.username || 'UnknownUser'; + const userId = socket.userData?.userId; + console.log(`[Socket.IO] Logout request from user ${username} (ID: ${userId}, Socket: ${socket.id})`); + + if (gameManager && typeof gameManager.handleDisconnect === 'function' && userId) { + // Уведомляем GameManager о "дисконнекте" этого пользователя из его игры, если он там был. + // handleDisconnect использует identifier (userId в данном случае) для поиска игры. + // Передаем socket.id на случай, если игра была AI и identifier был socket.id (хотя при logout должен быть userId). + gameManager.handleDisconnect(socket.id, userId); + } + + if (loggedInUsers[socket.id]) { + delete loggedInUsers[socket.id]; + } + socket.userData = null; + // Клиент сам обработает UI после logout (например, покажет экран логина) + // Можно отправить подтверждение, но обычно не требуется: socket.emit('logoutResponse', { success: true }); + console.log(`[Socket.IO] User ${username} (Socket: ${socket.id}) logged out.`); + }); + + // --- Обработчики событий Управления Играми --- + // Все эти события делегируются в GameManager + + socket.on('createGame', (data) => { + const identifier = socket.userData?.userId || socket.id; // userId для залогиненных, socket.id для гостей (AI игра) + const mode = data?.mode || 'ai'; + + if (mode === 'pvp' && !socket.userData) { + socket.emit('gameError', { message: 'Необходимо войти в систему для создания PvP игры.' }); + return; + } + console.log(`[Socket.IO] Create Game from ${socket.userData?.username || socket.id} (ID: ${identifier}). Mode: ${mode}, Char: ${data?.characterKey}`); + gameManager.createGame(socket, mode, data?.characterKey, identifier); + }); + + socket.on('joinGame', (data) => { + if (!socket.userData?.userId) { + socket.emit('gameError', { message: 'Необходимо войти для присоединения к PvP игре.' }); + return; + } + console.log(`[Socket.IO] Join Game from ${socket.userData.username} (ID: ${socket.userData.userId}). GameID: ${data?.gameId}`); + gameManager.joinGame(socket, data?.gameId, socket.userData.userId); + }); + + socket.on('findRandomGame', (data) => { + if (!socket.userData?.userId) { + socket.emit('gameError', { message: 'Необходимо войти для поиска случайной PvP игры.' }); + return; + } + console.log(`[Socket.IO] Find Random Game from ${socket.userData.username} (ID: ${socket.userData.userId}). PrefChar: ${data?.characterKey}`); + gameManager.findAndJoinRandomPvPGame(socket, data?.characterKey, socket.userData.userId); + }); + + socket.on('requestPvPGameList', () => { + // console.log(`[Socket.IO] Request PvP Game List from ${socket.userData?.username || socket.id}`); + const availableGames = gameManager.getAvailablePvPGamesListForClient(); + socket.emit('availablePvPGamesList', availableGames); + }); + + socket.on('requestGameState', () => { + if (!socket.userData?.userId) { + // console.log(`[Socket.IO] Request Game State from unauthenticated socket ${socket.id}.`); + socket.emit('gameNotFound', { message: 'Необходимо войти для восстановления игры.' }); + return; + } + // console.log(`[Socket.IO] Request Game State from ${socket.userData.username} (ID: ${socket.userData.userId}).`); + gameManager.handleRequestGameState(socket, socket.userData.userId); + }); + + // --- Обработчик события Игрового Действия --- + socket.on('playerAction', (actionData) => { + const identifier = socket.userData?.userId || socket.id; // Идентификатор для GameManager + // console.log(`[Socket.IO] Player Action from ${identifier} (socket ${socket.id}):`, actionData); + gameManager.handlePlayerAction(identifier, actionData); + }); + + // --- Обработчик отключения сокета --- + socket.on('disconnect', (reason) => { + const identifier = socket.userData?.userId || socket.id; + console.log(`[Socket.IO] Пользователь отключился: ${socket.id} (Причина: ${reason}). Identifier: ${identifier}`); + + gameManager.handleDisconnect(socket.id, identifier); // Передаем и socketId, и identifier + + if (loggedInUsers[socket.id]) { + delete loggedInUsers[socket.id]; + } + // socket.userData очистится автоматически при уничтожении объекта socket + }); +}); + +// Запуск HTTP сервера +const PORT = process.env.BC_INTERNAL_PORT || 3200; // Внутренний порт для bc.js +const HOSTNAME = '127.0.0.1'; // Слушать ТОЛЬКО на localhost + +server.listen(PORT, HOSTNAME, () => { // Явно указываем HOSTNAME + console.log(`Battle Club HTTP Application Server running at http://${HOSTNAME}:${PORT}`); + console.log(`This server should only be accessed locally by the reverse proxy.`); + console.log(`Serving static files from: ${path.join(__dirname, 'public')}`); +}); + + +// Обработка необработанных промис-ошибок +process.on('unhandledRejection', (reason, promise) => { + console.error('[Server FATAL] Unhandled Rejection at:', promise, 'reason:', reason); + // В продакшене здесь может быть более сложная логика или перезапуск процесса + // process.exit(1); +}); + +process.on('uncaughtException', (err) => { + console.error('[Server FATAL] Uncaught Exception:', err); + // Критическая ошибка, обычно требует перезапуска приложения + process.exit(1); // Аварийное завершение процесса +}); \ No newline at end of file diff --git a/server_modules/config.js b/server/core/config.js similarity index 91% rename from server_modules/config.js rename to server/core/config.js index a07d8af..864355a 100644 --- a/server_modules/config.js +++ b/server/core/config.js @@ -1,117 +1,111 @@ -// /server_modules/config.js - -const GAME_CONFIG = { - // --- Баланс Игры --- - BLOCK_DAMAGE_REDUCTION: 0.5, // Множитель урона при блоке (0.5 = 50% снижение) - DAMAGE_VARIATION_MIN: 0.9, // Минимальный множитель урона (0.9 = 90%) - DAMAGE_VARIATION_RANGE: 0.2, // Диапазон вариации урона (0.2 = от 90% до 110%) - HEAL_VARIATION_MIN: 0.8, // Минимальный множитель лечения (0.8 = 80%) - HEAL_VARIATION_RANGE: 0.4, // Диапазон вариации лечения (0.4 = от 80% до 120%) - NATURE_STRENGTH_MANA_REGEN: 10, // Количество маны, восстанавливаемое "Силой природы" (и ее аналогом) - - // --- Условия ИИ и Игрока --- - OPPONENT_HEAL_THRESHOLD_PERCENT: 50, // Процент HP Баларда, НИЖЕ которого он будет пытаться лечиться (для AI) - PLAYER_MERCY_TAUNT_THRESHOLD_PERCENT: 60, // Процент HP Баларда, НИЖЕ которого Елена использует "доминирующие" насмешки (для AI/текстов) - PLAYER_HP_BLEED_THRESHOLD_PERCENT: 60, // % HP Елены, НИЖЕ которого Балард предпочитает Кровотечение Безмолвию (для AI) - BALARD_MANA_DRAIN_HIGH_MANA_THRESHOLD: 60, // % Маны Елены, ВЫШЕ которого Балард может использовать "Похищение Света" (для AI) - - // --- Способности Баларда (AI) - Конфигурация --- - SILENCE_DURATION: 3, // Длительность Безмолвия в ходах Елены (после хода Баларда) - SILENCE_SUCCESS_RATE: 0.7, // Шанс успеха наложения Безмолвия (70%) - BALARD_SILENCE_ABILITY_COST: 15, // Стоимость "Эха Безмолвия" в Ярости - BALARD_SILENCE_INTERNAL_COOLDOWN: 5, // К-во ходов Баларда КД ПОСЛЕ успешного использования Безмолвия - // BALARD_BLEED_COST: 15, // Если будете добавлять способность Кровотечение - // BALARD_BLEED_POWER: 5, - // BALARD_BLEED_DURATION: 2, - // BALARD_BLEED_COOLDOWN: 3, - - // --- Таймер Хода --- - TURN_DURATION_SECONDS: 60, // Длительность хода в секундах - TURN_DURATION_MS: 60 * 1000, // Длительность хода в миллисекундах - TIMER_UPDATE_INTERVAL_MS: 1000, // Интервал обновления таймера на клиенте (в мс) - - // --- Идентификаторы и Типы --- - PLAYER_ID: 'player', // Технический идентификатор для слота 'Игрок 1' - OPPONENT_ID: 'opponent', // Технический идентификатор для слота 'Игрок 2' / 'Противник' - ACTION_TYPE_HEAL: 'heal', - ACTION_TYPE_DAMAGE: 'damage', - ACTION_TYPE_BUFF: 'buff', - ACTION_TYPE_DISABLE: 'disable', // Тип для контроля (безмолвие, стан и т.п.) - ACTION_TYPE_DEBUFF: 'debuff', // Тип для ослаблений, DoT и т.п. - ACTION_TYPE_DRAIN: 'drain', // Тип для Похищения Света - - // --- Строки для UI (могут быть полезны и на сервере для логов) --- - STATUS_READY: 'Готов(а)', // Сделал универсальным - STATUS_BLOCKING: 'Защищается', - - // --- Типы Логов (для CSS классов на клиенте, и для структурирования на сервере) --- - LOG_TYPE_INFO: 'info', - LOG_TYPE_DAMAGE: 'damage', - LOG_TYPE_HEAL: 'heal', - LOG_TYPE_TURN: 'turn', - LOG_TYPE_SYSTEM: 'system', - LOG_TYPE_BLOCK: 'block', - LOG_TYPE_EFFECT: 'effect', - - // --- CSS Классы (в основном для клиента, но константы могут быть полезны для согласованности) --- - CSS_CLASS_BLOCKING: 'blocking', - CSS_CLASS_NOT_ENOUGH_RESOURCE: 'not-enough-resource', - CSS_CLASS_BUFF_IS_ACTIVE: 'buff-is-active', - CSS_CLASS_ATTACK_BUFFED: 'attack-buffed', - CSS_CLASS_SHAKING: 'is-shaking', - CSS_CLASS_CASTING_PREFIX: 'is-casting-', // Например: is-casting-fireball - CSS_CLASS_HIDDEN: 'hidden', - CSS_CLASS_ABILITY_BUTTON: 'ability-button', - CSS_CLASS_ABILITY_SILENCED: 'is-silenced', - CSS_CLASS_ABILITY_ON_COOLDOWN: 'is-on-cooldown', // Для отображения кулдауна - - // --- Задержки (в миллисекундах) --- - // Эти задержки теперь в основном будут управляться сервером при отправке событий или планировании AI ходов - DELAY_OPPONENT_TURN: 1200, // Задержка перед ходом AI - DELAY_AFTER_PLAYER_ACTION: 500, // Сервер может использовать это для паузы перед следующим событием - // DELAY_AFTER_BLOCK: 500, // Менее релевантно для сервера напрямую - DELAY_INIT: 100, // Для клиентской инициализации, если нужна - DELAY_BEFORE_VICTORY_MODAL: 1500, // Для клиента, после получения gameOver - MODAL_TRANSITION_DELAY: 10, // Для анимации модалки на клиенте - - // --- Длительности анимаций (в миллисекундах, в основном для клиента, но сервер может знать для таймингов) --- - ANIMATION_SHAKE_DURATION: 400, - ANIMATION_CAST_DURATION: 600, - ANIMATION_DISSOLVE_DURATION: 6000, // var(--dissolve-duration) из CSS - - // --- Внутренние ID способностей (для удобства в логике, нужны и на сервере, и на клиенте) --- - // Игрока (Елена) - ABILITY_ID_HEAL: 'heal', // Малое Исцеление - ABILITY_ID_FIREBALL: 'fireball', // Огненный Шар - ABILITY_ID_NATURE_STRENGTH: 'naturesStrength', // Сила Природы - ABILITY_ID_DEFENSE_AURA: 'defenseAura', // Аура Защиты - ABILITY_ID_HYPNOTIC_GAZE: 'hypnoticGaze', // Гипнотический взгляд - ABILITY_ID_SEAL_OF_WEAKNESS: 'sealOfWeakness', // Печать Слабости - - // Противника (Балард - AI) - ABILITY_ID_BALARD_HEAL: 'darkPatronage', // Покровительство Тьмы - ABILITY_ID_BALARD_SILENCE: 'echoesOfSilence', // Эхо Безмолвия - ABILITY_ID_BALARD_MANA_DRAIN: 'manaDrainHeal', // Похищение Света - // ABILITY_ID_BALARD_BLEED: 'balardBleed', // Если будете добавлять - - // Противника (Альмагест - PvP - зеркало Елены) - ABILITY_ID_ALMAGEST_HEAL: 'darkHeal', // Темное Восстановление (Аналог heal) - ABILITY_ID_ALMAGEST_DAMAGE: 'shadowBolt', // Теневой Сгусток (Аналог fireball) - ABILITY_ID_ALMAGEST_BUFF_ATTACK: 'shadowEmpowerment', // Усиление Тьмой (Аналог naturesStrength) - ABILITY_ID_ALMAGEST_BUFF_DEFENSE: 'voidShield', // Щит Пустоты (Аналог defenseAura) - ABILITY_ID_ALMAGEST_DISABLE: 'mindShatter', // Раскол Разума (Аналог hypnoticGaze) - ABILITY_ID_ALMAGEST_DEBUFF: 'curseOfDecay', // Проклятие Увядания (Аналог sealOfWeakness) -}; - -// Для использования в Node.js модулях (например, server_modules/gameInstance.js) -if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { - module.exports = GAME_CONFIG; -} -// Для использования в браузере (если этот файл подключается напрямую через