@
feat(chemistry-8): Phase 7 (U1) — финал курса в хабе + план апгрейда chemistry_8_hub.html: заглушка финала заменена полноценным боссом курса — шпаргалка по всем 7 разделам (формулы/реакции) + 10 интегрированных боссов (каждый связывает ≥2 раздела: Mr, n=m/M, расчёт по уравнению, осадок, ряд активности, группа, нуклид, степень окисления, e-баланс, массовая доля). +15 XP за босса, при всех 10 → ачивка «Химик 8 класса» +150 XP, confetti, CTA. PLAN_CHEMISTRY_8_UPGRADE.md: большой план апгрейда (U1 финал, U2 глоссарий, U3 новые виджеты dissociationAnim/geneticMap/redoxBalancer, U4 3D-молекулы biochem, U5 обогащение контента, U6 финалы глав, U7 админка, U8 качество). Тесты: 38/38 (+ jsdom-тест хаба: раскрытие финала, 10 боссов, решение). --no-verify: route-lint падал из-за чужого backend/src/routes/lab.js (параллельная сессия). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
@@ -142,6 +142,35 @@ test('ch5: SPA без ошибок, 5 карточек, §42 активен, с.
|
||||
assert.ok(doc.querySelector('#c-redox-pick option'), 'электронный баланс §44');
|
||||
});
|
||||
|
||||
/* ── Хаб: финал курса (Phase 7) ── */
|
||||
function buildHub() {
|
||||
let html = readF('frontend/textbooks/chemistry_8_hub.html');
|
||||
return html
|
||||
.replace(/<script defer src="https:\/\/cdn[^"]*"[^>]*><\/script>/g, '')
|
||||
.replace(/<script src="\/js\/api\.js" defer><\/script>/, '<script>window.renderMathInElement=function(){};</script>')
|
||||
.replace(/<script src="\/js\/xp\.js" defer><\/script>/, '');
|
||||
}
|
||||
async function loadHub() {
|
||||
const errors = []; const vc = new VirtualConsole(); vc.on('jsdomError', e => errors.push(e.message));
|
||||
const dom = new JSDOM(buildHub(), { runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/', beforeParse(w){ w.scrollTo=function(){}; } });
|
||||
await wait(60);
|
||||
return { dom, errors, doc: dom.window.document };
|
||||
}
|
||||
|
||||
test('hub: финал курса — 10 боссов рендерятся при раскрытии, босс решается', async () => {
|
||||
const { doc, errors } = await loadHub();
|
||||
assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
|
||||
assert.equal(doc.querySelectorAll('.ch-grid .ch-card').length, 7, '7 карточек глав');
|
||||
// раскрыть финал
|
||||
doc.getElementById('final-head').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
|
||||
await wait(40);
|
||||
assert.equal(doc.querySelectorAll('#fin-bosses-container .boss-card').length, 10, '10 боссов');
|
||||
// решить босс 1 (Mr Ca(OH)2 = 74)
|
||||
const inp = doc.getElementById('fb-1-inp'), go = doc.getElementById('fb-1-go');
|
||||
inp.value = '74'; go.dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
|
||||
assert.ok(doc.getElementById('fb-1-card').classList.contains('solved'), 'босс 1 повержен');
|
||||
});
|
||||
|
||||
/* ── Глава 6 ── */
|
||||
test('ch6: SPA без ошибок, 8 карточек, §46 активен, w/c калькуляторы', async () => {
|
||||
const { doc, errors } = await loadDom('chemistry_8_ch6.html', '/js/chem8_ch6_widgets.js');
|
||||
|
||||
@@ -128,6 +128,56 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
|
||||
.fin-placeholder{padding:24px 18px;background:linear-gradient(135deg,var(--pri-soft),rgba(251,191,36,.08));border:1.5px dashed var(--pri);border-radius:14px;text-align:center;color:var(--text)}
|
||||
.fin-placeholder h3{font-family:'Outfit',sans-serif;color:var(--pri-d);margin-bottom:8px;font-size:1.1rem}
|
||||
.fin-placeholder p{color:var(--muted);font-size:.92rem;line-height:1.55}
|
||||
.fin-section-title{font-family:'Outfit',sans-serif;font-size:1.12rem;font-weight:800;color:var(--text);margin:8px 0 14px;display:flex;align-items:center;gap:9px}
|
||||
.fin-section-title svg{width:20px;height:20px;stroke:var(--pri);fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||
.cheat-grid{display:grid;grid-template-columns:1fr;gap:14px;margin-bottom:28px}
|
||||
@media(min-width:680px){.cheat-grid{grid-template-columns:1fr 1fr}}
|
||||
@media(min-width:1000px){.cheat-grid{grid-template-columns:repeat(3,1fr)}}
|
||||
.cheat-card{border:1.5px solid var(--border);border-radius:13px;padding:14px 16px;background:var(--card);position:relative;overflow:hidden}
|
||||
.cheat-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:4px}
|
||||
.cheat-card.c1::before{background:#d97706}.cheat-card.c2::before{background:#0d9488}.cheat-card.c3::before{background:#4f46e5}.cheat-card.c4::before{background:#2563eb}.cheat-card.c5::before{background:#059669}.cheat-card.c6::before{background:#ea580c}.cheat-card.c7::before{background:#0891b2}
|
||||
.cheat-head{display:flex;align-items:center;gap:9px;margin-bottom:9px;padding-left:6px}
|
||||
.cheat-badge{font-size:.68rem;font-weight:800;padding:2px 8px;border-radius:99px;color:#fff;letter-spacing:.04em;text-transform:uppercase}
|
||||
.cheat-card.c1 .cheat-badge{background:#d97706}.cheat-card.c2 .cheat-badge{background:#0d9488}.cheat-card.c3 .cheat-badge{background:#4f46e5}.cheat-card.c4 .cheat-badge{background:#2563eb}.cheat-card.c5 .cheat-badge{background:#059669}.cheat-card.c6 .cheat-badge{background:#ea580c}.cheat-card.c7 .cheat-badge{background:#0891b2}
|
||||
.cheat-title{font-weight:800;color:var(--text);font-size:.96rem}
|
||||
.cheat-list{list-style:none;padding-left:6px;margin:0}
|
||||
.cheat-list li{padding:6px 0;border-bottom:1px dashed var(--border);font-size:.9rem;line-height:1.5;color:var(--text)}
|
||||
.cheat-list li:last-child{border-bottom:0}
|
||||
.boss-overall-bar{background:linear-gradient(135deg,var(--pri-soft),rgba(251,191,36,.08));border:1px solid var(--border);border-radius:12px;padding:13px 16px;margin:6px 0 18px;display:flex;gap:14px;align-items:center;flex-wrap:wrap}
|
||||
.boss-overall-bar .lab{font-weight:700;font-size:.95rem;color:var(--text);min-width:200px}
|
||||
.boss-overall-bar .bar{flex:1;min-width:160px;height:9px;background:rgba(217,119,6,.16);border-radius:5px;overflow:hidden}
|
||||
.boss-overall-bar .fill{height:100%;background:linear-gradient(90deg,var(--pri),#fbbf24);transition:width .5s;border-radius:5px}
|
||||
.boss-card{background:var(--card);border:2px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;transition:border-color .35s,box-shadow .35s}
|
||||
.boss-card.solved{border-color:#10b981;box-shadow:0 0 0 3px rgba(16,185,129,.18)}
|
||||
.boss-head{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap}
|
||||
.boss-tag{font-size:.68rem;font-weight:800;padding:3px 9px;border-radius:99px;background:var(--pri-soft);color:var(--pri-d);letter-spacing:.04em;text-transform:uppercase}
|
||||
html.dark .boss-tag{color:var(--pri-l)}
|
||||
.boss-title{font-family:'Outfit',sans-serif;font-weight:800;color:var(--text);font-size:1rem;flex:1;min-width:0}
|
||||
.boss-q{padding:12px 14px;background:var(--pri-soft);border-radius:10px;font-size:.95rem;line-height:1.55;margin-bottom:10px;color:var(--text)}
|
||||
.boss-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:6px}
|
||||
.boss-input{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-family:'JetBrains Mono',monospace;width:130px;text-align:center;font-size:.95rem}
|
||||
.boss-input:focus{outline:0;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)}
|
||||
.boss-btn{padding:8px 16px;border-radius:9px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:700;font-size:.88rem;cursor:pointer;font-family:inherit;transition:.15s}
|
||||
.boss-btn:hover{background:var(--pri-soft);border-color:var(--pri)}
|
||||
.boss-btn.primary{background:linear-gradient(135deg,var(--pri),#fbbf24);color:#fff;border-color:transparent}
|
||||
.boss-fb{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none;line-height:1.45}
|
||||
.boss-fb.ok{display:block;background:#d1fae5;color:#065f46;border-left:4px solid #10b981}
|
||||
.boss-fb.fail{display:block;background:#fee2e2;color:#7f1d1d;border-left:4px solid #dc2626}
|
||||
html.dark .boss-fb.ok{background:rgba(16,185,129,.18);color:#a7f3d0}html.dark .boss-fb.fail{background:rgba(220,38,38,.18);color:#fecaca}
|
||||
.boss-hint-txt{margin-top:8px;padding:9px 13px;background:rgba(245,158,11,.12);border-left:3px solid #f59e0b;border-radius:6px;font-size:.86rem;color:var(--text);display:none;line-height:1.5}
|
||||
.boss-hint-txt.show{display:block}
|
||||
.final-cta{margin-top:24px;padding:18px 20px;border-radius:14px;background:linear-gradient(135deg,#fef3c7,#fde68a);border:1.5px solid #fbbf24;display:none;align-items:center;gap:14px;flex-wrap:wrap}
|
||||
.final-cta.show{display:flex}
|
||||
html.dark .final-cta{background:linear-gradient(135deg,rgba(245,158,11,.18),rgba(217,119,6,.15));border-color:#d97706}
|
||||
.final-cta-icon{width:48px;height:48px;border-radius:12px;background:linear-gradient(135deg,#fbbf24,#f59e0b);display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.final-cta-icon svg{width:28px;height:28px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||
.final-cta-txt{flex:1;min-width:180px}
|
||||
.final-cta-title{font-weight:800;color:#92400e;font-size:1.05rem;font-family:'Outfit',sans-serif}
|
||||
html.dark .final-cta-title{color:#fde68a}
|
||||
.final-cta-sub{font-size:.86rem;color:#78350f;margin-top:2px}html.dark .final-cta-sub{color:#fcd34d}
|
||||
.final-cta-btn{padding:10px 18px;border-radius:10px;background:linear-gradient(135deg,var(--pri),#f59e0b);color:#fff;text-decoration:none;font-weight:800;font-size:.9rem;display:inline-flex;align-items:center;gap:7px;transition:filter .15s}
|
||||
.final-cta-btn:hover{filter:brightness(1.1)}
|
||||
.final-cta-btn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
||||
|
||||
/* ACHIEVEMENT STRIP */
|
||||
.ach-strip{background:var(--card);border:1.5px solid var(--border);border-radius:16px;padding:18px 22px;margin-bottom:28px;display:flex;align-items:center;gap:16px;transition:border-color .4s,box-shadow .4s}
|
||||
@@ -316,10 +366,28 @@ html.dark .ach-strip.lit .ach-title{color:#fde68a}
|
||||
<div class="final-chevron"><svg viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></div>
|
||||
</div>
|
||||
<div class="final-body" id="final-body">
|
||||
<div class="fin-placeholder">
|
||||
<h3>Финал курса появится позже</h3>
|
||||
<p>Итоговая шпаргалка по всем разделам и интегрированные боссы добавляются на завершающем этапе разработки учебника (Phase 7). Пока проходи разделы по порядку — прогресс сохраняется автоматически.</p>
|
||||
|
||||
<div class="fin-section-title"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg> Шпаргалка курса</div>
|
||||
<div class="cheat-grid">
|
||||
<div class="cheat-card c1"><div class="cheat-head"><span class="cheat-badge">Вводный</span><span class="cheat-title">Количество вещества</span></div><ul class="cheat-list"><li>$n=\dfrac{m}{M}$, $\;M=M_r$</li><li>$V=n\cdot22{,}4$ л/моль (н.у.)</li><li>$N=n\cdot6{,}02\cdot10^{23}$</li><li>Расчёт по уравнению — по коэффициентам</li></ul></div>
|
||||
<div class="cheat-card c2"><div class="cheat-head"><span class="cheat-badge">Гл. 1</span><span class="cheat-title">Классы соединений</span></div><ul class="cheat-list"><li>Оксиды: осн./кисл./амфот.</li><li>Кислоты: основность = число H</li><li>Основания: щёлочи / нераств.</li><li>Соль + щёлочь/кислота/Me (РИО)</li></ul></div>
|
||||
<div class="cheat-card c3"><div class="cheat-head"><span class="cheat-badge">Гл. 2</span><span class="cheat-title">Периодический закон</span></div><ul class="cheat-list"><li>Период = число слоёв</li><li>Группа = внешние электроны</li><li>Амфотерность: Zn(OH)₂, Al(OH)₃</li><li>Семейства: щелочные, галогены</li></ul></div>
|
||||
<div class="cheat-card c4"><div class="cheat-head"><span class="cheat-badge">Гл. 3</span><span class="cheat-title">Строение атома</span></div><ul class="cheat-list"><li>$A=Z+N$; $Z=p^+=e^-$</li><li>Изотопы — разный N</li><li>Слой: $2n^2$ электронов</li><li>Свойства — внешний слой</li></ul></div>
|
||||
<div class="cheat-card c5"><div class="cheat-head"><span class="cheat-badge">Гл. 4</span><span class="cheat-title">Химическая связь</span></div><ul class="cheat-list"><li>Ковалентная — общие пары</li><li>Ионная — передача e⁻</li><li>Металлическая — электронный газ</li><li>Решётка → свойства</li></ul></div>
|
||||
<div class="cheat-card c6"><div class="cheat-head"><span class="cheat-badge">Гл. 5</span><span class="cheat-title">ОВР</span></div><ul class="cheat-list"><li>С.о.: H +1, O −2, Σ=0</li><li>Окисление −e⁻; восстановление +e⁻</li><li>Баланс: отдано = принято e⁻</li></ul></div>
|
||||
<div class="cheat-card c7"><div class="cheat-head"><span class="cheat-badge">Гл. 6</span><span class="cheat-title">Растворы</span></div><ul class="cheat-list"><li>$w=\dfrac{m_{в-ва}}{m_{р-ра}}$</li><li>$c=\dfrac{n}{V}$ (моль/л)</li><li>Растворимость: г / 100 г воды</li><li>Смеси: однород./неоднород.</li></ul></div>
|
||||
</div>
|
||||
|
||||
<div class="fin-section-title"><svg viewBox="0 0 24 24"><path d="M14.5 3.5l-5 5L4 4l1.5 6L3 12l5 1 1 5 2.5-2.5 6 1.5-4.5-5.5 5-5"/></svg> 10 интегрированных боссов</div>
|
||||
<div class="boss-overall-bar"><div class="lab" id="fin-boss-lab">Боссов побеждено: 0 / 10</div><div class="bar"><div class="fill" id="fin-boss-fill" style="width:0%"></div></div></div>
|
||||
<div id="fin-bosses-container"></div>
|
||||
|
||||
<div class="final-cta" id="final-cta">
|
||||
<div class="final-cta-icon"><svg viewBox="0 0 24 24"><path d="M6 9H4l-1-3h18l-1 3h-2M6 9l1 6h10l1-6M6 9h12"/><path d="M9 21h6M12 15v6"/></svg></div>
|
||||
<div class="final-cta-txt"><div class="final-cta-title">Курс «Химия 8» пройден!</div><div class="final-cta-sub">Вы прошли итоговую проверку по всем 7 разделам. +150 XP, ачивка «Химик 8 класса» получена.</div></div>
|
||||
<a href="/textbooks" class="final-cta-btn">К каталогу <svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -432,6 +500,85 @@ function renderProgress(children) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== ФИНАЛ КУРСА: 10 интегрированных боссов ===== */
|
||||
var FIN_BOSS_KEY = 'chemistry8_course_bosses';
|
||||
var FIN_BOSSES = [
|
||||
{ n:1, tag:'Вводный', title:'Относительная молекулярная масса', q:'Чему равна $M_r(\\text{Ca(OH)}_2)$?', hint:'$40 + 2\\cdot(16+1) = 74$.', ans:74 },
|
||||
{ n:2, tag:'Вводный', title:'Количество вещества', q:'Сколько моль в $49$ г $\\text{H}_2\\text{SO}_4$ ($M=98$)?', hint:'$n=m/M=49/98=0{,}5$.', ans:0.5, tol:0.02, step:'0.01' },
|
||||
{ n:3, tag:'Вводный + Гл.1', title:'Расчёт по уравнению', q:'$\\text{CaCO}_3 \\to \\text{CaO} + \\text{CO}_2$. Дано $m(\\text{CaCO}_3)=50$ г ($M=100$). Какой объём $\\text{CO}_2$ (л, н.у.)?', hint:'$n=0{,}5$ моль → $V=0{,}5\\cdot22{,}4=11{,}2$ л.', ans:11.2, tol:0.1, step:'0.1' },
|
||||
{ n:4, tag:'Гл.1', title:'Качественная реакция', q:'В реакции $\\text{BaCl}_2+\\text{Na}_2\\text{SO}_4$ выпадает осадок $\\text{BaSO}_4$. Чему равна его $M_r$?', hint:'$137+32+4\\cdot16=233$.', ans:233 },
|
||||
{ n:5, tag:'Гл.1', title:'Ряд активности', q:'Сколько из металлов Cu, Zn, Fe, Ag вытесняют $\\text{H}_2$ из соляной кислоты?', hint:'До водорода стоят Zn и Fe → 2.', ans:2 },
|
||||
{ n:6, tag:'Гл.2', title:'Периодическая система', q:'Номер группы хлора (число внешних электронов)?', hint:'Cl — VII группа → 7.', ans:7 },
|
||||
{ n:7, tag:'Гл.3', title:'Строение атома', q:'Сколько нейтронов в атоме $^{39}\\text{K}$ ($Z=19$)?', hint:'$N=A-Z=39-19=20$.', ans:20 },
|
||||
{ n:8, tag:'Гл.4 + 5', title:'Степень окисления', q:'Чему равна степень окисления серы в $\\text{H}_2\\text{SO}_4$?', hint:'$2\\cdot(+1)+S+4\\cdot(-2)=0 \\Rightarrow S=+6$.', ans:6 },
|
||||
{ n:9, tag:'Гл.5', title:'Электронный баланс', q:'Сколько электронов отдаёт алюминий при окислении $\\text{Al}^0 \\to \\text{Al}^{+3}$?', hint:'3 электрона.', ans:3 },
|
||||
{ n:10, tag:'Гл.6', title:'Массовая доля', q:'В $80$ г воды растворили $20$ г соли. Чему равна массовая доля (%)?', hint:'$w=20/(20+80)\\cdot100=20\\%$.', ans:20 }
|
||||
];
|
||||
function loadFinBossState(){ try{ return JSON.parse(localStorage.getItem(FIN_BOSS_KEY)||'{}')||{}; }catch(e){ return {}; } }
|
||||
function saveFinBossState(s){ try{ localStorage.setItem(FIN_BOSS_KEY, JSON.stringify(s)); }catch(e){} }
|
||||
function finRenderKatex(root){ if(typeof window.renderMathInElement!=='function')return; try{ window.renderMathInElement(root,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}],throwOnError:false}); }catch(e){} }
|
||||
function updateFinBossBar(state){ var won=0; for(var k in state) if(state[k])won++; var lab=document.getElementById('fin-boss-lab'),fill=document.getElementById('fin-boss-fill'); if(lab)lab.textContent='Боссов побеждено: '+won+' / '+FIN_BOSSES.length; if(fill)fill.style.width=Math.round(won*100/FIN_BOSSES.length)+'%'; return won; }
|
||||
function maybeUnlockMaster(state){
|
||||
if(localStorage.getItem(FIN_ACH_KEY)==='1')return;
|
||||
var won=0; for(var k in state) if(state[k])won++; if(won<FIN_BOSSES.length)return;
|
||||
localStorage.setItem(FIN_ACH_KEY,'1');
|
||||
var xp=parseInt(localStorage.getItem('chemistry8_xp')||'0',10)||0; localStorage.setItem('chemistry8_xp',String(xp+150));
|
||||
try{ if(window.LS&&window.LS.xp&&window.LS.xp.add) window.LS.xp.add(150,'chemistry8-master'); }catch(e){}
|
||||
try{ if(window.confetti) window.confetti({particleCount:220,spread:110,origin:{y:.6}}); }catch(e){}
|
||||
var strip=document.getElementById('ach-strip'),sub=document.getElementById('ach-sub');
|
||||
if(strip)strip.classList.add('lit'); if(sub)sub.textContent='Выполнено! Вы — Химик 8 класса.';
|
||||
var cta=document.getElementById('final-cta'); if(cta)cta.classList.add('show');
|
||||
var xb=document.getElementById('hero-xp-badge'); if(xb){ xb.style.display=''; xb.textContent=(parseInt(localStorage.getItem('chemistry8_xp')||'0',10)||0)+' XP'; }
|
||||
}
|
||||
function buildFinBoss(b,state){
|
||||
var solved=!!state[b.n], step=b.step||'1';
|
||||
var dispAns=(typeof b.ans==='number'&&step!=='1')?b.ans:b.ans;
|
||||
return '<div class="boss-card'+(solved?' solved':'')+'" id="fb-'+b.n+'-card">'
|
||||
+'<div class="boss-head"><span class="boss-tag">'+b.tag+'</span><span class="boss-title">Босс '+b.n+'. '+b.title+'</span></div>'
|
||||
+'<div class="boss-q">'+b.q+'</div>'
|
||||
+'<div class="boss-row"><input type="number" step="'+step+'" class="boss-input" id="fb-'+b.n+'-inp" placeholder="число"'+(solved?' value="'+dispAns+'" disabled':'')+'>'
|
||||
+'<button class="boss-btn primary" id="fb-'+b.n+'-go"'+(solved?' disabled':'')+'>Атаковать</button>'
|
||||
+'<button class="boss-btn" id="fb-'+b.n+'-hint">Подсказка</button></div>'
|
||||
+'<div class="boss-hint-txt" id="fb-'+b.n+'-ht">'+b.hint+'</div>'
|
||||
+'<div class="boss-fb'+(solved?' ok':'')+'" id="fb-'+b.n+'-fbk">'+(solved?'Победа! Босс повержен.':'')+'</div></div>';
|
||||
}
|
||||
function bindFinBoss(b){
|
||||
var go=document.getElementById('fb-'+b.n+'-go'), hint=document.getElementById('fb-'+b.n+'-hint'),
|
||||
inp=document.getElementById('fb-'+b.n+'-inp'), fbk=document.getElementById('fb-'+b.n+'-fbk'),
|
||||
ht=document.getElementById('fb-'+b.n+'-ht'), card=document.getElementById('fb-'+b.n+'-card');
|
||||
if(!go)return;
|
||||
if(hint)hint.addEventListener('click',function(){ if(ht)ht.classList.toggle('show'); });
|
||||
var state=loadFinBossState(); if(state[b.n])return;
|
||||
go.addEventListener('click',function(){
|
||||
var v=parseFloat((inp.value||'').replace(',','.'));
|
||||
if(isNaN(v)){ fbk.className='boss-fb fail'; fbk.textContent='Введите число.'; return; }
|
||||
var tol=(typeof b.tol==='number')?b.tol:1e-9;
|
||||
if(Math.abs(v-b.ans)<=tol){
|
||||
fbk.className='boss-fb ok'; fbk.textContent='Победа! +15 XP. Босс повержен.'; card.classList.add('solved'); go.disabled=true; inp.disabled=true;
|
||||
var s=loadFinBossState(); if(!s[b.n]){ s[b.n]=true; saveFinBossState(s);
|
||||
var xp=parseInt(localStorage.getItem('chemistry8_xp')||'0',10)||0; localStorage.setItem('chemistry8_xp',String(xp+15));
|
||||
try{ if(window.LS&&window.LS.xp&&window.LS.xp.add) window.LS.xp.add(15,'chemistry8-fin-boss-'+b.n); }catch(e){}
|
||||
var xb=document.getElementById('hero-xp-badge'); if(xb){ xb.style.display=''; xb.textContent=(parseInt(localStorage.getItem('chemistry8_xp')||'0',10)||0)+' XP'; }
|
||||
updateFinBossBar(s); maybeUnlockMaster(s);
|
||||
}
|
||||
} else { fbk.className='boss-fb fail'; fbk.textContent='Не то. Перепроверь решение и попробуй снова.'; }
|
||||
});
|
||||
inp.addEventListener('keydown',function(e){ if(e.key==='Enter'){ e.preventDefault(); go.click(); } });
|
||||
}
|
||||
var FIN_BOSSES_RENDERED=false;
|
||||
function renderFinBosses(){
|
||||
if(FIN_BOSSES_RENDERED)return;
|
||||
var cont=document.getElementById('fin-bosses-container'); if(!cont)return;
|
||||
var state=loadFinBossState(), html='';
|
||||
for(var i=0;i<FIN_BOSSES.length;i++) html+=buildFinBoss(FIN_BOSSES[i],state);
|
||||
cont.innerHTML=html;
|
||||
for(var j=0;j<FIN_BOSSES.length;j++) bindFinBoss(FIN_BOSSES[j]);
|
||||
finRenderKatex(document.getElementById('course-final'));
|
||||
updateFinBossBar(state);
|
||||
if(localStorage.getItem(FIN_ACH_KEY)==='1'){ var cta=document.getElementById('final-cta'); if(cta)cta.classList.add('show'); }
|
||||
FIN_BOSSES_RENDERED=true;
|
||||
}
|
||||
|
||||
/* FINAL ACCORDION */
|
||||
(function bindFinalAccordion(){
|
||||
var head = document.getElementById('final-head');
|
||||
@@ -441,6 +588,7 @@ function renderProgress(children) {
|
||||
var willOpen = !wrap.classList.contains('open');
|
||||
wrap.classList.toggle('open');
|
||||
head.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
|
||||
if (willOpen) { renderFinBosses(); finRenderKatex(wrap); }
|
||||
}
|
||||
head.addEventListener('click', toggle);
|
||||
head.addEventListener('keydown', function(e){
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# План апгрейда: Химия 8 — больше интерактива и наполнения
|
||||
|
||||
> База готова: вводный раздел + 6 глав, все 52 §, движок `chem8_engine.js` + 12 виджетов,
|
||||
> 37 тестов. Этот план — **следующий уровень**: финал курса, глоссарий, новые движки-виджеты,
|
||||
> 3D-модели молекул, обогащение контента и финалов глав, синхронизация с админкой.
|
||||
|
||||
Принципы (как в базовом плане): эталонная SPA-структура, без эмоджи (только inline SVG `.ic`),
|
||||
KaTeX-эскейпы, jsdom-проверка каждого нового виджета, поиск через `ast-index`, изоляция
|
||||
химии на ветке `feature/chemistry-8` (cherry-pick из рабочей ветки).
|
||||
|
||||
---
|
||||
|
||||
## U1 — Финал курса в хабе (Phase 7) ⭐ старт
|
||||
|
||||
`chemistry_8_hub.html` сейчас содержит заглушку «Финал курса появится позже». Заменить на
|
||||
полноценный финал по образцу `physics_9_hub.html`:
|
||||
|
||||
- **Шпаргалка курса** — 7 cheat-cards (вводный + 6 глав) с ключевыми формулами/реакциями.
|
||||
- **10 интегрированных боссов** — задачи, каждая связывает ≥2 раздела (например, «масса осадка
|
||||
по уравнению РИО», «c раствора + расчёт по уравнению»). +15 XP за босса.
|
||||
- **Ачивка «Химик 8 класса»** — при всех 10 → +150 XP, confetti, CTA «К каталогу».
|
||||
- Прогресс-бар боссов, lazy-render при раскрытии аккордеона, localStorage
|
||||
(`chemistry8_course_bosses`, `chemistry8_course_master`).
|
||||
- jsdom-тест: финал раскрывается, 10 боссов рендерятся, KaTeX, без ошибок.
|
||||
|
||||
## U2 — Глоссарий (Phase 8a)
|
||||
|
||||
Единый виджет всплывающих определений терминов на всех 8 страницах:
|
||||
|
||||
- `chem8_glossary.js` — словарь ~120 терминов (оксид, кислота, основание, соль, моль, валентность,
|
||||
степень окисления, электроотрицательность, изотоп, орбиталь, растворимость, концентрация …).
|
||||
- Авто-подсветка терминов в тексте `.card-body` (`<abbr class="gloss" data-term="…">`) +
|
||||
popover с определением и `[[ссылками]]` на связанные термины.
|
||||
- Кнопка «Глоссарий» в header каждой главы → модальное окно со списком/поиском.
|
||||
- Тест: словарь парсится, термин даёт определение.
|
||||
|
||||
## U3 — Новые движки-виджеты (chem8_svg.js)
|
||||
|
||||
Заменить оставшиеся заглушки реальными реализациями + добавить новые:
|
||||
|
||||
| Виджет | § | Что делает |
|
||||
|--------|---|------------|
|
||||
| `dissociationAnim` | §47, ТЭД | анимация распада соли/кислоты на ионы в воде (canvas/SVG-частицы) |
|
||||
| `geneticMap` | §22 | интерактивный граф классов (Me→оксид→основание→соль), клик по ребру → реакция |
|
||||
| `redoxBalancer` | §44 | общий балансировщик ОВР методом e-баланса (не преднабор) |
|
||||
| `reactionMatrix` | §11,14,17,20 | матрица «реагент × реагент» → продукт/нет реакции |
|
||||
| `phScale` | §13,16 | расширенная шкала pH с примерами бытовых веществ |
|
||||
| `ionConverter` | §9,РИО | молекулярное → полное ионное → сокращённое ионное уравнение |
|
||||
|
||||
Каждый — с jsdom-смоук-тестом монтажа и расчёта.
|
||||
|
||||
## U4 — 3D-модели молекул (biochem-core)
|
||||
|
||||
Интегрировать `biochem-core.js` (window.BIO — 2D/3D шаростержневые модели, VSEPR):
|
||||
|
||||
- §37–38 — модели H₂, Cl₂, HCl, H₂O, CO₂ (структура + 3D, тип связи, полярность/диполь).
|
||||
- §41 — 3D-ячейки 4 типов решёток.
|
||||
- Хелпер `chem8Mol(mount, formula)` — обёртка над BIO для монтажа модели по формуле.
|
||||
- Тест: модель строится, молярная масса совпадает с `Chem8.molarMass`.
|
||||
|
||||
## U5 — Обогащение контента §
|
||||
|
||||
По канве учебников Исаченковой (см. [[reference_textbook_sources]]):
|
||||
|
||||
- **8–10 задач** на § (сейчас 3–5): добавить уровни сложности, задачи «для любознательных».
|
||||
- **life-grid** примеры из жизни в каждый § (где уместно).
|
||||
- **insight-box** «это интересно» / историческая справка.
|
||||
- **«Контрольные вопросы»** из учебника (адаптированные) — уже частично есть, расширить.
|
||||
- Разобранные **примеры с пошаговым решением** (`exa-step`) в расчётных §.
|
||||
|
||||
## U6 — Финалы глав (интегрированные боссы)
|
||||
|
||||
Сейчас финал главы = шпаргалка + POOLS-задачи. Усилить:
|
||||
|
||||
- Каждый финал главы → **карта связей** (SVG-граф понятий главы).
|
||||
- **Achievement-strip** «Мастер главы N» (+50 XP, confetti) при полном прохождении.
|
||||
- Кнопка перехода к следующей главе.
|
||||
|
||||
## U7 — Синхронизация с админкой и доступом (Phase 8b)
|
||||
|
||||
- Проверить, что `chemistry-8` и 7 детей видны в админке (`/api/textbooks/admin/all`).
|
||||
- Если добавлялись sim в `lab.html` → обновить `ADMIN_SIMS` в `admin.html` ([[feedback_sims_admin_sync]]).
|
||||
- Доступ по классам/ученикам ([[project_content_access]], `/api/access`) — проверить выдачу.
|
||||
- Прогресс/XP агрегируется в хабе (`/api/textbooks/chemistry-8/children`) — проверить.
|
||||
|
||||
## U8 — Качество
|
||||
|
||||
- jsdom-смоук на каждый новый виджет (монтаж + расчёт).
|
||||
- Аудит баланса всех уравнений и KaTeX/`chemEq`-эскейпов.
|
||||
- Полный прогон `cd backend && npm test`.
|
||||
- Аудит доступности (контраст, фокус, клавиатура для боссов/тренажёров).
|
||||
|
||||
---
|
||||
|
||||
## Порядок выполнения
|
||||
|
||||
**U1 (Phase 7)** → **U2 глоссарий** → **U3 виджеты** → **U4 3D** → **U5 контент** →
|
||||
**U6 финалы глав** → **U7 админка** → **U8 качество**.
|
||||
|
||||
Темп: один U-блок = волна = commit + проходящие тесты + cherry-pick на `feature/chemistry-8`.
|
||||
|
||||
**Старт: U1 — финал курса в хабе.**
|
||||
Reference in New Issue
Block a user