Сохранение локальной работы перед синхронизацией с перезаписанной историей

This commit is contained in:
svoboda200786@gmail.com 2025-05-21 16:12:02 +03:00
parent 550212e18d
commit 6e8dce7d71
5 changed files with 971 additions and 13 deletions

View File

@ -41,8 +41,6 @@
</form>
</div>
<!-- Место для #user-info было здесь, но теперь оно выше для стилизации -->
<div id="game-setup" style="display: none;"> <!-- Секция Настройки Игры (после логина) -->
<h2>Настройка Игры</h2>
<div>
@ -77,14 +75,6 @@
<div class="game-wrapper" style="display: none;"> <!-- Игровая арена, изначально скрыта -->
<!-- === ИЗМЕНЕНИЕ: Удален game-header === -->
<!-- <header class="game-header">
<h1><span class="title-player">Игрок 1</span> <span class="separator"><i class="fas fa-fist-raised"></i></span> <span class="title-opponent">Игрок 2</span></h1>
</header> -->
<!-- === КОНЕЦ ИЗМЕНЕНИЯ === -->
<!-- === БЛОК: Контролы переключения панелей (для мобильных) - ИЗ СЕРВЕРНОЙ ВЕРСИИ === -->
<!-- Инлайновый стиль style="display: none;" УДАЛЕН. CSS управляет видимостью. -->
<div class="panel-switcher-controls">
<button id="show-player-panel-btn" class="panel-switch-button active">
<i class="fas fa-user"></i> <span class="button-text">Игрок</span>
@ -93,7 +83,6 @@
<i class="fas fa-ghost"></i> <span class="button-text">Противник</span>
</button>
</div>
<!-- === КОНЕЦ БЛОКА === -->
<main class="battle-arena-container">
<!-- Колонка Игрока (Панель 1 в UI) -->
@ -232,8 +221,15 @@
</div>
</div> <!-- Конец .game-wrapper -->
<!-- Библиотека Socket.IO клиента -->
<script src="/socket.io/socket.io.js"></script>
<script src="./js/ui.js"></script> <!-- ui.js теперь перед client.js -->
<script src="./js/client.js"></script>
<!-- Ваш скрипт для UI, который может создавать глобальные объекты или функции -->
<!-- Он должен быть загружен до main.js, если main.js ожидает window.gameUI -->
<script src="./js/ui.js"></script>
<!-- Ваш основной клиентский скрипт, теперь как модуль -->
<script type="module" src="./js/main.js"></script>
</body>
</html>

128
public/js/auth.js Normal file
View File

@ -0,0 +1,128 @@
// /public/js/auth.js
// Эта функция будет вызвана из main.js и получит необходимые зависимости
export function initAuth(dependencies) {
const { socket, clientState, ui } = dependencies;
const { loginForm, registerForm, logoutButton } = ui.elements; // Получаем нужные DOM элементы
// --- Обработчики событий DOM ---
if (registerForm) {
registerForm.addEventListener('submit', (e) => {
e.preventDefault();
const usernameInput = document.getElementById('register-username');
const passwordInput = document.getElementById('register-password');
if (!usernameInput || !passwordInput) return;
const username = usernameInput.value;
const password = passwordInput.value;
// Блокируем кнопки на время запроса
const regButton = registerForm.querySelector('button');
const loginButton = loginForm ? loginForm.querySelector('button') : null;
if (regButton) regButton.disabled = true;
if (loginButton) loginButton.disabled = true;
ui.setAuthMessage('Регистрация...');
socket.emit('register', { username, password });
});
}
if (loginForm) {
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
const usernameInput = document.getElementById('login-username');
const passwordInput = document.getElementById('login-password');
if (!usernameInput || !passwordInput) return;
const username = usernameInput.value;
const password = passwordInput.value;
// Блокируем кнопки на время запроса
const loginButton = loginForm.querySelector('button');
const regButton = registerForm ? registerForm.querySelector('button') : null;
if (loginButton) loginButton.disabled = true;
if (regButton) regButton.disabled = true;
ui.setAuthMessage('Вход...');
socket.emit('login', { username, password });
});
}
if (logoutButton) {
logoutButton.addEventListener('click', () => {
logoutButton.disabled = true;
socket.emit('logout');
// Обновляем состояние клиента немедленно, не дожидаясь ответа сервера (опционально)
clientState.isLoggedIn = false;
clientState.loggedInUsername = '';
clientState.myUserId = null;
// isInGame и другие игровые переменные сбросятся в ui.showAuthScreen()
// disableGameControls() также будет вызван опосредованно через showAuthScreen -> resetGameVariables
ui.showAuthScreen(); // Показываем экран логина
ui.setGameStatusMessage("Вы вышли из системы."); // Используем gameStatusMessage для общего статуса после выхода
// ui.setAuthMessage("Вы вышли из системы."); // или authMessage, если он виден
// Кнопка разблокируется при следующем показе userInfoDiv или можно здесь
// logoutButton.disabled = false; // но лучше, чтобы UI сам управлял этим при показе
});
}
// --- Обработчики событий Socket.IO ---
socket.on('registerResponse', (data) => {
ui.setAuthMessage(data.message, !data.success);
if (data.success && registerForm) {
registerForm.reset(); // Очищаем форму при успехе
}
// Разблокируем кнопки
if (registerForm) {
const regButton = registerForm.querySelector('button');
if (regButton) regButton.disabled = false;
}
if (loginForm) {
const loginButton = loginForm.querySelector('button');
if (loginButton) loginButton.disabled = false;
}
});
socket.on('loginResponse', (data) => {
if (data.success) {
clientState.isLoggedIn = true;
clientState.loggedInUsername = data.username;
clientState.myUserId = data.userId;
ui.setAuthMessage(""); // Очищаем сообщение об аутентификации
ui.showGameSelectionScreen(data.username); // Показываем экран выбора игры
// Запрос gameState при успешном логине и реконнекте теперь обрабатывается в main.js
// если пользователь уже был залогинен при 'connect'
} else {
clientState.isLoggedIn = false;
clientState.loggedInUsername = '';
clientState.myUserId = null;
ui.setAuthMessage(data.message, true); // Показываем ошибку
}
// Разблокируем кнопки
if (registerForm) {
const regButton = registerForm.querySelector('button');
if (regButton) regButton.disabled = false;
}
if (loginForm) {
const loginButton = loginForm.querySelector('button');
if (loginButton) loginButton.disabled = false;
}
// Убедимся, что кнопка logout активна, если пользователь успешно вошел
if (logoutButton && clientState.isLoggedIn) {
logoutButton.disabled = false;
}
});
// Примечание: событие 'logout' от сервера обычно не требует специального обработчика здесь,
// так как клиент сам инициирует выход и обновляет UI.
// Если сервер принудительно разлогинивает, то такой обработчик может понадобиться.
// socket.on('forceLogout', (data) => {
// clientState.isLoggedIn = false;
// // ...
// ui.showAuthScreen();
// ui.setAuthMessage(data.message || "Вы были разлогинены сервером.");
// });
}

