From 5b075cde86b0cf92a4b1b29fbce943b0fe1e9c52 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 09:55:44 +0300 Subject: [PATCH] =?UTF-8?q?feat(phys9=20finals):=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B3=D1=80=D0=B5=D1=81=D1=81-=D0=B1=D0=B0=D1=80=D1=8B=20?= =?UTF-8?q?=D0=B8=20=D0=B0=D1=87=D0=B8=D0=B2=D0=BA=D0=B8=20=D1=84=D0=B8?= =?UTF-8?q?=D0=BD=D0=B0=D0=BB=D0=BE=D0=B2=20Wave=20F=20+=20G?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новый модуль frontend/js/phys9_finals.js: 1. РАСШИРЯЕТ window.checkNum чтобы поддерживать сигнатуру (id, answer, unit, tol) — раньше legacy checkNum принимал только sec для POOLS, из-за чего кнопки «Проверить» в финалах не работали. 2. ПРОГРЕСС-БАР под заголовком каждого finalN: - Подсчитывает количество в финале - При правильном ответе обновляет % решённых - +8 XP за каждую решённую задачу 3. АЧИВКИ: - При 100% решённых задач финала — +50 XP + бэйдж «★ МАСТЕР ГЛАВЫ» (физика9_chN_master) - При всех 5 финалах — +150 XP + ачивка «МАГИСТР ФИЗИКИ 9» (Wave G — финал курса) Подключение во все 5 ch + хук на ensureBuilt вызывает PHYS9_FINALS_INIT(id) для id вида final1..final5. (linter добавил { delimiters, throwOnError:false } в renderMathInElement вызовы во всех 5 widget-модулях — сохранено). Co-Authored-By: Claude Opus 4.7 --- frontend/js/phys9_ch1_widgets.js | 6 +- frontend/js/phys9_ch2_widgets.js | 2 +- frontend/js/phys9_ch3_widgets.js | 2 +- frontend/js/phys9_ch4_widgets.js | 2 +- frontend/js/phys9_ch5_widgets.js | 2 +- frontend/js/phys9_finals.js | 125 ++++++++++++++++++++++++++ frontend/textbooks/physics_9_ch1.html | 3 +- frontend/textbooks/physics_9_ch2.html | 3 +- frontend/textbooks/physics_9_ch3.html | 3 +- frontend/textbooks/physics_9_ch4.html | 3 +- frontend/textbooks/physics_9_ch5.html | 3 +- 11 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 frontend/js/phys9_finals.js diff --git a/frontend/js/phys9_ch1_widgets.js b/frontend/js/phys9_ch1_widgets.js index 3c2dfa2..1855115 100644 --- a/frontend/js/phys9_ch1_widgets.js +++ b/frontend/js/phys9_ch1_widgets.js @@ -89,7 +89,7 @@ function appendTo(secId, html){ div.className = 'wg-phys9-extra-'+secId; div.innerHTML = html; box.appendChild(div); - try { if(window.renderMathInElement) window.renderMathInElement(box); } catch(e){} + try { if(window.renderMathInElement) window.renderMathInElement(box, { delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){} return true; } @@ -202,7 +202,7 @@ function add_p4(){ s += '$a_x$='+ax.toFixed(1)+''; s += '$a_y$='+(-ay).toFixed(1)+''; document.getElementById('p4w-svg').innerHTML = s; - try { if(window.renderMathInElement) window.renderMathInElement(document.getElementById('p4-extra').parentNode); } catch(e){} + try { if(window.renderMathInElement) window.renderMathInElement(document.getElementById('p4-extra').parentNode, { delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){} }; document.getElementById('p4w-a-r').addEventListener('input', upd); document.getElementById('p4w-an-r').addEventListener('input', upd); @@ -278,7 +278,7 @@ function add_p7(){ +'' +'
' +'$\\langle v\\rangle = (v_1 t_1 + v_2 t_2)/(t_1+t_2)$ = 13.3 м/с' - +'Ловушка: ($v_1+v_2)/2$ = 15.0 м/с — НЕВЕРНО' + +'Ловушка: $(v_1+v_2)/2$ = 15.0 м/с — НЕВЕРНО' +'
'; if(appendTo('p7', wgWrapper('p7-extra', 'CALC', 'Средняя скорость', 'Меняй $v$ и $t$ на двух участках. Сравни средневзвешенное и арифметическое.', body))){ const upd = ()=>{ diff --git a/frontend/js/phys9_ch2_widgets.js b/frontend/js/phys9_ch2_widgets.js index 20ba52b..d49deff 100644 --- a/frontend/js/phys9_ch2_widgets.js +++ b/frontend/js/phys9_ch2_widgets.js @@ -75,7 +75,7 @@ function appendTo(secId, html){ div.className = 'wg-phys9-extra-'+secId; div.innerHTML = html; box.appendChild(div); - try { if(window.renderMathInElement) window.renderMathInElement(box); } catch(e){} + try { if(window.renderMathInElement) window.renderMathInElement(box, { delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){} return true; } diff --git a/frontend/js/phys9_ch3_widgets.js b/frontend/js/phys9_ch3_widgets.js index 8c5bd31..7aa57f8 100644 --- a/frontend/js/phys9_ch3_widgets.js +++ b/frontend/js/phys9_ch3_widgets.js @@ -49,7 +49,7 @@ function appendTo(secId, html){ if(box.querySelector('.wg-phys9-extra-'+secId)) return false; const div = document.createElement('div'); div.className = 'wg-phys9-extra-'+secId; div.innerHTML = html; box.appendChild(div); - try { if(window.renderMathInElement) window.renderMathInElement(box); } catch(e){} + try { if(window.renderMathInElement) window.renderMathInElement(box, { delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){} return true; } diff --git a/frontend/js/phys9_ch4_widgets.js b/frontend/js/phys9_ch4_widgets.js index c791c4c..0217bc9 100644 --- a/frontend/js/phys9_ch4_widgets.js +++ b/frontend/js/phys9_ch4_widgets.js @@ -49,7 +49,7 @@ function appendTo(secId, html){ if(box.querySelector('.wg-phys9-extra-'+secId)) return false; const div = document.createElement('div'); div.className = 'wg-phys9-extra-'+secId; div.innerHTML = html; box.appendChild(div); - try { if(window.renderMathInElement) window.renderMathInElement(box); } catch(e){} + try { if(window.renderMathInElement) window.renderMathInElement(box, { delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){} return true; } diff --git a/frontend/js/phys9_ch5_widgets.js b/frontend/js/phys9_ch5_widgets.js index 88460be..073b271 100644 --- a/frontend/js/phys9_ch5_widgets.js +++ b/frontend/js/phys9_ch5_widgets.js @@ -11,7 +11,7 @@ function appendTo(secId, html){ if(box.querySelector('.wg-phys9-extra-'+secId)) return false; const div = document.createElement('div'); div.className = 'wg-phys9-extra-'+secId; div.innerHTML = html; box.appendChild(div); - try { if(window.renderMathInElement) window.renderMathInElement(box); } catch(e){} + try { if(window.renderMathInElement) window.renderMathInElement(box, { delimiters: [{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); } catch(e){} return true; } function wireSubmit(id){ diff --git a/frontend/js/phys9_finals.js b/frontend/js/phys9_finals.js new file mode 100644 index 0000000..fc13612 --- /dev/null +++ b/frontend/js/phys9_finals.js @@ -0,0 +1,125 @@ +// phys9_finals.js — улучшение финалов 1-5 Физики 9: +// 1. Расширяет window.checkNum чтобы поддерживать сигнатуру (id, answer, unit, tol) +// (раньше legacy checkNum принимал только sec из POOLS — финалы не работали). +// 2. Считает решённые задачи каждого финала, рисует прогресс-бар. +// 3. При 100% — выдаёт XP + ачивку «Мастер главы N». +(function(){ +'use strict'; + +const FINAL_TASKS = {}; /* finalN → { total, ok: Set } */ +const ACHIEVED = new Set(); + +/* === Расширение checkNum === */ +const _origCheckNum = typeof window.checkNum === 'function' ? window.checkNum : null; +function patchedCheckNum(arg1, arg2, arg3, arg4){ + /* Финальная задача: arg1 = id (например 'fin1-q1'), arg2 = answer, arg3 = unit, arg4 = tol */ + if (typeof arg2 === 'number' && /^fin\d+-q\d+/.test(arg1)) { + const id = arg1; + const answer = arg2; + const tol = arg4 || Math.max(0.005, Math.abs(answer) * 0.03); + const inp = document.getElementById(id); + const fb = document.getElementById('fb-' + id); + if (!inp || !fb) return; + const val = (inp.value || '').trim().replace(',', '.'); + const num = parseFloat(val); + if (val === '' || isNaN(num)) { + fb.className = 'feedback fail show'; + fb.style.display = 'block'; + fb.innerHTML = 'Введи число.'; + return; + } + const ok = Math.abs(num - answer) <= tol; + if (ok) { + fb.className = 'feedback ok show'; + fb.style.display = 'block'; + fb.innerHTML = '✓ Верно! ' + (arg3 ? '(' + arg3 + ')' : ''); + inp.disabled = true; + const finalKey = id.match(/^fin(\d+)/)[1]; + const finalId = 'final' + finalKey; + if (!FINAL_TASKS[finalId]) FINAL_TASKS[finalId] = { total: 0, ok: new Set() }; + FINAL_TASKS[finalId].ok.add(id); + _updateFinalProgress(finalId); + try { if (window.addXp) window.addXp(8, 'phys9-fin-' + id); } catch(e){} + } else { + fb.className = 'feedback fail show'; + fb.style.display = 'block'; + fb.innerHTML = '✗ Не то. Перепроверь решение.'; + } + return; + } + /* Legacy путь — для POOLS секций */ + if (_origCheckNum) return _origCheckNum.apply(this, arguments); +} +window.checkNum = patchedCheckNum; + +/* === Прогресс-бар + ачивка === */ +function _ensureProgressBar(finalId){ + const box = document.getElementById(finalId + '-body'); + if (!box) return null; + let bar = box.querySelector('.phys9-fin-bar'); + if (bar) return bar; + /* Подсчёт общего количества задач в финале */ + const tasks = box.querySelectorAll('input[id^="' + finalId.replace('final','fin') + '-q"]'); + const total = tasks.length; + FINAL_TASKS[finalId] = FINAL_TASKS[finalId] || { total: total, ok: new Set() }; + FINAL_TASKS[finalId].total = total; + /* Вставляем бар как первый дочерний элемент в body */ + const wrap = document.createElement('div'); + wrap.className = 'phys9-fin-bar'; + wrap.style.cssText = 'margin:14px 0 18px;padding:14px 16px;background:linear-gradient(135deg,var(--sec-acc-soft,#dbeafe),var(--card,#fff));border:1.5px solid var(--sec-acc,#2563eb);border-radius:12px'; + wrap.innerHTML = '
' + + '
Финал главы — задач решено: 0 / ' + total + '
' + + '' + + '
' + + '
'; + /* Вставка перед первой task-card или в начало */ + const firstTask = box.querySelector('.task-card'); + if (firstTask) box.insertBefore(wrap, firstTask); + else box.appendChild(wrap); + return wrap; +} + +function _updateFinalProgress(finalId){ + const bar = _ensureProgressBar(finalId); + if (!bar) return; + const data = FINAL_TASKS[finalId]; + if (!data) return; + const pct = data.total > 0 ? Math.round(data.ok.size / data.total * 100) : 0; + const cnt = document.getElementById(finalId + '-cnt'); + const fill = document.getElementById(finalId + '-fill'); + const badge = document.getElementById(finalId + '-badge'); + if (cnt) cnt.textContent = data.ok.size; + if (fill) fill.style.width = pct + '%'; + if (data.total > 0 && data.ok.size === data.total && !ACHIEVED.has(finalId)) { + ACHIEVED.add(finalId); + if (badge) badge.style.display = 'inline-block'; + try { if (window.addXp) window.addXp(50, 'phys9-master-' + finalId); } catch(e){} + try { + localStorage.setItem('physics9_' + finalId + '_master', '1'); + const allDone = ['final1','final2','final3','final4','final5'].every(f => + localStorage.getItem('physics9_' + f + '_master') === '1'); + if (allDone && !localStorage.getItem('physics9_grandmaster')) { + localStorage.setItem('physics9_grandmaster', '1'); + if (window.addXp) window.addXp(150, 'phys9-grandmaster'); + alert('Поздравляем! Все 5 финалов глав сданы.\nАчивка: МАГИСТР ФИЗИКИ 9 (+150 XP)'); + } + } catch(e){} + } +} + +/* === Инициализация при открытии финала === */ +window.PHYS9_FINALS_INIT = function(finalId){ + _ensureProgressBar(finalId); + /* Восстановить состояние из disabled полей (если перезагрузка/возврат) */ + const box = document.getElementById(finalId + '-body'); + if (!box) return; + box.querySelectorAll('input[id^="' + finalId.replace('final','fin') + '-q"]').forEach(inp=>{ + if (inp.disabled) { + if (!FINAL_TASKS[finalId]) FINAL_TASKS[finalId] = { total: 0, ok: new Set() }; + FINAL_TASKS[finalId].ok.add(inp.id); + } + }); + _updateFinalProgress(finalId); +}; + +})(); diff --git a/frontend/textbooks/physics_9_ch1.html b/frontend/textbooks/physics_9_ch1.html index d69bf77..0322e61 100644 --- a/frontend/textbooks/physics_9_ch1.html +++ b/frontend/textbooks/physics_9_ch1.html @@ -17,6 +17,7 @@ +