Files
Learn_System/frontend/quantik.html
T
Maxim Dolgolyov 351251d652 @
feat(quantik-game): фаза 1 — оболочка игры + физ-уровень + прогресс (MVP)

Страница /quantik монтирует уровень-спеку в SimEngine (игровой режим: HUD из
Ф0 + слайдеры закона + play/reset), на победу шлёт результат и показывает
экран успеха (звёзды/время/попытки, inline SVG). Уровень phys-artillery-1
как данные (levels.js): гравитация + запуск тела из угла/скорости, портал,
бонус-звезда. Бэкенд: миграция 076 game_progress (UNIQUE user+level),
/api/game/progress (GET свой / POST upsert best time/stars, attempts++,
auth-only, валидация входа), клиент LS.gameProgress*, пункт сайдбара.
game.test.js 13/13; npm test 251 pass/8 baseline; lint:routes 0.
Уровень проверен на реальном интеграторе (311 выигрышных комбо, 31 на 3★).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-13 15:31:25 +03:00

120 lines
5.9 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Квантик — Законы Мира</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml"/>
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="/css/ls.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<style>
/* ── Раскладка игровой страницы ── */
.qg-wrap { display: flex; flex-direction: column; height: 100vh; min-height: 0; }
.qg-top {
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
padding: 12px 20px; border-bottom: 1px solid var(--border); background: var(--surface);
flex-shrink: 0;
}
.qg-title { font-family: 'Unbounded', sans-serif; font-weight: 800; font-size: 1.05rem; color: var(--text); white-space: nowrap; }
.qg-sub { font-size: .8rem; color: var(--text-3); flex: 1; min-width: 0; }
.qg-pill { font-size: .68rem; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; padding: 3px 10px; border-radius: 99px; background: rgba(34,211,238,0.14); color: #0e7c8a; }
/* сцена: на всю площадь, тёмный фон (full-bleed root движка растягивается inset:0) */
.qg-stage { flex: 1; min-height: 0; position: relative; background: #0D0D1A; overflow: hidden; }
.qg-stage .sim-spec-root { position: absolute; inset: 0; }
.qg-fallback { padding: 40px; color: #cbd5e1; font-family: 'Manrope', sans-serif; max-width: 520px; }
/* ── Экран успеха (оверлей) ── */
.qg-overlay {
position: absolute; inset: 0; z-index: 20;
display: flex; align-items: center; justify-content: center;
background: rgba(7, 7, 18, 0.72); backdrop-filter: blur(4px);
}
.qg-card {
background: var(--surface); border: 1px solid var(--border); border-radius: 18px;
padding: 28px 30px; width: min(420px, 90vw); text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.45);
animation: qg-pop .22s ease;
}
@keyframes qg-pop { from { transform: scale(.92); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.qg-card-title { font-family: 'Unbounded', sans-serif; font-weight: 800; font-size: 1.3rem; color: var(--text); margin-bottom: 14px; }
.qg-stars { display: flex; justify-content: center; gap: 6px; margin-bottom: 18px; }
.qg-star { display: inline-flex; }
.qg-star-svg { filter: drop-shadow(0 2px 6px rgba(251,191,36,0.4)); }
.qg-stats { display: flex; justify-content: center; gap: 22px; margin-bottom: 22px; }
.qg-stat { display: flex; flex-direction: column; gap: 3px; }
.qg-stat-lbl { font-size: .7rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--text-3); }
.qg-stat-val { font-family: 'Unbounded', sans-serif; font-weight: 700; font-size: 1.05rem; color: var(--text); font-variant-numeric: tabular-nums; }
.qg-actions { display: flex; justify-content: center; gap: 10px; }
.qg-btn { min-width: 110px; }
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="app-sidebar"></aside>
<main class="sb-content">
<div class="qg-wrap">
<div class="qg-top">
<span class="qg-title" id="qg-title">Квантик — Законы Мира</span>
<span class="qg-sub" id="qg-sub"></span>
<span class="qg-pill">Физика</span>
</div>
<div class="qg-stage" id="qg-stage"></div>
</div>
</main>
</div>
<script src="/js/api.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/notifications.js"></script>
<script src="/js/mobile.js"></script>
<!-- движок спек-симуляций (тот же путь, что lab.html / sim-builder.html) -->
<script src="/js/labs/_sim_expr.js"></script>
<script src="/js/labs/_sim_engine.js"></script>
<!-- KaTeX для подписей сцены -->
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<!-- уровни (данные) + логика игры -->
<script src="/js/game/levels.js"></script>
<script src="/js/game/quantik-game.js"></script>
<script>
(function () {
// Доступ: любой авторизованный пользователь (играют и ученики).
if (!LS.initPage()) { return; } // initPage сам редиректит на /login, если не авторизован
var stage = document.getElementById('qg-stage');
if (!window.SimEngine || !window.SimExpr || !window.QuantikLevels || !window.QuantikGame) {
stage.innerHTML = '<div class="qg-fallback">Движок игры не загрузился. Обновите страницу.</div>';
return;
}
// Уровень: ?level=<id> или первый из реестра (MVP — один уровень).
var params = new URLSearchParams(location.search);
var wantId = params.get('level');
var level = wantId ? window.QuantikLevels.get(wantId) : null;
if (!level) level = window.QuantikLevels.list()[0] || null;
if (!level) {
stage.innerHTML = '<div class="qg-fallback">Уровень не найден.</div>';
return;
}
document.getElementById('qg-title').textContent = level.title || 'Квантик';
document.getElementById('qg-sub').textContent = level.hint || '';
var inst = window.QuantikGame.start({ host: stage, level: level });
if (!inst) {
stage.innerHTML = '<div class="qg-fallback">Не удалось запустить уровень.</div>';
return;
}
window.__quantik = inst; // для отладки
})();
</script>
</body>
</html>