Files
Maxim Dolgolyov 437be55a88 @
fix(chemistry-8): не прокручивать страницу вниз при переключении параграфов

Автофокус поля ответа (renderTask) браузер сопровождал прокруткой к блоку
задач внизу секции, перебивая scrollTo(top:0). Добавлен focus({preventScroll:true}).

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

431 lines
27 KiB
JavaScript
Raw Permalink 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.
/* chem8_engine.js — общий движок интерактивных учебников «Химия 8».
*
* Воспроизводит каркас учебников физики: SPA с para-selector, ленивая сборка §,
* карточки теории (makeCard), тренажёр задач (числовой ввод + MCQ), sidebar-шпаргалка,
* прогресс/XP/уровни/достижения, серверная синхронизация прогресса, тема.
*
* Страница главы ОБЪЯВЛЯЕТ данные (до загрузки движка, инлайн-скриптом):
* window.CHEM8_CFG = { slug, themeKey, xpKey, progKey, achKey, hubHref }
* window.PARAS = [{id, num, name, sub, final?}]
* window.BUILDERS = { p1: ()=>build_p1(), ... } // наполняют #<id>-body
* window.POOLS = { p1: [task,...], ... } // task: {q,hint,unit,a,ex,tol} | {q,opts,a,ex}
* window.SIDEBARS = { p1: {title, rows:[[k,v],...]}, ... }
* window.TIPS = [{sec, html}, ...]
* window.CHEM8_WIDGETS = { p1: ()=>add_p1(), ... } // монтаж виджетов §
* window.FLAG_MOUNTS = { p6: ()=>mountFlag('p6'), ... } // флагман-интерактивы
* window.ACH_LABELS = { start, p1_done, ... }
*
* Движок ЭКСПОРТИРУЕТ на window: goTo, checkNum, selectMcq, nextTask, goToTask,
* resetTasks, makeCard, secNav, readButton, addXp, achievement, bumpProgress.
* Инициализация — на DOMContentLoaded.
*/
(function (W) {
'use strict';
// Конфиг резолвится лениво в init() — страница задаёт window.CHEM8_CFG
// в body-скрипте, который при defer выполняется до движка, но не полагаемся на это.
var CFG = {}, SLUG = 'chemistry-8';
var K = { theme: 'chemistry8_theme', xp: 'chemistry8_xp', prog: 'chemistry-8_progress', ach: 'chemistry-8_ach' };
function resolveCfg() {
CFG = W.CHEM8_CFG || {};
SLUG = CFG.slug || 'chemistry-8';
K = {
theme: CFG.themeKey || 'chemistry8_theme',
xp: CFG.xpKey || 'chemistry8_xp',
prog: CFG.progKey || (SLUG + '_progress'),
ach: CFG.achKey || (SLUG + '_ach')
};
}
function PARAS() { return W.PARAS || []; }
function POOLS() { return W.POOLS || {}; }
function BUILDERS(){ return W.BUILDERS || {}; }
function ACHL() { return W.ACH_LABELS || {}; }
var STATE = { current: null, progress: {}, achievements: new Map(), xp: 0, level: 1 };
var SEC = {}; // STATE задач по секциям
/* ── XP / уровни ───────────────────────────────────────────────── */
function calcLevel(xp) { return Math.floor(Math.sqrt((xp || 0) / 100)) + 1; }
function xpForLevel(lv) { return (lv - 1) * (lv - 1) * 100; }
function loadProgress() {
try {
var s = localStorage.getItem(K.prog); if (s) Object.assign(STATE.progress, JSON.parse(s));
var a = localStorage.getItem(K.ach);
if (a) { var p = JSON.parse(a); if (p && typeof p === 'object') for (var id in p) STATE.achievements.set(id, p[id]); }
STATE.xp = parseInt(localStorage.getItem(K.xp) || '0', 10) || 0;
STATE.level = calcLevel(STATE.xp);
} catch (e) {}
}
function saveProgress() {
try {
localStorage.setItem(K.prog, JSON.stringify(STATE.progress));
localStorage.setItem(K.ach, JSON.stringify(mapToObj(STATE.achievements)));
localStorage.setItem(K.xp, String(STATE.xp));
} catch (e) {}
}
function mapToObj(m) { var o = {}; m.forEach(function (v, k) { o[k] = v; }); return o; }
function addXp(n, src) {
if (!n) return;
var prev = STATE.level;
STATE.xp = Math.max(0, (STATE.xp || 0) + n); STATE.level = calcLevel(STATE.xp);
saveProgress(); refreshUI();
try { if (W.LS && W.LS.xp && W.LS.xp.add) W.LS.xp.add(n, SLUG + '-' + (src || 'x')); } catch (e) {}
if (STATE.level > prev) popup('Уровень ' + STATE.level + '!');
}
function bumpProgress(key, delta) {
STATE.progress[key] = Math.max(0, Math.min(100, (STATE.progress[key] || 0) + delta));
saveProgress(); refreshUI();
if (STATE.progress[key] >= 50) markServerRead(key);
}
function achievement(id, text) {
if (STATE.achievements.has(id)) return;
var label = text || ACHL()[id] || id;
STATE.achievements.set(id, label); saveProgress();
popup(label, true);
addXp(20, 'ach-' + id);
}
/* ── серверная синхронизация ───────────────────────────────────── */
var _marked = {}, _pending = null, _timer = null;
function _flush() {
var body = _pending; _pending = null; if (!body) return;
var tok = (W.LS && W.LS.getToken) ? W.LS.getToken() : ''; if (!tok) return;
fetch('/api/textbooks/' + SLUG + '/progress', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + tok },
body: JSON.stringify(body), keepalive: true
}).catch(function () {});
}
function _queue(p) { _pending = Object.assign(_pending || {}, p); if (_timer) clearTimeout(_timer); _timer = setTimeout(_flush, 600); }
function markServerRead(id) { if (_marked[id] || /^final/.test(id)) return; _marked[id] = 1; _queue({ mark_read: id }); }
function markLastPara(id) { _queue({ last_para: id }); }
function loadServerReadState() {
var tok = (W.LS && W.LS.getToken) ? W.LS.getToken() : ''; if (!tok) return;
fetch('/api/textbooks/' + SLUG, { headers: { 'Authorization': 'Bearer ' + tok } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
if (!d || !d.progress || !d.progress.read) return;
d.progress.read.forEach(function (k) { _marked[k] = 1; if ((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100; });
saveProgress(); refreshUI();
}).catch(function () {});
}
W.addEventListener('beforeunload', _flush);
/* ── popup ачивки / уровня ─────────────────────────────────────── */
function popup(text, gold) {
var pop = document.getElementById('ach-popup'); if (!pop) return;
var t = document.getElementById('ach-text'); if (t) t.textContent = text;
pop.classList.toggle('gold', !!gold);
pop.classList.add('show'); setTimeout(function () { pop.classList.remove('show'); }, 3000);
if (gold) { try { if (W.confetti) W.confetti({ particleCount: 160, spread: 95, origin: { y: .65 } }); } catch (e) {} }
}
/* ── para-selector + hero ──────────────────────────────────────── */
function buildParaSelector() {
var g = document.getElementById('psel-grid'); if (!g) return;
g.innerHTML = '';
PARAS().forEach(function (p) {
var card = document.createElement('div');
card.className = 'psel-card' + (p.final ? ' final' : '');
card.dataset.id = p.id; card.dataset.progCard = p.id;
card.innerHTML = '<div class="psel-num">' + p.num + '</div><div class="psel-name">' + p.name + '</div>'
+ (p.sub ? '<div class="psel-sub">' + p.sub + '</div>' : '')
+ '<div class="psel-prog"><div class="psel-prog-fill"></div></div>'
+ '<span class="psel-done"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></span>';
card.addEventListener('click', function () { goTo(p.id); });
g.appendChild(card);
});
if (W.renderMathInElement) try { renderMath(g); } catch (e) {}
}
function refreshUI() {
var total = PARAS().length || 1;
var sum = 0; PARAS().forEach(function (p) { sum += (STATE.progress[p.id] || 0); });
var pct = Math.round(sum / total);
var hf = document.getElementById('hero-hp-fill'); if (hf) hf.style.width = pct + '%';
var ht = document.getElementById('hero-hp-text'); if (ht) ht.textContent = pct + '%';
var xb = document.getElementById('hero-xp-badge');
if (xb) xb.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 22 20 2 20"/></svg> Ур. ' + STATE.level + ' \xb7 ' + (STATE.xp || 0) + ' XP';
document.querySelectorAll('.psel-card').forEach(function (c) {
var id = c.dataset.id; var pp = STATE.progress[id] || 0;
var fl = c.querySelector('.psel-prog-fill'); if (fl) fl.style.width = pp + '%';
c.classList.toggle('done', pp >= 50);
});
if (STATE.current && document.getElementById('sidebar-content')) { try { buildSidebar(STATE.current); } catch (e) {} }
}
/* ── ленивая сборка § + инъекция задач ─────────────────────────── */
var BUILT = {};
function ensureBuilt(id) {
if (BUILT[id]) return;
var fn = BUILDERS()[id];
if (fn) { try { fn(); } catch (e) { if (W.console) console.warn('build ' + id, e.message); } BUILT[id] = 1; }
_injectTasks(id);
_mountWidgets(id);
}
function _mountWidgets(id) {
setTimeout(function () {
try { if (W.CHEM8_WIDGETS && W.CHEM8_WIDGETS[id]) W.CHEM8_WIDGETS[id](); } catch (e) { if (W.console) console.warn('widget ' + id, e.message); }
try { if (W.FLAG_MOUNTS && W.FLAG_MOUNTS[id]) W.FLAG_MOUNTS[id](); } catch (e) { if (W.console) console.warn('flag ' + id, e.message); }
}, 40);
}
function _makeTaskBlock(sec) {
return '<div class="legacy-tasks" id="ptab-' + sec + '">'
+ '<div class="lt-head"><span class="lt-title">Задачи параграфа</span>'
+ '<span class="chip chip-ok"><span id="ok' + sec + '">0</span> верно</span>'
+ '<span class="chip chip-tot"><span id="cur' + sec + '">0</span>/<span id="max' + sec + '">?</span></span>'
+ '<button class="btn lt-reset" onclick="resetTasks(\'' + sec + '\')">Заново</button></div>'
+ '<div class="prog-wrap"><div id="prog' + sec + '" class="prog-fill"></div></div>'
+ '<div class="nav-dots" id="navDots' + sec + '"></div>'
+ '<div id="taskArea' + sec + '"></div>'
+ '<div class="feedback" id="fb' + sec + '"></div>'
+ '<div class="lt-foot"><button class="btn primary" id="nextBtn' + sec + '" onclick="nextTask(\'' + sec + '\')" style="display:none">Следующая &rarr;</button></div>'
+ '<div class="summary" id="sum' + sec + '"><div class="sum-t">Параграф пройден!</div><div class="big-score" id="sumScore' + sec + '"></div><div class="sum-grade" id="sumGrade' + sec + '"></div></div>'
+ '</div>';
}
function _injectTasks(id) {
var pool = POOLS()[id]; if (!pool) return;
var body = document.getElementById(id + '-body'); if (!body || body.querySelector('.legacy-tasks')) return;
if (!SEC[id]) SEC[id] = { idx: 0, results: pool.map(function () { return null; }), selections: pool.map(function () { return null; }), answered: false };
body.insertAdjacentHTML('beforeend', _makeTaskBlock(id));
setTimeout(function () { try { renderTask(id); } catch (e) {} }, 50);
}
/* ── навигация по § ────────────────────────────────────────────── */
function goTo(id) {
STATE.current = id; ensureBuilt(id);
document.querySelectorAll('.sec').forEach(function (s) { s.classList.remove('active'); });
var el = document.getElementById('sec-' + id); if (el) el.classList.add('active');
document.querySelectorAll('.psel-card').forEach(function (c) { c.classList.toggle('active', c.dataset.id === id); });
buildSidebar(id);
try { W.scrollTo({ top: 0, behavior: 'smooth' }); } catch (e) {}
if ((STATE.progress[id] || 0) < 10) bumpProgress(id, 10);
if (W.renderMathInElement && el) setTimeout(function () { renderMath(el); }, 0);
markLastPara(id);
}
/* ── sidebar ───────────────────────────────────────────────────── */
function buildSidebar(id) {
var box = document.getElementById('sidebar-content'); if (!box) return;
var SB = W.SIDEBARS || {}; var sb = SB[id] || SB[(PARAS()[0] || {}).id] || { title: '', rows: [] };
var xpLv = xpForLevel(STATE.level), xpNext = xpForLevel(STATE.level + 1);
var pct = (xpNext - xpLv) > 0 ? Math.round((STATE.xp - xpLv) / (xpNext - xpLv) * 100) : 100;
var html = '<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><span class="xp-level">Ур. ' + STATE.level + '</span></div>'
+ '<div class="xp-bar"><div class="xp-fill" style="width:' + pct + '%"></div></div>'
+ '<div class="xp-nums"><span>' + STATE.xp + ' XP</span><span>' + xpNext + ' XP</span></div></div>';
html += '<div class="sidecard"><h4>' + sb.title + '</h4>';
sb.rows.forEach(function (r) { html += '<div class="sidecard-row"><b>' + r[0] + '</b>' + (r[1] ? ' — ' + r[1] : '') + '</div>'; });
html += '</div>';
var tips = W.TIPS || []; var tip = tips.filter(function (t) { return t.sec === id; })[0] || tips[0];
if (tip) html += '<div class="sidecard tip"><h4><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 22 20 2 20"/></svg> Подсказка</h4><div class="sidecard-row" style="font-size:.84rem;line-height:1.55">' + tip.html + '</div></div>';
if (STATE.achievements.size > 0) {
html += '<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">' + STATE.achievements.size + '</span></h4>';
var vals = []; STATE.achievements.forEach(function (v) { vals.push(v); });
vals.slice(-4).forEach(function (t) { html += '<div class="sidecard-row done">✓ ' + t + '</div>'; });
html += '</div>';
}
box.innerHTML = html;
if (W.renderMathInElement) try { renderMath(box); } catch (e) {}
}
/* ── карточки / навигация / кнопка прочтения ───────────────────── */
var ICONS = {
theory: '<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
example: '<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
rule: '<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
lab: '<svg class="ic" viewBox="0 0 24 24"><path d="M10 2v7.5L4.5 19a2 2 0 0 0 1.7 3h11.6a2 2 0 0 0 1.7-3L14 9.5V2"/><line x1="9" y1="2" x2="15" y2="2"/></svg>'
};
function makeCard(kind, title, num, body) {
var labels = { theory: 'Теория', example: 'Пример', rule: 'Правило', lab: 'Практика' };
return '<div class="card"><div class="card-header"><div class="card-icon ' + kind + '">' + (ICONS[kind] || ICONS.theory) + '</div>'
+ '<div class="card-title">' + (labels[kind] || '') + (title && title !== labels[kind] ? ' \xb7 ' + title : '') + '</div>'
+ (num ? '<div class="card-num">' + num + '</div>' : '') + '</div><div class="card-body">' + body + '</div></div>';
}
function paraName(id) { var p = PARAS().filter(function (x) { return x.id === id; })[0]; return p ? p.num : id; }
function secNav(prev, next) {
var h = '<div class="sec-nav">';
h += prev ? '<button class="btn" onclick="goTo(\'' + prev + '\')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> ' + paraName(prev) + '</button>' : '<span></span>';
h += next ? '<button class="btn primary" onclick="goTo(\'' + next + '\')">' + paraName(next) + ' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>' : '<span></span>';
return h + '</div>';
}
function readButton(paraId) {
var p = PARAS().filter(function (x) { return x.id === paraId; })[0];
var tail = p && p.final ? 'финал' : (p ? p.num : '?');
return '<div class="read-wrap"><button class="btn primary" id="' + paraId + '-read-btn">'
+ '<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg> Я изучил — ' + tail + ' (+10 XP)</button></div>';
}
function wireReadBtn(paraId) {
var btn = document.getElementById(paraId + '-read-btn'); if (!btn || btn._wired) return; btn._wired = 1;
btn.addEventListener('click', function () {
addXp(10, paraId + '-read'); bumpProgress(paraId, 30);
btn.textContent = 'Изучено! +10 XP'; btn.disabled = true; btn.style.opacity = .6;
var aId = paraId + '_done'; if (ACHL()[aId]) achievement(aId);
});
}
function renderMath(root) {
if (!W.renderMathInElement) return;
try { W.renderMathInElement(root, { delimiters: [{ left: '$$', right: '$$', display: true }, { left: '$', right: '$', display: false }], throwOnError: false }); } catch (e) {}
}
function doRender(el) { renderMath(el); }
/* ── ДВИЖОК ЗАДАЧ ──────────────────────────────────────────────── */
function renderTask(sec) {
var pool = POOLS()[sec], s = SEC[sec];
var area = document.getElementById('taskArea' + sec), fb = document.getElementById('fb' + sec), sum = document.getElementById('sum' + sec);
if (!area || !fb || !sum || !pool || !s) return;
sum.classList.remove('show');
var q = pool[s.idx], done = s.results[s.idx] !== null, isMcq = !!q.opts;
s.answered = done;
if (isMcq) {
var selIdx = s.selections[s.idx];
area.innerHTML = '<div class="task-card"><div class="task-num">Задача ' + (s.idx + 1) + ' из ' + pool.length + ' · Тест</div>'
+ '<div class="task-text">' + q.q + '</div><div class="mcq-opts">'
+ q.opts.map(function (opt, i) {
var cls = 'mcq-opt'; if (done) { if (i === q.a) cls += ' mcq-cor'; else if (i === selIdx) cls += ' mcq-wrong'; }
return '<button class="' + cls + '" id="mcqOpt' + sec + '_' + i + '" onclick="' + (done ? '' : 'selectMcq(\'' + sec + '\',' + i + ')') + '" ' + (done ? 'disabled' : '') + '><span class="mcq-let">' + String.fromCharCode(65 + i) + '.</span>' + opt + '</button>';
}).join('') + '</div></div>';
} else {
area.innerHTML = '<div class="task-card"><div class="task-num">Задача ' + (s.idx + 1) + ' из ' + pool.length + '</div>'
+ '<div class="task-text">' + q.q + '</div>'
+ (q.hint ? '<div class="task-hint"><svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6M10 22h4M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg><span>' + q.hint + '</span></div>' : '')
+ '<div class="ans-row"><label>Ответ:</label><input class="ans-inp" type="text" id="ainp' + sec + '" placeholder="?" autocomplete="off"' + (done ? ' disabled' : '') + '>'
+ '<span class="unit-lbl">' + (q.unit || '') + '</span>'
+ (done ? '' : '<button class="btn primary" onclick="checkNum(\'' + sec + '\')">Проверить</button>') + '</div></div>';
}
if (done) {
var ok = s.results[s.idx];
fb.className = 'feedback show ' + (ok ? 'fb-ok' : 'fb-fail');
fb.innerHTML = isMcq
? (ok ? 'Верно! ' + (q.ex || '') : 'Неверно. Правильный ответ: <b>' + q.opts[q.a] + '</b>. ' + (q.ex || ''))
: (ok ? 'Верно! ' + (q.ex || '') : 'Неверно. Правильный ответ: <b>' + q.a + ' ' + (q.unit || '') + '</b>. ' + (q.ex || ''));
var nb = document.getElementById('nextBtn' + sec); if (nb) nb.style.display = 'inline-flex';
doRender(fb);
} else { fb.className = 'feedback'; var nb2 = document.getElementById('nextBtn' + sec); if (nb2) nb2.style.display = 'none'; }
updateScoreBar(sec); renderNav(sec); doRender(area);
if (!done && !isMcq) {
var inp = document.getElementById('ainp' + sec);
// preventScroll: иначе фокус прокручивает страницу к блоку задач (внизу §)
setTimeout(function () { if (inp) { try { inp.focus({ preventScroll: true }); } catch (e) { inp.focus(); } } }, 80);
if (inp) inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') checkNum(sec); });
}
}
function selectMcq(sec, i) {
var s = SEC[sec]; if (!s || s.answered) return;
var q = POOLS()[sec][s.idx], ok = i === q.a;
s.results[s.idx] = ok; s.selections[s.idx] = i; s.answered = true;
if (ok) maybeAwardTask(sec);
q.opts.forEach(function (_, j) {
var btn = document.getElementById('mcqOpt' + sec + '_' + j); if (!btn) return;
btn.disabled = true; if (j === q.a) btn.classList.add('mcq-cor'); else if (j === i && !ok) btn.classList.add('mcq-wrong');
});
var fb = document.getElementById('fb' + sec);
fb.className = 'feedback show ' + (ok ? 'fb-ok' : 'fb-fail');
fb.innerHTML = ok ? 'Верно! ' + (q.ex || '') : 'Неверно. Правильный ответ: <b>' + q.opts[q.a] + '</b>. ' + (q.ex || '');
doRender(fb);
var nb = document.getElementById('nextBtn' + sec); if (nb) nb.style.display = 'inline-flex';
updateScoreBar(sec); renderNav(sec); finishCheck(sec);
}
function checkNum(sec) {
var s = SEC[sec]; if (!s || s.answered) return;
var q = POOLS()[sec][s.idx], inp = document.getElementById('ainp' + sec), fb = document.getElementById('fb' + sec);
var val = (inp.value || '').trim().replace(',', '.'), num = parseFloat(val);
if (!val || isNaN(num)) { fb.className = 'feedback show fb-fail'; fb.innerHTML = 'Введите числовой ответ!'; return; }
s.answered = true;
var tol = q.tol !== undefined ? q.tol : 0.03;
var ok = q.a === 0 ? Math.abs(num) < 0.05 : Math.abs((num - q.a) / q.a) < tol;
s.results[s.idx] = ok; if (ok) maybeAwardTask(sec);
inp.disabled = true; inp.style.borderColor = ok ? 'var(--ok)' : 'var(--fail)';
fb.className = 'feedback show ' + (ok ? 'fb-ok' : 'fb-fail');
fb.innerHTML = ok ? 'Верно! ' + (q.ex || '') : 'Неверно. Правильный ответ: <b>' + q.a + ' ' + (q.unit || '') + '</b>. ' + (q.ex || '');
doRender(fb);
var nb = document.getElementById('nextBtn' + sec); if (nb) nb.style.display = 'inline-flex';
updateScoreBar(sec); renderNav(sec); finishCheck(sec);
}
function maybeAwardTask(sec) {
var s = SEC[sec]; if (s._awarded === undefined) s._awarded = {};
if (s._awarded[s.idx]) return; s._awarded[s.idx] = 1; addXp(5, sec + '-task');
}
function finishCheck(sec) {
var s = SEC[sec];
if (s.results.every(function (r) { return r !== null; })) setTimeout(function () { showSummary(sec); }, 1600);
}
function nextTask(sec) {
var s = SEC[sec], pool = POOLS()[sec];
var next = -1;
for (var k = 1; k <= pool.length; k++) { var j = (s.idx + k) % pool.length; if (s.results[j] === null) { next = j; break; } }
if (next === -1) { showSummary(sec); return; }
s.idx = next; s.answered = s.results[next] !== null; renderTask(sec);
}
function goToTask(sec, idx) { var s = SEC[sec]; s.idx = idx; s.answered = s.results[idx] !== null; renderTask(sec); }
function resetTasks(sec) {
var pool = POOLS()[sec];
SEC[sec] = { idx: 0, results: pool.map(function () { return null; }), selections: pool.map(function () { return null; }), answered: false, _awarded: {} };
var sum = document.getElementById('sum' + sec); if (sum) sum.classList.remove('show');
renderTask(sec);
}
function renderNav(sec) {
var s = SEC[sec], pool = POOLS()[sec], nd = document.getElementById('navDots' + sec); if (!nd) return;
nd.innerHTML = pool.map(function (_, i) {
var cls = 'nav-dot'; if (i === s.idx) cls += ' nd-cur'; if (s.results[i] === true) cls += ' nd-ok'; else if (s.results[i] === false) cls += ' nd-fail';
return '<button class="' + cls + '" onclick="goToTask(\'' + sec + '\',' + i + ')">' + (i + 1) + '</button>';
}).join('');
}
function updateScoreBar(sec) {
var s = SEC[sec], pool = POOLS()[sec];
var ok = s.results.filter(function (r) { return r === true; }).length;
var ans = s.results.filter(function (r) { return r !== null; }).length;
setTxt('ok' + sec, ok); setTxt('cur' + sec, ans); setTxt('max' + sec, pool.length);
var pf = document.getElementById('prog' + sec); if (pf) pf.style.width = Math.round(ans / pool.length * 100) + '%';
}
function showSummary(sec) {
var s = SEC[sec], pool = POOLS()[sec], sum = document.getElementById('sum' + sec); if (!sum) return;
var ok = s.results.filter(function (r) { return r === true; }).length;
setTxt('sumScore' + sec, ok + ' / ' + pool.length);
var grade = ok === pool.length ? 'Отлично! Все задачи решены.' : ok >= pool.length * 0.6 ? 'Хорошо! Можно повторить ошибки.' : 'Стоит повторить параграф.';
setTxt('sumGrade' + sec, grade);
sum.classList.add('show');
if (ok === pool.length) { bumpProgress(sec, 60); var aId = sec + '_tasks'; if (ACHL()[aId]) achievement(aId); }
}
function setTxt(id, v) { var e = document.getElementById(id); if (e) e.textContent = v; }
/* ── тема ──────────────────────────────────────────────────────── */
function initTheme() {
var t = localStorage.getItem(K.theme) || localStorage.getItem('theme') || 'light';
if (t === 'dark') document.documentElement.classList.add('dark');
var lab = document.getElementById('theme-lab'); if (lab) lab.textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
var btn = document.getElementById('theme-btn'); if (!btn) return;
btn.addEventListener('click', function () {
document.documentElement.classList.toggle('dark');
var d = document.documentElement.classList.contains('dark');
localStorage.setItem(K.theme, d ? 'dark' : 'light'); localStorage.setItem('theme', d ? 'dark' : 'light');
if (lab) lab.textContent = d ? 'Светлая' : 'Тёмная';
});
}
/* ── init ──────────────────────────────────────────────────────── */
function init() {
resolveCfg();
loadProgress(); initTheme(); buildParaSelector(); refreshUI();
if (ACHL().start) achievement('start');
var first = (PARAS()[0] || {}).id; if (first) goTo(first);
refreshUI(); loadServerReadState();
W.addEventListener('focus', loadServerReadState);
}
/* экспорт */
W.goTo = goTo; W.ensureBuilt = ensureBuilt;
W.checkNum = checkNum; W.selectMcq = selectMcq; W.nextTask = nextTask; W.goToTask = goToTask; W.resetTasks = resetTasks;
W.renderTask = renderTask;
W.makeCard = makeCard; W.secNav = secNav; W.readButton = readButton; W.wireReadBtn = wireReadBtn;
W.addXp = addXp; W.achievement = achievement; W.bumpProgress = bumpProgress; W.chem8RenderMath = renderMath;
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init();
})(window);