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 + '
'
+ + '
★ МАСТЕР ГЛАВЫ +50 XP
'
+ + '
'
+ + '';
+ /* Вставка перед первой 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 @@
+