199
public/js/gameSetup.js Normal file
View File

@ -0,0 +1,199 @@
// /public/js/gameSetup.js
export function initGameSetup(dependencies) {
const { socket, clientState, ui } = dependencies;
const {
createAIGameButton, createPvPGameButton, joinPvPGameButton,
findRandomPvPGameButton, gameIdInput, availableGamesDiv, pvpCharacterRadios
} = ui.elements;
// --- Вспомогательные функции ---
function getSelectedCharacterKey() {
let selectedKey = 'elena'; // Значение по умолчанию
if (pvpCharacterRadios) {
pvpCharacterRadios.forEach(radio => {
if (radio.checked) {
selectedKey = radio.value;
}
});
}
return selectedKey;
}
function updateAvailableGamesList(games) {
if (!availableGamesDiv) return;
availableGamesDiv.innerHTML = '<h3>Доступные PvP игры:</h3>';
if (games && games.length > 0) {
const ul = document.createElement('ul');
games.forEach(game => {
if (game && game.id) {
const li = document.createElement('li');
// Отображаем только часть ID для краткости
li.textContent = `ID: ${game.id.substring(0, 8)}... - ${game.status || 'Ожидает игрока'}`;
const joinBtn = document.createElement('button');
joinBtn.textContent = 'Присоединиться';
joinBtn.dataset.gameId = game.id;
// Деактивация кнопки "Присоединиться" для своих игр
if (clientState.isLoggedIn && clientState.myUserId && game.ownerIdentifier === clientState.myUserId) {
joinBtn.disabled = true;
joinBtn.title = "Вы не можете присоединиться к своей же ожидающей игре.";
} else {
joinBtn.disabled = false;
}
joinBtn.addEventListener('click', (e) => {
if (!clientState.isLoggedIn) {
ui.setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true);
return;
}
if (e.target.disabled) return; // Не обрабатывать клик по отключенной кнопке
ui.disableSetupButtons(); // Блокируем все кнопки выбора игры
socket.emit('joinGame', { gameId: e.target.dataset.gameId });
ui.setGameStatusMessage(`Присоединение к игре ${e.target.dataset.gameId.substring(0, 8)}...`);
});
li.appendChild(joinBtn);
ul.appendChild(li);
}
});
availableGamesDiv.appendChild(ul);
} else {
availableGamesDiv.innerHTML += '<p>Нет доступных игр. Создайте свою!</p>';
}
ui.enableSetupButtons(); // Включаем основные кнопки создания/поиска после обновления списка
}
// --- Обработчики событий DOM ---
if (createAIGameButton) {
createAIGameButton.addEventListener('click', () => {
if (!clientState.isLoggedIn) {
ui.setGameStatusMessage("Пожалуйста, войдите, чтобы создать игру.", true);
return;
}
ui.disableSetupButtons();
// Для AI игры персонаж может быть фиксированным или выбираемым
// В вашем client.js был 'elena', оставим так
socket.emit('createGame', { mode: 'ai', characterKey: 'elena' });
ui.setGameStatusMessage("Создание игры против AI...");
});
}
if (createPvPGameButton) {
createPvPGameButton.addEventListener('click', () => {
if (!clientState.isLoggedIn) {
ui.setGameStatusMessage("Пожалуйста, войдите, чтобы создать игру.", true);
return;
}
ui.disableSetupButtons();
const characterKey = getSelectedCharacterKey();
socket.emit('createGame', { mode: 'pvp', characterKey: characterKey });
ui.setGameStatusMessage("Создание PvP игры...");
});
}
if (joinPvPGameButton) {
joinPvPGameButton.addEventListener('click', () => {
if (!clientState.isLoggedIn) {
ui.setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true);
return;
}
const gameId = gameIdInput ? gameIdInput.value.trim() : '';
if (gameId) {
ui.disableSetupButtons();
socket.emit('joinGame', { gameId: gameId });
ui.setGameStatusMessage(`Присоединение к игре ${gameId}...`);
} else {
ui.setGameStatusMessage("Введите ID игры, чтобы присоединиться.", true);
}
});
}
if (findRandomPvPGameButton) {
findRandomPvPGameButton.addEventListener('click', () => {
if (!clientState.isLoggedIn) {
ui.setGameStatusMessage("Пожалуйста, войдите, чтобы найти игру.", true);
return;
}
ui.disableSetupButtons();
const characterKey = getSelectedCharacterKey();
socket.emit('findRandomGame', { characterKey: characterKey });
ui.setGameStatusMessage("Поиск случайной PvP игры...");
});
}
// --- Обработчики событий Socket.IO ---
// gameCreated: Сервер присылает это после успешного createGame
// Это событие может быть важным для установки currentGameId и myPlayerId
// перед тем, как придет gameStarted или waitingForOpponent.
socket.on('gameCreated', (data) => {
if (!clientState.isLoggedIn) return; // Игнорируем, если не залогинены
console.log('[GameSetup] Game created by this client:', data);
clientState.currentGameId = data.gameId;
clientState.myPlayerId = data.yourPlayerId; // Сервер должен прислать роль создателя
ui.updateGlobalWindowVariablesForUI(); // Обновляем глобальные переменные для ui.js
// Если это PvP игра, обычно сервер следом пришлет 'waitingForOpponent'
// Если AI, то сразу 'gameStarted'
// На этом этапе UI не меняем кардинально, ждем следующего события.
// ui.setGameStatusMessage(`Игра ${data.gameId.substring(0,8)} создана. Ожидание...`);
// Кнопки уже должны быть заблокированы ui.disableSetupButtons()
});
socket.on('availablePvPGamesList', (games) => {
if (!clientState.isLoggedIn) return; // Только для залогиненных пользователей
updateAvailableGamesList(games);
});
// Это событие приходит, когда игрок искал случайную игру, но свободных не было,
// и сервер создал новую игру для этого игрока.
socket.on('noPendingGamesFound', (data) => {
if (!clientState.isLoggedIn) return;
ui.setGameStatusMessage(data.message || "Свободных игр не найдено. Создана новая для вас. Ожидание оппонента...");
updateAvailableGamesList([]); // Очищаем список доступных игр, так как мы уже в созданной
// clientState.currentGameId и clientState.myPlayerId должны были быть установлены
// через событие 'gameCreated', которое сервер должен прислать перед 'noPendingGamesFound'.
// Если 'gameCreated' не присылается в этом сценарии, нужно будет получать gameId и yourPlayerId из data 'noPendingGamesFound'.
if (data.gameId) clientState.currentGameId = data.gameId;
if (data.yourPlayerId) clientState.myPlayerId = data.yourPlayerId;
ui.updateGlobalWindowVariablesForUI();
clientState.isInGame = false; // Мы еще не в активной фазе боя, а в ожидании
// ui.disableGameControls(); // Будет вызвано из gameplay.js, если он уже был инициализирован
// или неактуально, так как мы не на игровом экране
ui.disableSetupButtons(); // Мы в ожидающей игре, кнопки выбора не нужны
// Можно оставить кнопку "Создать PvP" активной для возможности "отменить" и создать другую,
// но это усложнит логику. Пока блокируем все.
// Если есть таймер, его нужно сбросить или показать "Ожидание"
if (window.gameUI?.updateTurnTimerDisplay) {
window.gameUI.updateTurnTimerDisplay(null, false, 'pvp'); // Таймер неактивен в ожидании
}
});
// waitingForOpponent: Когда PvP игра создана и ожидает второго игрока
socket.on('waitingForOpponent', () => {
if (!clientState.isLoggedIn) return;
ui.setGameStatusMessage("Ожидание присоединения оппонента...");
// clientState.isInGame = false; // Уже должно быть false или будет установлено при gameStarted
// ui.disableGameControls(); // не на игровом экране
ui.disableSetupButtons(); // Блокируем кнопки создания/присоединения
// Можно оставить кнопку "Создать PvP" или добавить кнопку "Отменить ожидание",
// но это требует дополнительной логики на сервере и клиенте.
// if (ui.elements.createPvPGameButton) ui.elements.createPvPGameButton.disabled = false;
if (window.gameUI?.updateTurnTimerDisplay) {
window.gameUI.updateTurnTimerDisplay(null, false, 'pvp');
}
});
// Примечание: gameNotFound обрабатывается в main.js, так как он может сбросить
// игрока на экран выбора игры или даже на экран логина.
}

