With DataBaseREdis
This commit is contained in:
277
public/game.js
277
public/game.js
@ -189,6 +189,10 @@
|
||||
socket = io(url, { transports: ['websocket', 'polling'], reconnection: false });
|
||||
socket.on('connect', () => {
|
||||
clearError();
|
||||
// Отправляем имя пользователя на сервер, если авторизованы
|
||||
if (currentUser && currentUser.username) {
|
||||
socket.emit('setUsername', currentUser.username);
|
||||
}
|
||||
});
|
||||
socket.on('connect_error', (e) => {
|
||||
const errorMsg = e.message || 'Не удалось подключиться к серверу';
|
||||
@ -2830,8 +2834,281 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Авторизация
|
||||
let currentUser = null;
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const response = await fetch('/api/user', { credentials: 'include' });
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
currentUser = data.user;
|
||||
updateUserUI();
|
||||
return true;
|
||||
} else {
|
||||
// Не авторизован, но не показываем модальное окно автоматически
|
||||
// Пользователь может продолжить без авторизации
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки авторизации:', error);
|
||||
// Не показываем модальное окно при ошибке, пользователь может продолжить
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateUserUI() {
|
||||
const userInfo = $('user-info');
|
||||
const userName = $('user-name');
|
||||
if (currentUser && userInfo && userName) {
|
||||
userInfo.style.display = 'flex';
|
||||
userName.textContent = currentUser.username;
|
||||
if (currentUser.gamesPlayed !== undefined) {
|
||||
userName.textContent += ` (${currentUser.gamesWon || 0}/${currentUser.gamesPlayed || 0})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showAuthModal() {
|
||||
const authOverlay = $('auth-overlay');
|
||||
if (authOverlay) {
|
||||
authOverlay.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function hideAuthModal() {
|
||||
const authOverlay = $('auth-overlay');
|
||||
if (authOverlay) {
|
||||
authOverlay.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function initAuth() {
|
||||
// Переключение между вкладками авторизации
|
||||
$all('.auth-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
$all('.auth-tab').forEach(t => t.classList.remove('active'));
|
||||
$all('.auth-panel').forEach(p => p.classList.add('hidden'));
|
||||
tab.classList.add('active');
|
||||
const panelId = 'auth-' + tab.dataset.authTab + '-panel';
|
||||
const panel = $(panelId);
|
||||
if (panel) {
|
||||
panel.classList.remove('hidden');
|
||||
panel.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Форма входа
|
||||
const loginForm = $('login-form');
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const username = $('login-username')?.value.trim();
|
||||
const password = $('login-password')?.value;
|
||||
const errorEl = $('login-error');
|
||||
|
||||
if (!username || !password) {
|
||||
if (errorEl) {
|
||||
errorEl.textContent = 'Заполните все поля';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
currentUser = data.user;
|
||||
updateUserUI();
|
||||
hideAuthModal();
|
||||
if (errorEl) errorEl.classList.add('hidden');
|
||||
} else {
|
||||
if (errorEl) {
|
||||
errorEl.textContent = data.error || 'Ошибка входа';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка входа:', error);
|
||||
if (errorEl) {
|
||||
errorEl.textContent = 'Ошибка соединения с сервером';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Форма регистрации
|
||||
const registerForm = $('register-form');
|
||||
if (registerForm) {
|
||||
registerForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const username = $('register-username')?.value.trim();
|
||||
const password = $('register-password')?.value;
|
||||
const email = $('register-email')?.value.trim();
|
||||
const errorEl = $('register-error');
|
||||
|
||||
if (!username || !password) {
|
||||
if (errorEl) {
|
||||
errorEl.textContent = 'Заполните обязательные поля';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ username, password, email })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
currentUser = data.user;
|
||||
updateUserUI();
|
||||
hideAuthModal();
|
||||
if (errorEl) errorEl.classList.add('hidden');
|
||||
// Отправляем имя пользователя на сервер через socket
|
||||
if (socket && socket.connected && currentUser.username) {
|
||||
socket.emit('setUsername', currentUser.username);
|
||||
}
|
||||
} else {
|
||||
if (errorEl) {
|
||||
errorEl.textContent = data.error || 'Ошибка регистрации';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка регистрации:', error);
|
||||
if (errorEl) {
|
||||
errorEl.textContent = 'Ошибка соединения с сервером';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Кнопка закрытия авторизации
|
||||
const authClose = $('auth-close');
|
||||
if (authClose) {
|
||||
authClose.addEventListener('click', () => {
|
||||
hideAuthModal();
|
||||
});
|
||||
}
|
||||
|
||||
// Кнопка выхода
|
||||
const btnLogout = $('btn-logout');
|
||||
if (btnLogout) {
|
||||
btnLogout.addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('/api/logout', { method: 'POST', credentials: 'include' });
|
||||
currentUser = null;
|
||||
const userInfo = $('user-info');
|
||||
if (userInfo) userInfo.style.display = 'none';
|
||||
showAuthModal();
|
||||
} catch (error) {
|
||||
console.error('Ошибка выхода:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Leaderboard
|
||||
const btnLeaderboard = $('btn-leaderboard');
|
||||
if (btnLeaderboard) {
|
||||
btnLeaderboard.addEventListener('click', async () => {
|
||||
const overlay = $('leaderboard-overlay');
|
||||
const content = $('leaderboard-content');
|
||||
if (overlay) overlay.classList.remove('hidden');
|
||||
if (content) content.innerHTML = '<p class="hint" style="text-align: center; padding: 2rem;">Загрузка...</p>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/leaderboard', { credentials: 'include' });
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.leaderboard) {
|
||||
if (data.leaderboard.length === 0) {
|
||||
content.innerHTML = '<p class="hint" style="text-align: center; padding: 2rem;">Рейтинг пуст</p>';
|
||||
} else {
|
||||
content.innerHTML = `
|
||||
<table class="leaderboard-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="rank">#</th>
|
||||
<th class="username">Игрок</th>
|
||||
<th class="stats">Игр</th>
|
||||
<th class="stats">Побед</th>
|
||||
<th class="stats">% Побед</th>
|
||||
<th class="stats">Урон</th>
|
||||
<th class="stats">Хил</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${data.leaderboard.map((player, idx) => `
|
||||
<tr>
|
||||
<td class="rank">${idx + 1}</td>
|
||||
<td class="username">${escapeHtml(player.username)}</td>
|
||||
<td class="stats">${player.gamesPlayed}</td>
|
||||
<td class="stats">${player.gamesWon}</td>
|
||||
<td class="stats">${player.winRate}%</td>
|
||||
<td class="stats">${player.totalDamage}</td>
|
||||
<td class="stats">${player.totalHealing}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
content.innerHTML = '<p class="hint" style="text-align: center; padding: 2rem; color: var(--red);">Ошибка загрузки рейтинга</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки leaderboard:', error);
|
||||
if (content) {
|
||||
content.innerHTML = '<p class="hint" style="text-align: center; padding: 2rem; color: var(--red);">Ошибка соединения</p>';
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof lucide !== 'undefined') {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
lucide.createIcons();
|
||||
} catch (e) {
|
||||
console.warn('Error updating Lucide icons:', e);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const leaderboardClose = $('leaderboard-close');
|
||||
if (leaderboardClose) {
|
||||
leaderboardClose.addEventListener('click', () => {
|
||||
const overlay = $('leaderboard-overlay');
|
||||
if (overlay) overlay.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
initLobby();
|
||||
initAuth();
|
||||
// Проверяем авторизацию, но не блокируем доступ
|
||||
checkAuth().catch(() => {
|
||||
// Если авторизация не удалась, просто показываем модальное окно
|
||||
// Пользователь может продолжить без авторизации
|
||||
});
|
||||
showScreen('lobby');
|
||||
socket = null;
|
||||
}
|
||||
|
||||
@ -20,6 +20,55 @@
|
||||
<div id="stars2"></div>
|
||||
<div id="stars3"></div>
|
||||
|
||||
<!-- Auth Modal -->
|
||||
<div id="auth-overlay" class="modal-overlay hidden">
|
||||
<div class="modal auth-modal">
|
||||
<div class="auth-tabs">
|
||||
<button type="button" class="auth-tab active" data-auth-tab="login">Вход</button>
|
||||
<button type="button" class="auth-tab" data-auth-tab="register">Регистрация</button>
|
||||
</div>
|
||||
|
||||
<div id="auth-login-panel" class="auth-panel active">
|
||||
<h2>Вход</h2>
|
||||
<form id="login-form">
|
||||
<label>Имя пользователя</label>
|
||||
<input type="text" id="login-username" placeholder="Введите имя" required maxlength="20" />
|
||||
<label style="margin-top: 1rem;">Пароль</label>
|
||||
<input type="password" id="login-password" placeholder="Введите пароль" required />
|
||||
<button type="submit" class="btn btn-primary" style="margin-top: 1.5rem; width: 100%;">Войти</button>
|
||||
</form>
|
||||
<p id="login-error" class="error hidden" style="margin-top: 1rem;"></p>
|
||||
</div>
|
||||
|
||||
<div id="auth-register-panel" class="auth-panel hidden">
|
||||
<h2>Регистрация</h2>
|
||||
<form id="register-form">
|
||||
<label>Имя пользователя</label>
|
||||
<input type="text" id="register-username" placeholder="От 3 до 20 символов" required minlength="3" maxlength="20" />
|
||||
<label style="margin-top: 1rem;">Пароль</label>
|
||||
<input type="password" id="register-password" placeholder="Не менее 6 символов" required minlength="6" />
|
||||
<label style="margin-top: 1rem;">Email (необязательно)</label>
|
||||
<input type="email" id="register-email" placeholder="your@email.com" />
|
||||
<button type="submit" class="btn btn-primary" style="margin-top: 1.5rem; width: 100%;">Зарегистрироваться</button>
|
||||
</form>
|
||||
<p id="register-error" class="error hidden" style="margin-top: 1rem;"></p>
|
||||
</div>
|
||||
|
||||
<button type="button" id="auth-close" class="btn btn-ghost" style="margin-top: 1rem; width: 100%;">Продолжить без авторизации</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaderboard Modal -->
|
||||
<div id="leaderboard-overlay" class="modal-overlay hidden">
|
||||
<div class="modal leaderboard-modal">
|
||||
<h2><i data-lucide="trophy" style="width: 24px; height: 24px; vertical-align: middle; margin-right: 0.5rem;"></i>Глобальный рейтинг</h2>
|
||||
<div id="leaderboard-content" style="max-height: 500px; overflow-y: auto; margin-top: 1rem;">
|
||||
<p class="hint" style="text-align: center; padding: 2rem;">Загрузка...</p>
|
||||
</div>
|
||||
<button type="button" id="leaderboard-close" class="btn btn-primary" style="margin-top: 1rem; width: 100%;">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lobby -->
|
||||
<section id="lobby" class="screen">
|
||||
<div class="lobby-card">
|
||||
@ -29,9 +78,16 @@
|
||||
<span class="logo-hs">HEARTHSTONE</span>
|
||||
</h1>
|
||||
<p class="subtitle">PvP до 4 игроков · Игра с ИИ · Работает через Radmin VPN</p>
|
||||
<button type="button" id="btn-instructions-lobby" class="btn-instructions-lobby" title="Как играть">?</button>
|
||||
<button type="button" id="btn-cards-gallery" class="btn-instructions-lobby" title="Галерея карт" style="right: 3.5rem;"><i data-lucide="book-open"></i></button>
|
||||
<button type="button" id="btn-settings-lobby" class="btn-settings-lobby" title="Настройки"><i data-lucide="settings"></i></button>
|
||||
<div style="position: absolute; top: 1rem; right: 1rem; display: flex; gap: 0.5rem; align-items: center;">
|
||||
<button type="button" id="btn-leaderboard" class="btn-instructions-lobby" title="Рейтинг"><i data-lucide="trophy"></i></button>
|
||||
<button type="button" id="btn-instructions-lobby" class="btn-instructions-lobby" title="Как играть">?</button>
|
||||
<button type="button" id="btn-cards-gallery" class="btn-instructions-lobby" title="Галерея карт"><i data-lucide="book-open"></i></button>
|
||||
<button type="button" id="btn-settings-lobby" class="btn-settings-lobby" title="Настройки"><i data-lucide="settings"></i></button>
|
||||
</div>
|
||||
<div id="user-info" style="position: absolute; top: 1rem; left: 1rem; display: none; align-items: center; gap: 0.5rem;">
|
||||
<span id="user-name" style="color: var(--cyan); font-weight: 600;"></span>
|
||||
<button type="button" id="btn-logout" class="btn btn-ghost" style="padding: 0.3rem 0.6rem; font-size: 0.85rem;">Выйти</button>
|
||||
</div>
|
||||
|
||||
<div class="lobby-tabs">
|
||||
<button type="button" class="tab active" data-tab="host">Создать игру</button>
|
||||
|
||||
@ -1564,6 +1564,103 @@ html, body {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
.modal-overlay.hidden { display: none !important; }
|
||||
|
||||
/* Auth Modal */
|
||||
.auth-modal {
|
||||
max-width: 400px;
|
||||
}
|
||||
.auth-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 2px solid rgba(212,168,75,0.2);
|
||||
}
|
||||
.auth-tab {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.auth-tab.active {
|
||||
color: var(--cyan);
|
||||
border-bottom-color: var(--cyan);
|
||||
}
|
||||
.auth-tab:hover {
|
||||
color: var(--cyan);
|
||||
}
|
||||
.auth-panel {
|
||||
display: none;
|
||||
}
|
||||
.auth-panel.active {
|
||||
display: block;
|
||||
}
|
||||
.auth-panel label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.auth-panel input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border: 2px solid rgba(0,180,255,0.35);
|
||||
border-radius: 8px;
|
||||
color: #f8fafc;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.auth-panel input:focus {
|
||||
outline: none;
|
||||
border-color: var(--cyan);
|
||||
box-shadow: 0 0 10px rgba(0,212,255,0.3);
|
||||
}
|
||||
|
||||
/* Leaderboard Modal */
|
||||
.leaderboard-modal {
|
||||
max-width: 700px;
|
||||
max-height: 80vh;
|
||||
}
|
||||
.leaderboard-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.leaderboard-table th,
|
||||
.leaderboard-table td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.leaderboard-table th {
|
||||
background: rgba(0,0,0,0.3);
|
||||
color: var(--cyan);
|
||||
font-weight: 700;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
.leaderboard-table tr:hover {
|
||||
background: rgba(0,212,255,0.1);
|
||||
}
|
||||
.leaderboard-table .rank {
|
||||
font-weight: 700;
|
||||
color: var(--gold);
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
.leaderboard-table .username {
|
||||
font-weight: 600;
|
||||
color: var(--cyan);
|
||||
}
|
||||
.leaderboard-table .stats {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: linear-gradient(145deg, #2d2a22 0%, #1a1814 100%);
|
||||
border: 3px solid var(--gold);
|
||||
|
||||
Reference in New Issue
Block a user