Add top-5 leaderboard UI — was fully wired server-side, no way to view it

topSolutions()/api.php?action=top existed since the first PHP deploy but
nothing ever called it. Add 🏆 HUD button + T key, small modal listing
rank/name/move count/date, closes via button or Esc. User-supplied names
go through escapeHtml() before landing in innerHTML.
This commit is contained in:
Alex Cube
2026-08-10 15:46:05 +03:00
parent 63a051805c
commit 374092a03c
+51
View File
@@ -27,6 +27,20 @@
#solve-cancel.solve-found { background: #7CFF6B; color: #0a0a15; } #solve-cancel.solve-found { background: #7CFF6B; color: #0a0a15; }
#solve-cancel.solve-found:hover { background: #9dff8f; } #solve-cancel.solve-found:hover { background: #9dff8f; }
#top-btn { cursor: pointer; }
#top-btn:hover { background: rgba(233,69,96,0.25) !important; }
#top-overlay { position: fixed; inset: 0; background: rgba(10,10,21,0.75); display: flex; justify-content: center; align-items: center; z-index: 10; }
#top-box { background: #1a1a2e; border: 2px solid #e94560; border-radius: 12px; padding: 20px 28px; text-align: center; box-shadow: 0 0 30px rgba(233,69,96,0.5); min-width: 280px; }
#top-box p { color: #e94560; font-size: 16px; font-weight: bold; margin-bottom: 12px; }
#top-list { text-align: left; margin-bottom: 16px; }
#top-list .row { display: flex; justify-content: space-between; gap: 16px; padding: 5px 4px; color: #ccc; border-bottom: 1px solid rgba(233,69,96,0.15); }
#top-list .rank { color: #f0c27a; font-weight: bold; width: 1.5em; }
#top-list .name { flex: 1; color: #fff; }
#top-list .count { color: #48cae4; }
#top-list .empty { color: #777; padding: 8px 4px; }
#top-close { background: #e94560; color: #fff; border: none; border-radius: 8px; padding: 8px 20px; font: inherit; font-weight: bold; cursor: pointer; }
#top-close:hover { background: #ff6b8a; }
/* ============ ЭКРАНЫ (меню/старт/финал/настройки) ============ */ /* ============ ЭКРАНЫ (меню/старт/финал/настройки) ============ */
.screen { display: flex; flex-direction: column; align-items: center; gap: 16px; color: #e94560; text-align: center; padding: 20px; } .screen { display: flex; flex-direction: column; align-items: center; gap: 16px; color: #e94560; text-align: center; padding: 20px; }
.screen.hidden { display: none; } .screen.hidden { display: none; }
@@ -126,6 +140,7 @@
<span id="level-name">Уровень 1</span> <span id="level-name">Уровень 1</span>
<span id="box-count">📦 0/0</span> <span id="box-count">📦 0/0</span>
<span id="moves-count">👣 0</span> <span id="moves-count">👣 0</span>
<span id="top-btn" title="Топ-5 уровня">🏆</span>
<span id="menu-btn" title="В меню">🏠</span> <span id="menu-btn" title="В меню">🏠</span>
</div> </div>
<div id="controls"> <div id="controls">
@@ -135,6 +150,7 @@
<kbd>R</kbd> рестарт &nbsp; <kbd>R</kbd> рестарт &nbsp;
<kbd>F</kbd> автопоиск (<kbd>Esc</kbd> отмена) &nbsp; <kbd>F</kbd> автопоиск (<kbd>Esc</kbd> отмена) &nbsp;
<kbd>G</kbd> лучшее сохранённое решение &nbsp; <kbd>G</kbd> лучшее сохранённое решение &nbsp;
<kbd>T</kbd> топ-5 уровня &nbsp;
<kbd>M</kbd> в меню <kbd>M</kbd> в меню
</div> </div>
</div> </div>
@@ -147,6 +163,14 @@
</div> </div>
</div> </div>
<div id="top-overlay" class="hidden">
<div id="top-box">
<p id="top-title">🏆 Топ-5</p>
<div id="top-list"></div>
<button id="top-close">Закрыть</button>
</div>
</div>
<!-- Вставь ВЕСЬ дамп между тегами --> <!-- Вставь ВЕСЬ дамп между тегами -->
<script type="text/plain" id="levels-data"> <script type="text/plain" id="levels-data">
************************************* *************************************
@@ -2076,6 +2100,9 @@ window.addEventListener('keydown', e => {
const key = e.key.toLowerCase(); const key = e.key.toLowerCase();
if (key === 'escape' && (solveState || pendingSolution)) { cancelAutoSolve(); e.preventDefault(); return; } if (key === 'escape' && (solveState || pendingSolution)) { cancelAutoSolve(); e.preventDefault(); return; }
if (key === 'escape' && !document.getElementById('top-overlay').classList.contains('hidden')) {
document.getElementById('top-overlay').classList.add('hidden'); e.preventDefault(); return;
}
if (solveState || pendingSolution) { e.preventDefault(); return; } // поиск идёт или ждём клика по найденному — левел не трогаем if (solveState || pendingSolution) { e.preventDefault(); return; } // поиск идёт или ждём клика по найденному — левел не трогаем
// Системные клавиши — обрабатываем сразу // Системные клавиши — обрабатываем сразу
@@ -2085,6 +2112,7 @@ window.addEventListener('keydown', e => {
if (key === 'f') { startAutoSolve(); e.preventDefault(); return; } if (key === 'f') { startAutoSolve(); e.preventDefault(); return; }
if (key === 'g') { playBestSolution(); e.preventDefault(); return; } if (key === 'g') { playBestSolution(); e.preventDefault(); return; }
if (key === 'm') { backToMenu(); e.preventDefault(); return; } if (key === 'm') { backToMenu(); e.preventDefault(); return; }
if (key === 't') { showTop(); e.preventDefault(); return; }
// Стрелки и WASD // Стрелки и WASD
if (['arrowup','arrowdown','arrowleft','arrowright','w','a','s','d'].includes(key)) { if (['arrowup','arrowdown','arrowleft','arrowright','w','a','s','d'].includes(key)) {
@@ -2165,6 +2193,29 @@ function backToMenu() {
} }
document.getElementById('menu-btn').addEventListener('click', backToMenu); document.getElementById('menu-btn').addEventListener('click', backToMenu);
async function showTop() {
const rows = await topSolutions(LEVELS[currentLevel].name);
const list = document.getElementById('top-list');
document.getElementById('top-title').textContent = `🏆 Топ-5 — ${LEVELS[currentLevel].name}`;
if (!rows || !rows.length) {
list.innerHTML = '<div class="empty">Пока никто не прошёл этот уровень</div>';
} else {
list.innerHTML = rows.map((r, i) => {
const date = new Date(r.created_at * 1000).toLocaleDateString('ru-RU');
const icon = r.source === 'solver' ? '🤖' : '🧑';
return `<div class="row"><span class="rank">${i+1}.</span><span class="name">${icon} ${escapeHtml(r.name)}</span><span class="count">${r.move_count} ход.</span><span class="count">${date}</span></div>`;
}).join('');
}
document.getElementById('top-overlay').classList.remove('hidden');
}
function escapeHtml(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
document.getElementById('top-btn').addEventListener('click', showTop);
document.getElementById('top-close').addEventListener('click', () => document.getElementById('top-overlay').classList.add('hidden'));
document.getElementById('settings-save').addEventListener('click', async () => { document.getElementById('settings-save').addEventListener('click', async () => {
const name = document.getElementById('settings-name-input').value.trim(); const name = document.getElementById('settings-name-input').value.trim();
if (!name) return; if (!name) return;