339
public/js/gameplay.js Normal file
View File

@ -0,0 +1,339 @@
// /public/js/gameplay.js
export function initGameplay(dependencies) {
const { socket, clientState, ui } = dependencies;
// Элементы управления боем обычно находятся внутри gameWrapper и управляются через ui.js,
// но нам могут понадобиться ссылки на кнопки для привязки событий, если они не привязаны в ui.js
// или если ui.js не экспортирует их напрямую.
// В данном случае, attackButton и abilitiesGrid есть в client.js, так что получим их.
// ui.elements из main.js содержит returnToMenuButton
const { returnToMenuButton } = ui.elements;
// Получаем ссылки на кнопки атаки и способностей напрямую, как было в client.js
// или, если бы ui.js их экспортировал, можно было бы через window.gameUI.uiElements
const attackButton = document.getElementById('button-attack');
const abilitiesGrid = document.getElementById('abilities-grid');
// --- Вспомогательные функции ---
function enableGameControls(enableAttack = true, enableAbilities = true) {
if (attackButton) attackButton.disabled = !enableAttack;
if (abilitiesGrid) {
// Предполагаем, что GAME_CONFIG доступен глобально или его нужно передать
const config = window.GAME_CONFIG || {};
const cls = 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);
}
// Эта функция была в client.js, переносим сюда
function initializeAbilityButtons() {
if (!abilitiesGrid || !window.gameUI || !window.GAME_CONFIG) {
if (abilitiesGrid) abilitiesGrid.innerHTML = '<p class="placeholder-text">Ошибка загрузки способностей.</p>';
return;
}
abilitiesGrid.innerHTML = ''; // Очищаем предыдущие кнопки
const config = window.GAME_CONFIG;
// Используем данные из clientState, которые были обновлены из событий сервера
const abilitiesToDisplay = clientState.playerAbilitiesServer;
const baseStatsForResource = clientState.playerBaseStatsServer;
if (!abilitiesToDisplay || abilitiesToDisplay.length === 0 || !baseStatsForResource) {
abilitiesGrid.innerHTML = '<p class="placeholder-text">Нет доступных способностей.</p>';
return;
}
const resourceName = baseStatsForResource.resourceName || "Ресурс";
const abilityButtonClass = config.CSS_CLASS_ABILITY_BUTTON || 'ability-button';
abilitiesToDisplay.forEach(ability => {
const button = document.createElement('button');
button.id = `ability-btn-${ability.id}`;
button.classList.add(abilityButtonClass);
button.dataset.abilityId = ability.id;
let cooldown = ability.cooldown; // Это базовый КД из данных персонажа
let cooldownText = (typeof cooldown === 'number' && cooldown > 0) ? ` (КД: ${cooldown} х.)` : "";
let title = `${ability.name} (${ability.cost} ${resourceName})${cooldownText} - ${ability.description || 'Нет описания'}`;
button.setAttribute('title', title);
const nameSpan = document.createElement('span');
nameSpan.classList.add('ability-name');
nameSpan.textContent = ability.name;
button.appendChild(nameSpan);
const descSpan = document.createElement('span');
descSpan.classList.add('ability-desc');
descSpan.textContent = `(${ability.cost} ${resourceName})`;
button.appendChild(descSpan);
const cdDisplay = document.createElement('span');
cdDisplay.classList.add('ability-cooldown-display'); // Для отображения текущего КД
cdDisplay.style.display = 'none'; // Скрыт по умолчанию
button.appendChild(cdDisplay);
button.addEventListener('click', handleAbilityButtonClick);
abilitiesGrid.appendChild(button);
});
const placeholder = abilitiesGrid.querySelector('.placeholder-text');
if (placeholder) placeholder.remove();
// После инициализации кнопок, их состояние (disabled/enabled) будет управляться window.gameUI.updateUI()
}
function handleAbilityButtonClick(event) {
const abilityId = event.currentTarget.dataset.abilityId;
if (clientState.isLoggedIn &&
clientState.isInGame &&
clientState.currentGameId &&
abilityId &&
clientState.currentGameState &&
!clientState.currentGameState.isGameOver) {
// Перед отправкой действия можно добавить быструю проверку на клиенте (например, хватает ли ресурса),
// но основная валидация все равно на сервере.
socket.emit('playerAction', { actionType: 'ability', abilityId: abilityId });
disableGameControls(); // Блокируем управление до ответа сервера или следующего хода
} else {
console.warn("Cannot perform ability action, invalid state:", {
isLoggedIn: clientState.isLoggedIn,
isInGame: clientState.isInGame,
gameId: clientState.currentGameId,
abilityId,
gameState: clientState.currentGameState
});
}
}
// --- Обработчики событий DOM ---
if (attackButton) {
attackButton.addEventListener('click', () => {
if (clientState.isLoggedIn &&
clientState.isInGame &&
clientState.currentGameId &&
clientState.currentGameState &&
!clientState.currentGameState.isGameOver) {
socket.emit('playerAction', { actionType: 'attack' });
disableGameControls(); // Блокируем управление до ответа сервера или следующего хода
} else {
console.warn("Cannot perform attack action, invalid state.");
}
});
}
if (returnToMenuButton) { // Кнопка из модалки GameOver
returnToMenuButton.addEventListener('click', () => {
if (!clientState.isLoggedIn) {
ui.showAuthScreen(); // Если как-то оказались здесь без логина
return;
}
returnToMenuButton.disabled = true; // Блокируем на время перехода
// ui.resetGameVariables(); // Вызывается в showGameSelectionScreen
clientState.isInGame = false; // Устанавливаем, что мы больше не в игре
disableGameControls(); // Деактивируем игровые контролы
// window.gameUI.showGameOver(false, "", null, { finalGameState: { isGameOver: false } }); // Скрываем модалку (делается в showGameSelectionScreen)
ui.showGameSelectionScreen(clientState.loggedInUsername); // Возвращаемся на экран выбора
// Кнопка returnToMenuButton включится при следующем показе модалки GameOver (логика в ui.js или здесь при gameOver)
});
}
// --- Обработчики событий Socket.IO ---
socket.on('gameStarted', (data) => {
if (!clientState.isLoggedIn) return; // Игнорируем, если не залогинены
console.log('[Gameplay] Game started:', data);
// Обновляем состояние клиента
clientState.currentGameId = data.gameId;
clientState.myPlayerId = data.yourPlayerId;
clientState.currentGameState = data.initialGameState;
clientState.playerBaseStatsServer = data.playerBaseStats;
clientState.opponentBaseStatsServer = data.opponentBaseStats;
clientState.playerAbilitiesServer = data.playerAbilities;
clientState.opponentAbilitiesServer = data.opponentAbilities;
clientState.myCharacterKey = data.playerBaseStats?.characterKey;
clientState.opponentCharacterKey = data.opponentBaseStats?.characterKey;
clientState.isInGame = true;
if (data.clientConfig) { // Если сервер прислал конфиг
// Важно: GAME_CONFIG используется в ui.js и других местах
window.GAME_CONFIG = { ...window.GAME_CONFIG, ...data.clientConfig };
} else if (!window.GAME_CONFIG) { // Базовый конфиг, если не пришел с сервера и не определен
window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' /* ... другие важные ключи ... */ };
}
ui.updateGlobalWindowVariablesForUI(); // Обновляем глобальные переменные для ui.js
ui.showGameScreen(); // Показываем игровой экран
initializeAbilityButtons(); // Инициализируем кнопки способностей с новыми данными
// Очистка лога перед началом новой игры
if (window.gameUI?.uiElements?.log?.list) {
window.gameUI.uiElements.log.list.innerHTML = '';
}
// Добавление начальных логов, если есть
if (window.gameUI && typeof window.gameUI.addToLog === 'function' && data.log) {
data.log.forEach(logEntry => window.gameUI.addToLog(logEntry.message, logEntry.type));
}
// Первичное обновление UI боевого экрана
requestAnimationFrame(() => {
if (window.gameUI && typeof window.gameUI.updateUI === 'function') {
window.gameUI.updateUI();
}
});
// ui.hideGameOverModal(); // Теперь делается в showGameScreen
ui.setGameStatusMessage(""); // Очищаем общий статус
// Таймер хода будет обновлен событием 'turnTimerUpdate'
});
// Используется для восстановления состояния уже идущей игры (например, при реконнекте)
socket.on('gameState', (data) => {
if (!clientState.isLoggedIn) return;
console.log('[Gameplay] Received full gameState (e.g. on reconnect):', data);
// Обновляем состояние клиента (похоже на gameStarted)
clientState.currentGameId = data.gameId;
clientState.myPlayerId = data.yourPlayerId;
clientState.currentGameState = data.gameState; // Используем gameState вместо initialGameState
clientState.playerBaseStatsServer = data.playerBaseStats;
clientState.opponentBaseStatsServer = data.opponentBaseStats;
clientState.playerAbilitiesServer = data.playerAbilities;
clientState.opponentAbilitiesServer = data.opponentAbilities;
clientState.myCharacterKey = data.playerBaseStats?.characterKey;
clientState.opponentCharacterKey = data.opponentBaseStats?.characterKey;
clientState.isInGame = true; // Устанавливаем, что мы в игре
if (data.clientConfig) {
window.GAME_CONFIG = { ...window.GAME_CONFIG, ...data.clientConfig };
} else if (!window.GAME_CONFIG) {
window.GAME_CONFIG = { PLAYER_ID: 'player', OPPONENT_ID: 'opponent', CSS_CLASS_HIDDEN: 'hidden' };
}
ui.updateGlobalWindowVariablesForUI();
if (!clientState.isInGame || document.querySelector('.game-wrapper').style.display === 'none') {
ui.showGameScreen(); // Показываем игровой экран, если еще не там
}
initializeAbilityButtons(); // Переинициализируем кнопки способностей
// Лог при 'gameState' может быть уже накопленным, очищаем и добавляем новый
if (window.gameUI?.uiElements?.log?.list && data.log) {
window.gameUI.uiElements.log.list.innerHTML = '';
}
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();
}
});
// ui.hideGameOverModal(); // Делается в showGameScreen
// Таймер хода будет обновлен событием 'turnTimerUpdate'
});
socket.on('gameStateUpdate', (data) => {
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
clientState.currentGameState = data.gameState;
ui.updateGlobalWindowVariablesForUI(); // Обновляем window.gameState для ui.js
if (window.gameUI?.updateUI) window.gameUI.updateUI();
// Добавляем только новые логи, если они есть в этом частичном обновлении
if (window.gameUI?.addToLog && data.log) {
data.log.forEach(log => window.gameUI.addToLog(log.message, log.type));
}
// Логика включения/выключения контролов на основе gameState.isPlayerTurn и myPlayerId
// обычно делается внутри window.gameUI.updateUI()
});
socket.on('logUpdate', (data) => {
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.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 (!clientState.isLoggedIn || !clientState.currentGameId || !window.GAME_CONFIG) {
// Если мы не в игре, но залогинены, запросим состояние (вдруг это старое событие)
if (!clientState.currentGameId && clientState.isLoggedIn) socket.emit('requestGameState');
else if (!clientState.isLoggedIn) ui.showAuthScreen(); // Если вообще не залогинены
return;
}
const playerWon = data.winnerId === clientState.myPlayerId;
clientState.currentGameState = data.finalGameState; // Обновляем состояние последним
// clientState.isInGame = false; // НЕ СБРАСЫВАЕМ ЗДЕСЬ, чтобы UI показывал экран GameOver. Сбросится при выходе в меню.
ui.updateGlobalWindowVariablesForUI(); // Обновляем window.gameState для ui.js
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?.showGameOver) {
const oppKey = clientState.opponentBaseStatsServer?.characterKey;
window.gameUI.showGameOver(playerWon, data.reason, oppKey, data); // ui.js покажет модалку
}
if (returnToMenuButton) returnToMenuButton.disabled = false; // Активируем кнопку "Вернуться в меню"
ui.setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли."));
// Обновляем UI таймера, чтобы показать "Конец" или скрыть
if (window.gameUI?.updateTurnTimerDisplay) {
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState?.gameMode);
}
// Контролы должны быть заблокированы, т.к. игра окончена (ui.js->updateUI это сделает)
});
socket.on('opponentDisconnected', (data) => {
if (!clientState.isLoggedIn || !clientState.isInGame || !clientState.currentGameId || !window.GAME_CONFIG) return;
const name = data.disconnectedCharacterName || clientState.opponentBaseStatsServer?.name || 'Противник';
if (window.gameUI?.addToLog) {
window.gameUI.addToLog(`🔌 Противник (${name}) отключился.`, 'system');
}
// Если игра еще не окончена, сервер может дать время на переподключение или объявить победу
if (clientState.currentGameState && !clientState.currentGameState.isGameOver) {
ui.setGameStatusMessage(`Противник (${name}) отключился. Ожидание...`, true);
disableGameControls(); // Блокируем управление, пока сервер не решит исход
}
});
socket.on('turnTimerUpdate', (data) => {
if (!clientState.isInGame || !clientState.currentGameState || clientState.currentGameState.isGameOver) {
// Если игра окончена или не в игре, обновляем таймер соответственно
if (window.gameUI?.updateTurnTimerDisplay && !clientState.currentGameState?.isGameOver) {
window.gameUI.updateTurnTimerDisplay(null, false, clientState.currentGameState?.gameMode);
}
return;
}
if (window.gameUI && typeof window.gameUI.updateTurnTimerDisplay === 'function') {
const config = window.GAME_CONFIG || {};
// Определяем, является ли текущий ход ходом этого клиента
const isMyActualTurn = clientState.myPlayerId && clientState.currentGameState.isPlayerTurn === (clientState.myPlayerId === config.PLAYER_ID);
window.gameUI.updateTurnTimerDisplay(data.remainingTime, isMyActualTurn, clientState.currentGameState.gameMode);
}
// Логика включения/выключения контролов на основе isMyActualTurn
// обычно выполняется в window.gameUI.updateUI(), которая вызывается после gameStateUpdate.
// Если turnTimerUpdate приходит отдельно и должен влиять на контролы, то нужно добавить:
// if (isMyActualTurn) enableGameControls(); else disableGameControls();
// Но это может конфликтовать с логикой в updateUI(). Обычно updateUI() - главный источник правды.
});
// Начальная деактивация игровых контролов при загрузке модуля
disableGameControls();
}

