Files
Learn_System/frontend/red-book-games.html
T
Maxim Dolgolyov 5381679c68 chore: консолидация незакоммиченной работы (биохимия + System Health + lab/textbooks)
Зафиксирована накопленная незакоммиченная работа рабочего дерева, КРОМЕ файлов
учебника «Химия 7» (migration 046, chemistry_7_*.html, chem7_svg.js, тест —
оставлены незакоммиченными по запросу).

Включает: модуль биохимии (ядро BIO, 3D VSEPR, химдвижок, баланс, challenges,
пути из БД), System Health Level 1 (вердикт/мониторинг), а также frontend-
страницы и lab/textbooks-правки параллельной сессии.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 18:12:55 +03:00

651 lines
37 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="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;600;700;900&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="/css/ls.css"/>
<style>
:root {
--rb-bg: #0a1a0d;
--rb-surface: #111d13;
--rb-border: #1e3523;
--rb-accent: #4ade80;
--rb-text: #e2f5e8;
--rb-muted: #6b9a74;
--rb-cr: #ef4444; --rb-en: #f97316; --rb-vu: #eab308;
--rb-nt: #22c55e; --rb-lc: #3b82f6;
--sb-width: 240px;
}
body { background: var(--rb-bg); color: var(--rb-text); font-family: 'Manrope', sans-serif; }
.app-layout { background: var(--rb-bg); }
.sb-content { padding: 0; }
/* Sidebar */
.app-layout > .sidebar { background: var(--rb-surface) !important; border-right: 1px solid var(--rb-border) !important; }
.sb-brand { border-bottom: 1px solid var(--rb-border); padding: 16px 12px 12px; }
.sb-link, a.sb-link { color: var(--rb-muted) !important; }
.sb-link:hover { background: rgba(74,222,128,.08) !important; color: var(--rb-text) !important; }
.sb-link.active { background: rgba(74,222,128,.14) !important; color: var(--rb-accent) !important; font-weight:700; }
.sb-link.active::before { background: var(--rb-accent) !important; }
.sb-toggle { color: var(--rb-muted) !important; border-color: var(--rb-border) !important; background: transparent !important; }
.sb-toggle:hover { background: rgba(74,222,128,.12) !important; color: var(--rb-accent) !important; }
.nav-user-chip { border-color: var(--rb-border) !important; }
.nav-avatar { background: linear-gradient(135deg, #166534, #15803d) !important; }
.nav-user-name { color: var(--rb-muted) !important; }
.rb-brand-link { display:flex;align-items:center;gap:9px;text-decoration:none;flex:1;min-width:0;overflow:hidden; }
.rb-brand-icon { font-size:24px;line-height:1;flex-shrink:0; }
.rb-brand-text { font-family:'Unbounded',sans-serif;font-size:10px;font-weight:700;color:var(--rb-accent);line-height:1.4;white-space:nowrap; }
.rb-sb-section { font-size:9px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:var(--rb-muted);padding:14px 12px 4px;margin:0;opacity:.55; }
.app-layout.sb-collapsed .rb-sb-section { display:none; }
.rb-sb-divider { border:none;border-top:1px solid var(--rb-border);margin:8px 4px; }
.rb-back-link { opacity:.65;font-size:.82rem !important; }
.rb-back-link:hover { opacity:1 !important; }
/* Page */
.games-page { padding: 40px 40px 80px; max-width: 1000px; margin: 0 auto; }
/* Header */
.page-title { font-family: 'Unbounded', sans-serif; font-size: 24px; font-weight: 900; margin: 0 0 8px; }
.page-sub { color: var(--rb-muted); font-size: 13px; margin: 0 0 32px; }
/* Game selector */
.game-select { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; margin-bottom: 36px; }
.game-card {
background: var(--rb-surface); border: 1px solid var(--rb-border);
border-radius: 16px; padding: 24px 20px; cursor: pointer;
transition: all .2s; text-align: center;
}
.game-card:hover { border-color: var(--rb-accent); transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,.4); }
.game-card.active { border-color: var(--rb-accent); background: rgba(74,222,128,.08); }
.game-card-icon { font-size: 40px; margin-bottom: 12px; }
.game-card-title { font-family: 'Unbounded', sans-serif; font-size: 14px; font-weight: 700; margin: 0 0 6px; }
.game-card-desc { font-size: 12px; color: var(--rb-muted); margin: 0; line-height: 1.5; }
/* ── QUIZ ── */
#quiz-area { display:none; }
.quiz-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:24px; }
.quiz-score { background: rgba(74,222,128,.1); border: 1px solid rgba(74,222,128,.3); color: var(--rb-accent); font-weight:700; font-size:13px; padding:6px 16px; border-radius:10px; }
.quiz-card {
background: var(--rb-surface); border: 1px solid var(--rb-border);
border-radius: 20px; padding: 32px; margin-bottom: 24px; text-align: center;
}
.quiz-icon { font-size: 64px; margin-bottom: 16px; display: block; }
.quiz-question { font-family: 'Unbounded', sans-serif; font-size: 18px; font-weight: 700; margin: 0 0 8px; color: #fff; }
.quiz-hint { font-size: 13px; color: var(--rb-muted); margin: 0 0 24px; }
.quiz-options { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.quiz-opt {
background: rgba(255,255,255,.04); border: 1.5px solid var(--rb-border);
color: var(--rb-text); font-size: 13px; font-weight: 600;
padding: 14px 16px; border-radius: 12px; cursor: pointer;
transition: all .15s; text-align: left;
}
.quiz-opt:hover { border-color: var(--rb-accent); background: rgba(74,222,128,.07); }
.quiz-opt.correct { border-color: #22c55e; background: rgba(34,197,94,.15); color: #4ade80; }
.quiz-opt.wrong { border-color: #ef4444; background: rgba(239,68,68,.12); color: #fca5a5; }
.quiz-opt.disabled { cursor: default; pointer-events: none; }
.quiz-feedback {
background: rgba(74,222,128,.08); border: 1px solid rgba(74,222,128,.3);
border-radius: 12px; padding: 16px 20px; margin-top: 16px;
font-size: 13px; color: #c8e6ce; line-height: 1.6; display: none;
}
.quiz-feedback.wrong-bg { background: rgba(239,68,68,.08); border-color: rgba(239,68,68,.3); color: #fca5a5; }
.btn-next {
background: var(--rb-accent); color: #0a1a0d;
font-weight: 700; font-size: 14px; padding: 12px 28px;
border: none; border-radius: 12px; cursor: pointer; margin-top: 16px;
display: none;
}
.quiz-progress {
height: 4px; background: var(--rb-border); border-radius: 2px; margin-bottom: 20px;
}
.quiz-progress-fill { height: 100%; background: var(--rb-accent); border-radius: 2px; transition: width .4s; }
.quiz-result {
text-align: center; padding: 40px; display: none;
}
.quiz-result h2 { font-family: 'Unbounded', sans-serif; font-size: 24px; font-weight: 900; margin: 0 0 12px; }
.quiz-result p { font-size: 16px; color: var(--rb-muted); margin: 0 0 24px; }
/* ── FOOD CHAIN ── */
#chain-area { display:none; }
.chain-header { margin-bottom: 20px; }
.chain-header h3 { font-family: 'Unbounded', sans-serif; font-size: 16px; font-weight:700; margin:0 0 6px; }
.chain-header p { font-size:13px; color:var(--rb-muted); margin:0 0 16px; }
.chain-dropzones { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 28px; }
.chain-slot {
width: 110px; min-height: 90px;
background: rgba(255,255,255,.03); border: 2px dashed var(--rb-border);
border-radius: 14px; display: flex; flex-direction:column;
align-items: center; justify-content: center;
font-size: 11px; color: var(--rb-muted); font-weight:600;
transition: border-color .15s; cursor: pointer; padding: 8px;
text-align: center;
}
.chain-slot.over { border-color: var(--rb-accent); background: rgba(74,222,128,.08); }
.chain-slot.filled { border-style: solid; }
.chain-slot .slot-icon { font-size:28px; margin-bottom:4px; }
.chain-slot .slot-name { font-size:11px; color:var(--rb-text); font-weight:600; }
.chain-arrow { font-size: 20px; color: var(--rb-muted); flex-shrink: 0; }
.chain-pieces { display: flex; gap: 10px; flex-wrap: wrap; }
.chain-piece {
display: flex; flex-direction:column; align-items:center; gap:4px;
background: var(--rb-surface); border: 1.5px solid var(--rb-border);
border-radius: 12px; padding: 12px 14px; cursor: grab;
transition: all .15s; user-select: none; min-width: 90px; text-align:center;
}
.chain-piece:hover { border-color: var(--rb-accent); transform: translateY(-2px); }
.chain-piece.dragging { opacity: .5; }
.chain-piece.used { opacity: .3; pointer-events:none; }
.chain-piece-icon { font-size: 28px; }
.chain-piece-name { font-size: 11px; font-weight: 600; }
.chain-check-btn {
background: var(--rb-accent); color: #0a1a0d;
font-weight:700; font-size:14px; padding:12px 28px;
border:none; border-radius:12px; cursor:pointer; margin-right:10px;
}
.chain-reset-btn {
background: transparent; border: 1px solid var(--rb-border);
color: var(--rb-muted); font-size:13px; padding:12px 20px;
border-radius:12px; cursor:pointer;
}
.chain-result {
padding: 16px 20px; border-radius:12px; margin-top:16px; font-size:13px; font-weight:600;
display:none;
}
.chain-result.ok { background:rgba(74,222,128,.1);border:1px solid rgba(74,222,128,.3);color:var(--rb-accent); }
.chain-result.err { background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);color:#fca5a5; }
/* Toasts */
#rb-toasts { position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:10px;pointer-events:none; }
.rb-toast { display:flex;align-items:center;gap:10px;background:#0d2210;border:1px solid var(--rb-border);color:var(--rb-text);font-size:13px;font-weight:600;padding:12px 18px;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.5);pointer-events:auto;animation:toastIn .3s both; }
.rb-toast.success { border-color:rgba(74,222,128,.4); }
.rb-toast.error { border-color:rgba(239,68,68,.4);color:#fca5a5; }
.rb-toast.out { animation:toastOut .3s both; }
@keyframes toastIn { from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:translateX(0)} }
@keyframes toastOut { from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(20px)} }
/* ── Mobile ── */
@media (max-width: 768px) {
main.sb-content { overflow-y: auto; padding: 16px 14px 80px !important; }
.game-grid, .games-grid { grid-template-columns: 1fr !important; }
.chain-btns { display: flex; flex-wrap: wrap; gap: 8px; }
.chain-check-btn, .chain-reset-btn { flex: 1; min-width: 120px; padding: 10px; font-size: 13px; }
#rb-toasts { right: 14px; bottom: 80px; }
}
@media (max-width: 480px) {
.chain-pieces { gap: 6px; }
.chain-piece { min-width: 60px; padding: 8px 10px; font-size: 12px; }
}
</style>
</head>
<body>
<div class="app-layout" id="app">
<!-- RB Sidebar -->
<nav class="sidebar">
<div class="sb-brand">
<a href="/red-book.html" class="rb-brand-link">
<span class="rb-brand-icon"><svg class="ic" viewBox="0 0 24 24"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg></span>
<span class="sb-lbl rb-brand-text">Красная<br>книга РБ</span>
</a>
<button class="sb-toggle" onclick="toggleSidebar()" title="Свернуть"><i data-lucide="panel-left-close"></i></button>
</div>
<nav class="sb-nav">
<p class="rb-sb-section">РАЗДЕЛЫ</p>
<a href="/red-book.html" class="sb-link"><i data-lucide="leaf" class="sb-icon"></i><span class="sb-lbl">Каталог видов</span></a>
<a href="/classroom" class="sb-link"><i data-lucide="presentation" class="sb-icon"></i><span class="sb-lbl">Онлайн-урок</span></a>
<a href="/lesson-history" class="sb-link"><i data-lucide="archive" class="sb-icon"></i><span class="sb-lbl">Архив уроков</span></a>
<a href="/collection-rb.html" class="sb-link"><i data-lucide="star" class="sb-icon"></i><span class="sb-lbl">Моя коллекция</span></a>
<a href="/red-book-ecosystem.html" class="sb-link"><i data-lucide="git-fork" class="sb-icon"></i><span class="sb-lbl">Пищевые сети</span></a>
<a href="/red-book-biomes.html" class="sb-link"><i data-lucide="trees" class="sb-icon"></i><span class="sb-lbl">Биомы</span></a>
<a href="/red-book-games.html" class="sb-link active"><i data-lucide="gamepad-2" class="sb-icon"></i><span class="sb-lbl">Игры</span></a>
<hr class="rb-sb-divider">
<a href="/dashboard.html" class="sb-link rb-back-link"><i data-lucide="chevron-left" class="sb-icon"></i><span class="sb-lbl">Назад</span></a>
<a href="/profile.html" class="sb-link"><i data-lucide="user" class="sb-icon"></i><span class="sb-lbl">Профиль</span></a>
</nav>
<div class="sb-footer">
<div class="nav-user-chip" onclick="location.href='/profile.html'">
<div class="nav-avatar" id="nav-avatar">LS</div>
<span class="nav-user-name" id="nav-user"></span>
</div>
</div>
</nav>
<main class="sb-content">
<div class="games-page">
<h1 class="page-title"><svg class="ic" viewBox="0 0 24 24"><line x1="6" y1="12" x2="10" y2="12"/><line x1="8" y1="10" x2="8" y2="14"/><line x1="15" y1="13" x2="15.01" y2="13"/><line x1="18" y1="11" x2="18.01" y2="11"/><rect x="2" y="6" width="20" height="12" rx="2"/></svg> Игры</h1>
<p class="page-sub">Узнавай виды и восстанавливай экосистемы</p>
<!-- Game selector -->
<div class="game-select">
<div class="game-card" id="card-quiz" onclick="selectGame('quiz')">
<div class="game-card-icon"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></div>
<div class="game-card-title">Угадай вид</div>
<p class="game-card-desc">По иконке и подсказкам определи вид из Красной книги</p>
</div>
<div class="game-card" id="card-chain" onclick="selectGame('chain')">
<div class="game-card-icon"><svg class="ic" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></div>
<div class="game-card-title">Пищевая цепочка</div>
<p class="game-card-desc">Расставь виды в правильном порядке от продуцента до хищника</p>
</div>
</div>
<!-- QUIZ -->
<div id="quiz-area">
<div class="quiz-header">
<h3 style="margin:0;font-family:'Unbounded',sans-serif;font-size:16px">Угадай вид</h3>
<div style="display:flex;gap:10px;align-items:center">
<span id="quiz-progress-label" style="font-size:12px;color:var(--rb-muted)"></span>
<span class="quiz-score" id="quiz-score">0 / 0</span>
</div>
</div>
<div class="quiz-progress"><div class="quiz-progress-fill" id="quiz-progress-fill" style="width:0%"></div></div>
<div id="quiz-card-wrap">
<!-- filled by JS -->
</div>
<div id="quiz-result" class="quiz-result">
<div id="quiz-result-icon" style="font-size:64px;margin-bottom:16px"><svg class="ic" viewBox="0 0 24 24"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg></div>
<h2 id="quiz-result-title">Игра завершена!</h2>
<p id="quiz-result-text"></p>
<button onclick="startQuiz()" style="background:var(--rb-accent);color:#0a1a0d;font-weight:700;font-size:14px;padding:12px 28px;border:none;border-radius:12px;cursor:pointer;">Сыграть ещё</button>
</div>
</div>
<!-- FOOD CHAIN -->
<div id="chain-area">
<div class="chain-header">
<h3>Восстанови пищевую цепочку</h3>
<p>Расставь виды от продуцента (растения) до высшего хищника</p>
<div id="chain-level-selector" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px;"></div>
</div>
<div class="chain-dropzones" id="chain-slots"></div>
<div style="margin-bottom:16px;">
<p style="font-size:12px;color:var(--rb-muted);margin:0 0 12px">Перетащи виды в нужный порядок:</p>
<div class="chain-pieces" id="chain-pieces"></div>
</div>
<button class="chain-check-btn" onclick="checkChain()"><svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg> Проверить</button>
<button class="chain-reset-btn" onclick="resetChain()"><svg class="ic" viewBox="0 0 24 24"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.12"/></svg> Сбросить</button>
<div class="chain-result" id="chain-result"></div>
</div>
</div>
</main>
</div>
<div id="rb-toasts"></div>
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<script src="/js/api.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/mobile.js"></script>
<script>
/* ══════════════════════════════════════════════════════════════════════════
Init
══════════════════════════════════════════════════════════════════════════ */
let allSpecies = [];
let currentGame = null;
async function init() {
LS.hideDisabledFeatures?.();
lucide.createIcons();
if (localStorage.getItem('ls_sb_collapsed') === '1') document.getElementById('app').classList.add('sb-collapsed');
const user = LS.getUser?.();
if (user) {
document.getElementById('nav-user').textContent = user.name?.split(' ')[0] || '—';
LS.renderNavAvatar(document.getElementById('nav-avatar'), user);
}
const data = await LS.get('/api/red-book/species?limit=100').catch(() => ({ species: [] }));
allSpecies = data.species || [];
}
function selectGame(game) {
currentGame = game;
document.querySelectorAll('.game-card').forEach(c => c.classList.remove('active'));
document.getElementById(`card-${game}`).classList.add('active');
document.getElementById('quiz-area').style.display = game === 'quiz' ? 'block' : 'none';
document.getElementById('chain-area').style.display = game === 'chain' ? 'block' : 'none';
if (game === 'quiz') startQuiz();
if (game === 'chain') initChains();
}
/* ══════════════════════════════════════════════════════════════════════════
QUIZ — Угадай вид
══════════════════════════════════════════════════════════════════════════ */
const QUIZ_TOTAL = 10;
let quizState = { questions: [], idx: 0, score: 0, answered: false };
function buildQuizQuestions() {
const pool = [...allSpecies].sort(() => Math.random() - 0.5).slice(0, QUIZ_TOTAL);
return pool.map(correct => {
// 3 wrong answers from remaining species
const others = allSpecies.filter(s => s.id !== correct.id)
.sort(() => Math.random() - 0.5).slice(0, 3);
const options = [correct, ...others].sort(() => Math.random() - 0.5);
// Quiz type: either by icon or by description snippet
const type = Math.random() < 0.5 ? 'icon' : 'desc';
return { correct, options, type };
});
}
function startQuiz() {
if (allSpecies.length < 4) { showToast('Загрузите данные...', 'error'); return; }
quizState = { questions: buildQuizQuestions(), idx: 0, score: 0, answered: false };
document.getElementById('quiz-result').style.display = 'none';
updateScoreDisplay();
renderQuizCard();
}
function updateScoreDisplay() {
const { idx, score } = quizState;
document.getElementById('quiz-score').textContent = `${score} / ${QUIZ_TOTAL}`;
document.getElementById('quiz-progress-label').textContent = `${idx + 1} / ${QUIZ_TOTAL}`;
document.getElementById('quiz-progress-fill').style.width = `${(idx / QUIZ_TOTAL) * 100}%`;
}
function renderQuizCard() {
const { questions, idx } = quizState;
if (idx >= questions.length) { showQuizResult(); return; }
const q = questions[idx];
const wrap = document.getElementById('quiz-card-wrap');
let questionHtml, hintHtml;
if (q.type === 'icon') {
questionHtml = `<span class="quiz-icon">${q.correct.group_icon || '<svg class="ic" viewBox="0 0 24 24"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg>'}</span>
<p class="quiz-question">Что это за вид?</p>`;
hintHtml = `<p class="quiz-hint">Категория: <b style="color:var(--rb-${q.correct.category?.toLowerCase() || 'nt'})">${q.correct.category}</b></p>`;
} else {
const desc = q.correct.interesting_fact || q.correct.description?.slice(0,120) || '';
questionHtml = `<span class="quiz-icon"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>
<p class="quiz-question">Угадай вид по описанию:</p>`;
hintHtml = `<p class="quiz-hint" style="max-width:440px;margin-left:auto;margin-right:auto">"${desc}…"</p>`;
}
wrap.innerHTML = `
<div class="quiz-card">
${questionHtml}
${hintHtml}
<div class="quiz-options">
${q.options.map((sp, i) => `
<button class="quiz-opt" id="qopt-${i}" onclick="answerQuiz(${sp.id}, ${i})">
${sp.group_icon || ''} ${sp.name_ru}
<div style="font-size:10px;color:var(--rb-muted);font-style:italic">${sp.name_lat || ''}</div>
</button>`).join('')}
</div>
<div class="quiz-feedback" id="quiz-feedback"></div>
<button class="btn-next" id="btn-next" onclick="nextQuestion()">Далее <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg></button>
</div>`;
quizState.answered = false;
}
function answerQuiz(selectedId, optIdx) {
if (quizState.answered) return;
quizState.answered = true;
const q = quizState.questions[quizState.idx];
const correct = selectedId === q.correct.id;
if (correct) quizState.score++;
// Highlight options
q.options.forEach((sp, i) => {
const btn = document.getElementById(`qopt-${i}`);
btn.classList.add('disabled');
if (sp.id === q.correct.id) btn.classList.add('correct');
else if (i === optIdx && !correct) btn.classList.add('wrong');
});
// Show feedback
const fb = document.getElementById('quiz-feedback');
fb.style.display = 'block';
if (correct) {
fb.className = 'quiz-feedback';
fb.innerHTML = `<svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg> Верно! ${q.correct.interesting_fact || q.correct.description?.slice(0,120) || ''}`;
} else {
fb.className = 'quiz-feedback wrong-bg';
fb.innerHTML = `<svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Неверно. Правильный ответ: ${q.correct.name_ru} (${q.correct.name_lat || ''})`;
}
document.getElementById('btn-next').style.display = 'inline-block';
updateScoreDisplay();
}
function nextQuestion() {
quizState.idx++;
if (quizState.idx >= quizState.questions.length) { showQuizResult(); return; }
document.getElementById('quiz-progress-fill').style.width = `${(quizState.idx / QUIZ_TOTAL) * 100}%`;
document.getElementById('quiz-progress-label').textContent = `${quizState.idx + 1} / ${QUIZ_TOTAL}`;
renderQuizCard();
}
function showQuizResult() {
document.getElementById('quiz-card-wrap').innerHTML = '';
const r = document.getElementById('quiz-result');
r.style.display = 'block';
const pct = Math.round(quizState.score / QUIZ_TOTAL * 100);
document.getElementById('quiz-result-icon').innerHTML = pct >= 80 ? '<svg class="ic" viewBox="0 0 24 24"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2z"/></svg>' : pct >= 50 ? '<svg class="ic" viewBox="0 0 24 24"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg>' : '<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>';
document.getElementById('quiz-result-title').textContent =
pct >= 80 ? 'Отлично! Ты знаток Красной книги!' :
pct >= 50 ? 'Хороший результат!' : 'Нужно больше изучать!';
document.getElementById('quiz-result-text').textContent =
`Правильных ответов: ${quizState.score} из ${QUIZ_TOTAL} (${pct}%)`;
document.getElementById('quiz-progress-fill').style.width = '100%';
document.getElementById('quiz-progress-label').textContent = `${QUIZ_TOTAL} / ${QUIZ_TOTAL}`;
// Award XP if logged in
if (quizState.score >= 5) {
const xp = quizState.score * 5;
LS.post('/api/red-book/species/1/collect', {}).catch(() => {}); // just as XP trigger
showToast(`+${xp} XP за игру!`, 'success');
}
}
/* ══════════════════════════════════════════════════════════════════════════
FOOD CHAIN GAME
══════════════════════════════════════════════════════════════════════════ */
const CHAINS = [
{
id: 'forest',
title: 'Лесная цепь',
desc: 'Широколиственный лес',
chain: [
{ name: 'Дуб (желуди)', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M17 14 12 3 7 14"/><path d="M4 20 8 11h8l4 9"/><line x1="12" y1="20" x2="12" y2="22"/></svg>', level: 'Продуцент' },
{ name: 'Жук-олень', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M17 5C17 5 18 3 20 3s3 1 3 3-1 3-3 3h-2M17 5h-7M7 5C7 5 6 3 4 3S1 4 1 6s1 3 3 3h2M7 5v6M17 5v6M9 13a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2c0-2-1-5-1-7H10c0 2-1 5-1 7z"/><path d="M12 15v7"/></svg>', level: 'Консумент I' },
{ name: 'Воробьиный сыч', icon: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="14" r="4"/><path d="M12 4a4 4 0 0 0-4 4v2h8V8a4 4 0 0 0-4-4z"/><path d="M10 14h.01M14 14h.01"/></svg>', level: 'Консумент II' },
{ name: 'Рысь', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M12 5c.67 0 1.35.09 2 .26 1.78-2 5.03-2.84 6.42-2.26 1.4.58-.42 7-.42 7 .57 1.07 1 2.24 1 3.44C21 17.9 16.97 21 12 21s-9-3-9-7.56c0-1.25.5-2.4 1-3.44 0 0-1.89-6.42-.5-7 1.39-.58 4.72.23 6.5 2.23A9.04 9.04 0 0 1 12 5z"/><path d="M8 14v.5M16 14v.5"/></svg>', level: 'Консумент III' },
],
},
{
id: 'wetland',
title: 'Болотная цепь',
desc: 'Болото Беларуси',
chain: [
{ name: 'Камыш (водоросли)', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg>', level: 'Продуцент' },
{ name: 'Тритон', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M11 20c1.44-4.1 4.89-6 4.89-6s-2.95-2.11-4.89-6"/><path d="m2 13 2-5 5 5-5 3z"/><path d="M2 8s3 5 3 8"/><path d="M22 16c0 2.76-2.24 5-5 5s-5-2.24-5-5"/></svg>', level: 'Консумент I' },
{ name: 'Чёрный аист', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M16 7h.01"/><path d="M3.4 18H12a8 8 0 0 0 8-8V7a4 4 0 0 0-7.28-2.3L2 20"/><path d="m20 7 2 .5-2 .5"/><path d="M10 18v3M14 17.75v3.25"/><path d="M7 18a6 6 0 0 0 3.84-10.61"/></svg>', level: 'Консумент II' },
{ name: 'Орлан-белохвост', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M16 7h.01"/><path d="M3.4 18H12a8 8 0 0 0 8-8V7a4 4 0 0 0-7.28-2.3L2 20"/><path d="m20 7 2 .5-2 .5"/><path d="M10 18v3M14 17.75v3.25"/><path d="M7 18a6 6 0 0 0 3.84-10.61"/></svg>', level: 'Консумент III' },
],
},
{
id: 'river',
title: 'Речная цепь',
desc: 'Реки и озёра',
chain: [
{ name: 'Водоросли', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M7 20h10"/><path d="M10 20c5.5-2.5.8-6.4 3-10"/><path d="M9.5 9.4c1.1.8 1.8 2.2 2.3 3.7-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2 2.8-.5 4.4 0 5.5.8z"/><path d="M14.1 6a7 7 0 0 1 1.5 4.7c-1.7 1.3-3 1.4-4 1.1a3 3 0 0 1-1.8-1.9c.8-2 2.3-3.3 4.3-3.9z"/></svg>', level: 'Продуцент' },
{ name: 'Миног', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M6.5 12c.94-3.46 4.94-6 8.5-6 3.56 0 6.06 2.54 7 6-.94 3.47-3.44 6-7 6s-7.56-2.53-8.5-6z"/><path d="M18 12v.5"/><path d="M16 17.93a9.77 9.77 0 0 1 0-11.86"/><path d="M7 10.67C7 8 5.58 5.97 2.73 5.5c-1 3.98-.23 7.23 1.95 8.05C6.09 14.26 7 13.06 7 11.5v-1.01z"/></svg>', level: 'Консумент I' },
{ name: 'Выдра', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M12 17c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z"/><path d="M4 22 2 8l10 3 10-3-2 14"/></svg>', level: 'Консумент II' },
{ name: 'Волк', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M12 22c-4 0-8-2-10-6 0-4 2-8 5-10l2 4 2-2 1 3 2-4c3 2 5 6 5 10-2 4-6 5-7 5z"/></svg>', level: 'Консумент III' },
],
},
];
let chainState = { chainIdx: 0, slots: [], dragging: null };
function initChains() {
const sel = document.getElementById('chain-level-selector');
sel.innerHTML = CHAINS.map((c, i) => `
<button onclick="loadChain(${i})" id="chain-btn-${i}"
style="background:transparent;border:1px solid var(--rb-border);color:var(--rb-muted);font-size:12px;font-weight:600;padding:6px 14px;border-radius:10px;cursor:pointer;">
${c.icon || ''} ${c.title}
</button>`).join('');
loadChain(0);
}
function loadChain(idx) {
chainState.chainIdx = idx;
chainState.slots = new Array(CHAINS[idx].chain.length).fill(null);
chainState.dragging = null;
// Update active button
CHAINS.forEach((_, i) => {
const btn = document.getElementById(`chain-btn-${i}`);
if (btn) {
btn.style.borderColor = i === idx ? 'var(--rb-accent)' : 'var(--rb-border)';
btn.style.color = i === idx ? 'var(--rb-accent)' : 'var(--rb-muted)';
}
});
const c = CHAINS[idx];
document.getElementById('chain-result').style.display = 'none';
// Render slots
const slotsEl = document.getElementById('chain-slots');
slotsEl.innerHTML = c.chain.map((_, i) => `
<div class="chain-slot" id="chain-slot-${i}"
ondragover="e.preventDefault();this.classList.add('over')"
ondragleave="this.classList.remove('over')"
ondrop="dropToSlot(event,${i})">
<span style="font-size:11px;color:var(--rb-muted)">${_.level}</span>
</div>
${i < c.chain.length - 1 ? '<div class="chain-arrow"><svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg></div>' : ''}`
).join('');
// Render shuffled pieces
const shuffled = [...c.chain].sort(() => Math.random() - 0.5);
const piecesEl = document.getElementById('chain-pieces');
piecesEl.innerHTML = shuffled.map((item, i) => `
<div class="chain-piece" id="chain-piece-${i}" draggable="true"
data-name="${item.name}" data-icon="${item.icon}"
ondragstart="dragStart(event,${i})">
<span class="chain-piece-icon">${item.icon}</span>
<span class="chain-piece-name">${item.name}</span>
</div>`).join('');
}
function dragStart(e, pieceIdx) {
chainState.dragging = pieceIdx;
e.dataTransfer.setData('text/plain', pieceIdx);
document.getElementById(`chain-piece-${pieceIdx}`)?.classList.add('dragging');
}
function dropToSlot(e, slotIdx) {
e.preventDefault();
const slotEl = document.getElementById(`chain-slot-${slotIdx}`);
slotEl?.classList.remove('over');
const pieceIdx = parseInt(e.dataTransfer.getData('text/plain'));
const pieceEl = document.getElementById(`chain-piece-${pieceIdx}`);
if (!pieceEl) return;
const name = pieceEl.dataset.name;
const icon = pieceEl.dataset.icon;
// If slot already has something, put it back
const prev = chainState.slots[slotIdx];
if (prev !== null) {
// Find piece with that name and un-hide it
document.querySelectorAll('.chain-piece').forEach(p => {
if (p.dataset.name === chainState.slots[slotIdx]?.name) p.classList.remove('used');
});
}
chainState.slots[slotIdx] = { name, icon };
pieceEl.classList.add('used');
pieceEl.classList.remove('dragging');
// Update slot display
if (slotEl) {
slotEl.classList.add('filled');
slotEl.innerHTML = `
<div class="slot-icon">${icon}</div>
<div class="slot-name">${name}</div>
<span onclick="clearSlot(${slotIdx})" style="font-size:10px;color:var(--rb-muted);margin-top:4px;cursor:pointer"><svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></span>`;
}
}
function clearSlot(slotIdx) {
const slot = chainState.slots[slotIdx];
if (!slot) return;
chainState.slots[slotIdx] = null;
// Un-hide matching piece
document.querySelectorAll('.chain-piece').forEach(p => {
if (p.dataset.name === slot.name) p.classList.remove('used');
});
// Reset slot display
const c = CHAINS[chainState.chainIdx];
const slotEl = document.getElementById(`chain-slot-${slotIdx}`);
if (slotEl) {
slotEl.classList.remove('filled');
slotEl.innerHTML = `<span style="font-size:11px;color:var(--rb-muted)">${c.chain[slotIdx].level}</span>`;
}
}
function checkChain() {
const c = CHAINS[chainState.chainIdx];
const res = document.getElementById('chain-result');
// Check all filled
if (chainState.slots.some(s => s === null)) {
res.style.display = 'block';
res.className = 'chain-result err';
res.innerHTML = '<svg class="ic" viewBox="0 0 24 24"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> Заполни все ячейки цепочки';
return;
}
const correct = c.chain.every((item, i) => chainState.slots[i]?.name === item.name);
res.style.display = 'block';
if (correct) {
res.className = 'chain-result ok';
res.innerHTML = '<svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg> Верно! Пищевая цепочка составлена правильно. +25 XP';
showToast('<svg class="ic" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg> Цепочка верна! +25 XP', 'success');
} else {
res.className = 'chain-result err';
const correctOrder = c.chain.map(i => `${i.icon} ${i.name}`).join(' <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> ');
res.innerHTML = `<svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Неверно. Правильный порядок: ${correctOrder}`;
}
}
function resetChain() {
loadChain(chainState.chainIdx);
}
/* ══════════════════════════════════════════════════════════════════════════
Helpers
══════════════════════════════════════════════════════════════════════════ */
function toggleSidebar() {
const app = document.getElementById('app');
app.classList.toggle('sb-collapsed');
localStorage.setItem('ls_sb_collapsed', app.classList.contains('sb-collapsed') ? '1' : '0');
lucide.createIcons();
}
function showToast(msg, type = 'success', duration = 3000) {
const wrap = document.getElementById('rb-toasts');
const t = document.createElement('div');
t.className = `rb-toast ${type}`;
t.innerHTML = msg;
wrap.appendChild(t);
setTimeout(() => { t.classList.add('out'); t.addEventListener('animationend', () => t.remove()); }, duration);
}
init();
</script>
</body>
</html>