bc/public/js/client.js
svoboda200786 ba183c472f revert d5d5497dd67530dd3f08a80f8226d71e16c33f9d
revert Загрузить файлы в «public/js»
2025-05-16 08:10:36 +00:00

577 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// /public/js/client.js
document.addEventListener('DOMContentLoaded', () => {
const socket = io({ path: '/battleclub/socket.io' })
// --- Состояние клиента ---
let currentGameState = null;
let myPlayerId = null; // Технический ID слота, который занимает ЭТОТ клиент ('player' или 'opponent')
let myCharacterKey = null;
let opponentCharacterKey = null;
let currentGameId = null;
let playerBaseStatsServer = null; // Статы персонажа, которым УПРАВЛЯЕТ этот клиент (приходят от сервера как data.playerBaseStats)
let opponentBaseStatsServer = null; // Статы персонажа-оппонента этого клиента (приходят от сервера как data.opponentBaseStats)
let playerAbilitiesServer = null;
let opponentAbilitiesServer = null;
let isLoggedIn = false;
let loggedInUsername = '';
// --- DOM Элементы ---
// Аутентификация
const authSection = document.getElementById('auth-section');
const registerForm = document.getElementById('register-form');
const loginForm = document.getElementById('login-form');
const authMessage = document.getElementById('auth-message');
const userInfoDiv = document.getElementById('user-info');
const loggedInUsernameSpan = document.getElementById('logged-in-username');
const logoutButton = document.getElementById('logout-button');
// Настройка игры
const gameSetupDiv = document.getElementById('game-setup');
const createAIGameButton = document.getElementById('create-ai-game');
const createPvPGameButton = document.getElementById('create-pvp-game');
const joinPvPGameButton = document.getElementById('join-pvp-game');
const findRandomPvPGameButton = document.getElementById('find-random-pvp-game');
const gameIdInput = document.getElementById('game-id-input');
const availableGamesDiv = document.getElementById('available-games-list');
const gameStatusMessage = document.getElementById('game-status-message');
const pvpCharacterRadios = document.querySelectorAll('input[name="pvp-character"]');
// Игровая Арена
const gameWrapper = document.querySelector('.game-wrapper');
const attackButton = document.getElementById('button-attack');
const returnToMenuButton = document.getElementById('return-to-menu-button');
const gameOverScreen = document.getElementById('game-over-screen');
console.log('Client.js DOMContentLoaded. Initializing elements...');
// --- Функции управления 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';
hideGameOverModal();
setGameStatusMessage("Войдите или зарегистрируйтесь для начала игры.");
setAuthMessage("");
}
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';
hideGameOverModal();
setGameStatusMessage("Выберите режим игры или присоединитесь к существующей.");
socket.emit('requestPvPGameList');
updateAvailableGamesList([]);
if (gameIdInput) gameIdInput.value = '';
const elenaRadio = document.getElementById('char-elena');
if (elenaRadio) elenaRadio.checked = true;
}
function showGameScreen() {
console.log('[UI] Showing Game Screen');
hideGameOverModal();
if (authSection) authSection.style.display = 'none';
if (userInfoDiv) userInfoDiv.style.display = 'block';
if (gameSetupDiv) gameSetupDiv.style.display = 'none';
if (gameWrapper) gameWrapper.style.display = 'flex';
setGameStatusMessage("");
}
function hideGameOverModal() {
const hiddenClass = (window.GAME_CONFIG && window.GAME_CONFIG.CSS_CLASS_HIDDEN) ? 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 && gameUI.uiElements && gameUI.uiElements.gameOver && gameUI.uiElements.gameOver.modalContent) {
gameUI.uiElements.gameOver.modalContent.style.transform = 'scale(0.8) translateY(30px)';
gameUI.uiElements.gameOver.modalContent.style.opacity = '0';
}
if (window.gameUI && window.gameUI.uiElements && window.gameUI.uiElements.opponent && window.gameUI.uiElements.opponent.panel) {
const opponentPanel = window.gameUI.uiElements.opponent.panel;
if (opponentPanel.classList.contains('dissolving')) {
console.log('[Client.js DEBUG] Removing .dissolving from opponent panel during hideGameOverModal.');
opponentPanel.classList.remove('dissolving');
const originalTransition = opponentPanel.style.transition;
opponentPanel.style.transition = 'none';
opponentPanel.style.opacity = '1';
opponentPanel.style.transform = 'scale(1) translateY(0)';
requestAnimationFrame(() => {
opponentPanel.style.transition = originalTransition || '';
});
}
}
}
}
function setAuthMessage(message, isError = false) {
if (authMessage) {
authMessage.textContent = message;
authMessage.className = isError ? 'error' : 'success';
authMessage.style.display = message ? 'block' : '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)';
}
}
function getSelectedCharacterKey() {
let selectedKey = 'elena';
if (pvpCharacterRadios) {
pvpCharacterRadios.forEach(radio => {
if (radio.checked) {
selectedKey = radio.value;
}
});
}
return selectedKey;
}
// --- Инициализация кнопок и обработчиков ---
if (registerForm) {
registerForm.addEventListener('submit', (e) => {
e.preventDefault();
const usernameInput = document.getElementById('register-username');
const passwordInput = document.getElementById('register-password');
if (usernameInput && passwordInput) {
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) {
socket.emit('login', { username: usernameInput.value, password: passwordInput.value });
} else {
setAuthMessage("Ошибка: поля ввода не найдены.", true);
}
});
}
if (logoutButton) {
logoutButton.addEventListener('click', () => {
socket.emit('logout');
isLoggedIn = false;
loggedInUsername = '';
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;
showAuthScreen();
setGameStatusMessage("Вы вышли из системы.");
});
}
if (createAIGameButton) {
createAIGameButton.addEventListener('click', () => {
if (!isLoggedIn) {
setGameStatusMessage("Пожалуйста, войдите, чтобы начать игру.", true); return;
}
socket.emit('createGame', { mode: 'ai', characterKey: 'elena' });
setGameStatusMessage("Создание игры против AI...");
});
}
if (createPvPGameButton) {
createPvPGameButton.addEventListener('click', () => {
if (!isLoggedIn) {
setGameStatusMessage("Пожалуйста, войдите, чтобы начать игру.", true); return;
}
const selectedCharacter = getSelectedCharacterKey();
socket.emit('createGame', { mode: 'pvp', characterKey: selectedCharacter });
setGameStatusMessage(`Создание PvP игры за ${selectedCharacter === 'elena' ? 'Елену' : 'Альмагест'}...`);
});
}
if (joinPvPGameButton && gameIdInput) {
joinPvPGameButton.addEventListener('click', () => {
if (!isLoggedIn) {
setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true); return;
}
const gameIdToJoin = gameIdInput.value.trim();
if (gameIdToJoin) {
socket.emit('joinGame', { gameId: gameIdToJoin });
setGameStatusMessage(`Присоединение к игре ${gameIdToJoin}...`);
} else {
setGameStatusMessage("Пожалуйста, введите ID игры для присоединения.", true);
}
});
}
if (findRandomPvPGameButton) {
findRandomPvPGameButton.addEventListener('click', () => {
if (!isLoggedIn) {
setGameStatusMessage("Пожалуйста, войдите, чтобы найти игру.", true); return;
}
const selectedCharacter = getSelectedCharacterKey();
socket.emit('findRandomGame', { characterKey: selectedCharacter });
setGameStatusMessage(`Поиск случайной PvP игры (предпочтение: ${selectedCharacter === 'elena' ? 'Елена' : 'Альмагест'})...`);
});
}
if (attackButton) {
attackButton.addEventListener('click', () => {
if (currentGameId && currentGameState && !currentGameState.isGameOver && isLoggedIn) {
socket.emit('playerAction', { actionType: 'attack' });
}
});
}
function handleAbilityButtonClick(event) {
const button = event.currentTarget;
const abilityId = button.dataset.abilityId;
if (currentGameId && abilityId && currentGameState && !currentGameState.isGameOver && isLoggedIn) {
socket.emit('playerAction', { actionType: 'ability', abilityId: abilityId });
}
}
if (returnToMenuButton) {
returnToMenuButton.addEventListener('click', () => {
if (!isLoggedIn) {
showAuthScreen();
return;
}
console.log('[Client] Return to menu button clicked.');
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;
showGameSelectionScreen(loggedInUsername);
});
}
function initializeAbilityButtons() {
const abilitiesGrid = document.getElementById('abilities-grid');
if (!abilitiesGrid || !window.gameUI || !window.GAME_CONFIG) {
if(abilitiesGrid) abilitiesGrid.innerHTML = '<p class="placeholder-text">Ошибка загрузки способностей.</p>';
return;
}
abilitiesGrid.innerHTML = '';
const config = window.GAME_CONFIG;
const abilitiesToDisplay = playerAbilitiesServer; // Используем данные, сохраненные при gameStarted
const baseStatsForResource = playerBaseStatsServer; // Используем данные, сохраненные при gameStarted
if (!abilitiesToDisplay || abilitiesToDisplay.length === 0 || !baseStatsForResource) {
abilitiesGrid.innerHTML = '<p class="placeholder-text">Нет доступных способностей.</p>';
return;
}
const resourceName = baseStatsForResource.resourceName || "Ресурс";
abilitiesToDisplay.forEach(ability => {
const button = document.createElement('button');
button.id = `ability-btn-${ability.id}`;
button.classList.add(config.CSS_CLASS_ABILITY_BUTTON || 'ability-button');
button.dataset.abilityId = ability.id;
let descriptionText = ability.description;
if (typeof ability.descriptionFunction === 'function') {
const targetStatsForDesc = opponentBaseStatsServer; // Используем данные, сохраненные при gameStarted
descriptionText = ability.descriptionFunction(config, targetStatsForDesc);
}
let title = `${ability.name} (${ability.cost} ${resourceName}) - ${descriptionText}`;
let cooldown = ability.cooldown;
if (ability.internalCooldownFromConfig && config[ability.internalCooldownFromConfig]) {
cooldown = config[ability.internalCooldownFromConfig];
} else if (ability.internalCooldownValue) {
cooldown = ability.internalCooldownValue;
}
if (cooldown) title += ` (КД: ${cooldown} х.)`;
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);
});
}
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');
li.textContent = `ID: ${game.id.substring(0, 8)}... - ${game.status || 'Ожидает игрока'}`;
const joinBtn = document.createElement('button');
joinBtn.textContent = 'Присоединиться';
joinBtn.dataset.gameId = game.id;
joinBtn.addEventListener('click', (e) => {
if (!isLoggedIn) {
setGameStatusMessage("Пожалуйста, войдите, чтобы присоединиться к игре.", true); return;
}
socket.emit('joinGame', { gameId: e.target.dataset.gameId });
});
li.appendChild(joinBtn);
ul.appendChild(li);
}
});
availableGamesDiv.appendChild(ul);
} else {
availableGamesDiv.innerHTML += '<p>Нет доступных игр. Создайте свою!</p>';
}
}
// --- Обработчики событий Socket.IO ---
socket.on('connect', () => {
console.log('[Client] Socket connected to server! Socket ID:', socket.id);
if (!isLoggedIn) {
showAuthScreen();
} else {
console.log(`[Client] Reconnected as ${loggedInUsername}. Requesting state or showing game selection.`);
showGameSelectionScreen(loggedInUsername);
}
});
socket.on('disconnect', (reason) => {
console.log('[Client] Disconnected from server:', reason);
setGameStatusMessage(`Отключено от сервера: ${reason}. Попробуйте обновить страницу.`, true);
hideGameOverModal();
});
socket.on('registerResponse', (data) => {
setAuthMessage(data.message, !data.success);
if (data.success && registerForm) registerForm.reset();
});
socket.on('loginResponse', (data) => {
setAuthMessage(data.message, !data.success);
if (data.success) {
isLoggedIn = true; loggedInUsername = data.username;
setAuthMessage(""); showGameSelectionScreen(data.username);
} else {
isLoggedIn = false; loggedInUsername = '';
}
});
socket.on('gameCreated', (data) => {
if (!isLoggedIn) return;
currentGameId = data.gameId;
myPlayerId = data.yourPlayerId; // Запоминаем наш технический ID слота
console.log(`[Client] Game created/joined: ${currentGameId}, Mode: ${data.mode}, You (${loggedInUsername}) are in slot: ${myPlayerId}`);
if (data.mode === 'pvp') {
if (gameIdInput) gameIdInput.value = currentGameId;
setGameStatusMessage(`PvP игра ${currentGameId.substring(0,8)}... создана. ID для друга: ${currentGameId}. Ожидание второго игрока...`);
} else {
setGameStatusMessage(`Игра против AI ${currentGameId.substring(0,8)}... создана. Ожидание начала...`);
}
});
socket.on('gameStarted', (data) => {
if (!isLoggedIn) return;
console.log('[Client] Event "gameStarted" received:', data);
if (window.gameUI && window.gameUI.uiElements && window.gameUI.uiElements.opponent && window.gameUI.uiElements.opponent.panel) {
const opponentPanel = window.gameUI.uiElements.opponent.panel;
opponentPanel.classList.remove('dissolving');
const originalTransition = opponentPanel.style.transition;
opponentPanel.style.transition = 'none';
opponentPanel.style.opacity = '1';
opponentPanel.style.transform = 'scale(1) translateY(0)';
requestAnimationFrame(() => {
opponentPanel.style.transition = originalTransition || '';
});
console.log('[Client RESTART FIX Improved] Opponent panel styles explicitly reset for new game.');
}
currentGameId = data.gameId;
myPlayerId = data.yourPlayerId; // Сервер присылает ID слота, который занимает ЭТОТ клиент
currentGameState = data.initialGameState;
// Сервер присылает playerBaseStats и opponentBaseStats ОТНОСИТЕЛЬНО этого клиента
// То есть, data.playerBaseStats - это статы персонажа, которым управляет этот клиент
// data.opponentBaseStats - это статы персонажа-оппонента для этого клиента
playerBaseStatsServer = data.playerBaseStats;
opponentBaseStatsServer = data.opponentBaseStats;
playerAbilitiesServer = data.playerAbilities;
opponentAbilitiesServer = data.opponentAbilities;
myCharacterKey = playerBaseStatsServer?.characterKey; // Ключ персонажа этого клиента
opponentCharacterKey = opponentBaseStatsServer?.characterKey; // Ключ персонажа оппонента этого клиента
console.log(`[Client gameStarted] My Slot ID (technical): ${myPlayerId}`);
console.log(`[Client gameStarted] My Character: ${myCharacterKey} (Name: ${playerBaseStatsServer?.name})`);
console.log(`[Client gameStarted] Opponent Character: ${opponentCharacterKey} (Name: ${opponentBaseStatsServer?.name})`);
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' };
}
// Глобальные переменные для ui.js
window.gameState = currentGameState;
window.gameData = { // Эти данные используются в ui.js для отображения панелей
playerBaseStats: playerBaseStatsServer, // Статы "моего" персонажа
opponentBaseStats: opponentBaseStatsServer, // Статы "моего оппонента"
playerAbilities: playerAbilitiesServer, // Способности "моего" персонажа
opponentAbilities: opponentAbilitiesServer // Способности "моего оппонента"
};
window.myPlayerId = myPlayerId; // Технический ID слота этого клиента
showGameScreen();
initializeAbilityButtons();
if (window.gameUI && gameUI.uiElements?.log?.list) {
gameUI.uiElements.log.list.innerHTML = '';
}
if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) {
data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type));
}
requestAnimationFrame(() => {
if (window.gameUI && typeof gameUI.updateUI === 'function') {
console.log('[Client] Calling gameUI.updateUI() after style reset and rAF in gameStarted.');
gameUI.updateUI();
}
});
hideGameOverModal();
if (returnToMenuButton) {
returnToMenuButton.disabled = true;
}
setGameStatusMessage("");
});
socket.on('gameStateUpdate', (data) => {
if (!isLoggedIn || !currentGameId) return;
currentGameState = data.gameState;
window.gameState = currentGameState; // ui.js использует это для обновления
if (window.gameUI && typeof gameUI.updateUI === 'function') {
gameUI.updateUI();
}
if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) {
data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type));
}
});
socket.on('logUpdate', (data) => {
if (!isLoggedIn || !currentGameId) return;
if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) {
data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type));
}
});
socket.on('gameOver', (data) => {
if (!isLoggedIn || !currentGameId) return;
console.log(`[Client gameOver] Received. 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;
// Логи для отладки имен, которые будут использоваться в ui.js
if (window.gameData) {
console.log(`[Client gameOver] For ui.js, myName will be: ${window.gameData.playerBaseStats?.name}, opponentName will be: ${window.gameData.opponentBaseStats?.name}`);
}
if (window.gameUI && typeof gameUI.updateUI === 'function') gameUI.updateUI();
if (window.gameUI && typeof gameUI.addToLog === 'function' && data.log) {
data.log.forEach(logEntry => gameUI.addToLog(logEntry.message, logEntry.type));
}
if (window.gameUI && typeof gameUI.showGameOver === 'function') {
// opponentCharacterKeyFromClient передается, чтобы ui.js знал, какой персонаж был оппонентом
// и мог применить, например, анимацию .dissolving к правильному типу оппонента (Балард/Альмагест)
const opponentKeyForModal = window.gameData?.opponentBaseStats?.characterKey;
gameUI.showGameOver(playerWon, data.reason, opponentKeyForModal);
if (returnToMenuButton) {
returnToMenuButton.disabled = false;
}
}
setGameStatusMessage("Игра окончена. " + (playerWon ? "Вы победили!" : "Вы проиграли."));
});
socket.on('waitingForOpponent', () => {
if (!isLoggedIn) return;
setGameStatusMessage("Ожидание присоединения оппонента...");
});
socket.on('opponentDisconnected', (data) => {
if (!isLoggedIn || !currentGameId) return;
const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system';
if (window.gameUI && typeof gameUI.addToLog === 'function') {
gameUI.addToLog(`Противник (${data.disconnectedCharacterName || 'Игрок'}) отключился.`, systemLogType);
}
if (currentGameState && !currentGameState.isGameOver) {
setGameStatusMessage("Противник отключился. Игра может быть завершена сервером.", true);
// Сервер должен прислать 'gameOver', если игра действительно завершается
}
});
socket.on('gameError', (data) => {
console.error('[Client] Server error:', data.message);
const systemLogType = (window.GAME_CONFIG?.LOG_TYPE_SYSTEM) || 'system';
if (isLoggedIn && currentGameId && currentGameState && !currentGameState.isGameOver && window.gameUI && typeof gameUI.addToLog === 'function') {
gameUI.addToLog(`Ошибка: ${data.message}`, systemLogType);
}
setGameStatusMessage(`Ошибка: ${data.message}`, true);
});
socket.on('availablePvPGamesList', (games) => {
if (!isLoggedIn) return;
updateAvailableGamesList(games);
});
socket.on('noPendingGamesFound', (data) => {
if (!isLoggedIn) return;
setGameStatusMessage(data.message || "Свободных игр не найдено. Создана новая для вас, ожидайте оппонента.");
updateAvailableGamesList([]);
if (data.gameId) {
currentGameId = data.gameId;
myPlayerId = data.yourPlayerId; // Запоминаем наш технический ID слота
if (gameIdInput) gameIdInput.value = currentGameId;
console.log(`[Client] New game ${currentGameId} created after no pending games found. My slot: ${myPlayerId}`);
}
});
// --- Начальное состояние UI ---
showAuthScreen();
});