296
public/js/main.js Normal file
View File

@ -0,0 +1,296 @@
// /public/js/main.js
import { initAuth } from './auth.js';
import { initGameSetup } from './gameSetup.js';
import { initGameplay } from './gameplay.js';
// Предполагаем, что ui.js загружен перед этим скриптом (в HTML)
// и создал глобальный объект window.gameUI
// Также ui.js будет читать window.gameState, window.gameData, window.myPlayerId, window.GAME_CONFIG
document.addEventListener('DOMContentLoaded', () => {
const socket = io({
// Опции Socket.IO, если нужны
});
// --- DOM Элементы для общего UI-управления ---
// (Эти элементы управляют общим потоком приложения, а не деталями боя)
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'); // Для auth.js
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"]'); // для gameSetup.js
const gameWrapper = document.querySelector('.game-wrapper');
// Элементы, связанные с gameOver, управляются через window.gameUI.showGameOver,
// но кнопка "Вернуться в меню" может быть здесь для общего сброса.
const returnToMenuButton = document.getElementById('return-to-menu-button');
const turnTimerContainer = document.getElementById('turn-timer-container');
const turnTimerSpan = document.getElementById('turn-timer');
// --- Состояние клиента (глобальное для main и передаваемое в модули) ---
// Это состояние будет модифицироваться из разных модулей
let clientState = {
isLoggedIn: false,
loggedInUsername: '',
myUserId: null,
isInGame: false,
// Игровые переменные, которые ранее были глобальными в client.js
// и от которых зависит ui.js
currentGameId: null,
currentGameState: null,
myPlayerId: null,
myCharacterKey: null,
opponentCharacterKey: null,
playerBaseStatsServer: null,
opponentBaseStatsServer: null,
playerAbilitiesServer: null,
opponentAbilitiesServer: null,
};
// Обновляем глобальные переменные window, на которые рассчитывает ui.js
// Это временная мера. В идеале, ui.js должен получать эти данные как аргументы функций.
function updateGlobalWindowVariablesForUI() {
window.gameState = clientState.currentGameState;
window.gameData = {
playerBaseStats: clientState.playerBaseStatsServer,
opponentBaseStats: clientState.opponentBaseStatsServer,
playerAbilities: clientState.playerAbilitiesServer,
opponentAbilities: clientState.opponentAbilitiesServer
};
window.myPlayerId = clientState.myPlayerId;
// window.GAME_CONFIG остается как есть, если он глобальный и не меняется часто
// Если GAME_CONFIG приходит от сервера, его тоже нужно обновлять здесь
// if (clientState.serverConfig) window.GAME_CONFIG = { ...clientState.serverConfig };
}
// --- Функции управления UI (для переключения основных экранов и общих сообщений) ---
function showAuthScreen() {
authSection.style.display = 'block';
userInfoDiv.style.display = 'none';
gameSetupDiv.style.display = 'none';
gameWrapper.style.display = 'none';
if (window.gameUI?.showGameOver) { // Скрываем модалку GameOver, если была
window.gameUI.showGameOver(false, "", null, { finalGameState: { isGameOver: false } });
}
setAuthMessage("Ожидание подключения к серверу...");
statusContainer.style.display = 'block';
clientState.isInGame = false;
// disableGameControls(); // Вызов будет из gameplay.js
resetGameVariables(); // Важно для сброса состояния
updateGlobalWindowVariablesForUI(); // Обновляем глоб. переменные для ui.js
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
if (turnTimerSpan) turnTimerSpan.textContent = '--';
}
function showGameSelectionScreen(username) {
authSection.style.display = 'none';
userInfoDiv.style.display = 'block';
loggedInUsernameSpan.textContent = username;
gameSetupDiv.style.display = 'block';
gameWrapper.style.display = 'none';
if (window.gameUI?.showGameOver) { // Скрываем модалку GameOver
window.gameUI.showGameOver(false, "", null, { finalGameState: { isGameOver: false } });
}
setGameStatusMessage("Выберите режим игры или присоединитесь к существующей.");
statusContainer.style.display = 'block';
socket.emit('requestPvPGameList'); // Запрашиваем список игр
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;
// disableGameControls(); // Вызов будет из gameplay.js
resetGameVariables();
updateGlobalWindowVariablesForUI();
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
if (turnTimerSpan) turnTimerSpan.textContent = '--';
enableSetupButtons();
}
function showGameScreen() {
if (window.gameUI?.showGameOver) { // Скрываем модалку GameOver
window.gameUI.showGameOver(false, "", null, { finalGameState: { isGameOver: false } });
}
authSection.style.display = 'none';
userInfoDiv.style.display = 'block'; // Оставляем инфо о пользователе
gameSetupDiv.style.display = 'none';
gameWrapper.style.display = 'flex';
setGameStatusMessage(""); // Очищаем статус
statusContainer.style.display = 'none'; // Скрываем общий статус контейнер
clientState.isInGame = true;
// disableGameControls(); // Начальная деактивация, gameplay.js включит при ходе
updateGlobalWindowVariablesForUI(); // Убедимся, что ui.js имеет свежие данные
if (turnTimerContainer) turnTimerContainer.style.display = 'block';
if (turnTimerSpan) turnTimerSpan.textContent = '--';
}
function resetGameVariables() {
clientState.currentGameId = null;
clientState.currentGameState = null;
clientState.myPlayerId = null;
clientState.myCharacterKey = null;
clientState.opponentCharacterKey = null;
clientState.playerBaseStatsServer = null;
clientState.opponentBaseStatsServer = null;
clientState.playerAbilitiesServer = null;
clientState.opponentAbilitiesServer = null;
// Также обновляем глобальные переменные для ui.js
updateGlobalWindowVariablesForUI();
}
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'; // Скрываем другой статус
}
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'; // Скрываем другой статус
}
// Функции для управления кнопками на экране выбора игры (могут быть вызваны из gameSetup)
function disableSetupButtons() {
if(createAIGameButton) createAIGameButton.disabled = true;
if(createPvPGameButton) createPvPGameButton.disabled = true;
if(joinPvPGameButton) joinPvPGameButton.disabled = true;
if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = true;
if(availableGamesDiv) availableGamesDiv.querySelectorAll('button').forEach(btn => btn.disabled = true);
}
function enableSetupButtons() {
if(createAIGameButton) createAIGameButton.disabled = false;
if(createPvPGameButton) createPvPGameButton.disabled = false;
if(joinPvPGameButton) joinPvPGameButton.disabled = false;
if(findRandomPvPGameButton) findRandomPvPGameButton.disabled = false;
// Кнопки в списке игр включаются в updateAvailableGamesList (в gameSetup.js)
}
// --- Сборка зависимостей для передачи в модули ---
const dependencies = {
socket,
clientState, // Объект состояния, который модули могут читать и изменять
ui: { // Функции и элементы для управления общим UI и состоянием
showAuthScreen,
showGameSelectionScreen,
showGameScreen,
setAuthMessage,
setGameStatusMessage,
resetGameVariables,
updateGlobalWindowVariablesForUI, // Важно для ui.js
disableSetupButtons,
enableSetupButtons,
elements: { // Передаем элементы, нужные для специфической логики модулей
// Для auth.js
loginForm,
registerForm,
logoutButton,
// Для gameSetup.js
createAIGameButton,
createPvPGameButton,
joinPvPGameButton,
findRandomPvPGameButton,
gameIdInput,
availableGamesDiv,
pvpCharacterRadios,
// Для gameplay.js (или для обработки gameover здесь)
returnToMenuButton,
}
},
// gameUI: window.gameUI // Можно передать, если модули должны напрямую вызывать gameUI.
// Но пока gameplay.js будет использовать глобальный window.gameUI
};
// Инициализация модулей
initAuth(dependencies);
initGameSetup(dependencies);
initGameplay(dependencies);
// --- Обработчики событий Socket.IO (глобальные для приложения) ---
socket.on('connect', () => {
console.log('[Client] Socket connected:', socket.id);
setAuthMessage("Успешно подключено к серверу. Вход...");
if (clientState.isLoggedIn && clientState.myUserId) {
// Пытаемся восстановить состояние игры, если были залогинены
socket.emit('requestGameState');
} else {
// Показываем экран логина, если не залогинены
showAuthScreen();
}
});
socket.on('disconnect', (reason) => {
console.warn('[Client] Disconnected:', reason);
setGameStatusMessage(`Отключено от сервера: ${reason}. Попытка переподключения...`, true);
// Здесь можно добавить логику для UI, показывающую состояние "отключено"
// disableGameControls(); // будет в gameplay
if (turnTimerSpan) turnTimerSpan.textContent = 'Откл.';
// Не сбрасываем isLoggedIn, чтобы при переподключении можно было восстановить сессию
});
// Общая обработка ошибок от сервера, если они не перехвачены в модулях
socket.on('gameError', (data) => {
console.error('[Client] Received gameError:', data.message);
// Показываем ошибку пользователю
if (clientState.isInGame && window.gameUI?.addToLog) {
window.gameUI.addToLog(`❌ Ошибка сервера: ${data.message}`, 'system');
// Здесь можно решить, нужно ли возвращать в меню или просто показать сообщение
} else if (clientState.isLoggedIn) {
setGameStatusMessage(`❌ Ошибка: ${data.message}`, true);
enableSetupButtons(); // Возвращаем активность кнопкам на экране выбора игры
} else {
setAuthMessage(`❌ Ошибка: ${data.message}`, true);
if(registerForm) registerForm.querySelector('button').disabled = false;
if(loginForm) loginForm.querySelector('button').disabled = false;
}
});
// Обработчик для gameNotFound, который может прийти при реконнекте, если игры нет
socket.on('gameNotFound', (data) => {
console.log('[Client] Main: Game not found/ended:', data?.message);
dependencies.ui.resetGameVariables(); // Сбрасываем игровые переменные
clientState.isInGame = false;
// disableGameControls(); // в gameplay
if (window.gameUI?.showGameOver) window.gameUI.showGameOver(false, "", null, { finalGameState: { isGameOver: false } }); // Скрыть модалку
if (turnTimerContainer) turnTimerContainer.style.display = 'none';
if (turnTimerSpan) turnTimerSpan.textContent = '--';
if (clientState.isLoggedIn) {
showGameSelectionScreen(clientState.loggedInUsername);
setGameStatusMessage(data?.message || "Активная игровая сессия не найдена.");
} else {
showAuthScreen();
setAuthMessage(data?.message || "Пожалуйста, войдите.");
}
});
// --- Инициализация UI ---
showAuthScreen(); // Показываем начальный экран аутентификации
});