feat(phys9 finals): прогресс-бары и ачивки финалов Wave F + G

Новый модуль frontend/js/phys9_finals.js:

1. РАСШИРЯЕТ window.checkNum чтобы поддерживать сигнатуру
   (id, answer, unit, tol) — раньше legacy checkNum принимал только
   sec для POOLS, из-за чего кнопки «Проверить» в финалах не работали.

2. ПРОГРЕСС-БАР под заголовком каждого finalN:
   - Подсчитывает количество <input id="fin1-q1"...> в финале
   - При правильном ответе обновляет % решённых
   - +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 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 09:55:44 +03:00
parent 77e4dffb43
commit 5b075cde86
11 changed files with 142 additions and 12 deletions
+3 -3
View File
@@ -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 += '<text x="'+(cx+tipX)/2+'" y="'+(cy+18)+'" text-anchor="middle" font-size="12" font-weight="700" fill="'+(col.displacement||'#2563eb')+'">$a_x$='+ax.toFixed(1)+'</text>';
s += '<text x="'+(tipX+14)+'" y="'+(cy+tipY)/2+'" font-size="12" font-weight="700" fill="'+(col.force||'#10b981')+'">$a_y$='+(-ay).toFixed(1)+'</text>';
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(){
+'</div>'
+'<div class="score-display" style="flex-direction:column;align-items:flex-start;gap:5px">'
+'<span>$\\langle v\\rangle = (v_1 t_1 + v_2 t_2)/(t_1+t_2)$ = <b id="p7w-vavg">13.3</b> м/с</span>'
+'<span style="font-size:.86rem;color:var(--muted)">Ловушка: ($v_1+v_2)/2$ = <b id="p7w-trap">15.0</b> м/с — <span id="p7w-trap-lbl" style="font-weight:700;color:var(--fail)">НЕВЕРНО</span></span>'
+'<span style="font-size:.86rem;color:var(--muted)">Ловушка: $(v_1+v_2)/2$ = <b id="p7w-trap">15.0</b> м/с — <span id="p7w-trap-lbl" style="font-weight:700;color:var(--fail)">НЕВЕРНО</span></span>'
+'</div>';
if(appendTo('p7', wgWrapper('p7-extra', 'CALC', 'Средняя скорость', 'Меняй $v$ и $t$ на двух участках. Сравни средневзвешенное и арифметическое.', body))){
const upd = ()=>{
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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){
+125
View File
@@ -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<id> } */
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 = '&#10003; Верно! ' + (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 = '&#10007; Не то. Перепроверь решение.';
}
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 = '<div style="display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:8px">'
+ '<div style="font-weight:800;color:var(--sec-acc-d,#1d4ed8);font-size:1rem">Финал главы — задач решено: <span id="' + finalId + '-cnt">0</span> / ' + total + '</div>'
+ '<div id="' + finalId + '-badge" style="margin-left:auto;display:none;padding:5px 13px;background:linear-gradient(135deg,#fbbf24,#f59e0b);color:#fff;border-radius:99px;font-weight:800;font-size:.82rem;font-family:Unbounded,sans-serif">&#9733; МАСТЕР ГЛАВЫ +50 XP</div>'
+ '</div>'
+ '<div style="height:10px;background:rgba(0,0,0,.08);border-radius:6px;overflow:hidden"><div id="' + finalId + '-fill" style="height:100%;width:0%;background:linear-gradient(90deg,var(--sec-acc,#2563eb),var(--sec-acc-d,#1d4ed8));transition:width .5s cubic-bezier(.16,1,.3,1)"></div></div>';
/* Вставка перед первой 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);
};
})();
+2 -1
View File
@@ -17,6 +17,7 @@
<script src="/js/phys.js" defer></script>
<script src="/js/phys9_palette.js" defer></script>
<script src="/js/phys9_legacy.js" defer></script>
<script src="/js/phys9_finals.js" defer></script>
<script src="/js/phys9_ch1_widgets.js" defer></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
@@ -804,7 +805,7 @@ function _injectTasks(id){
var body = document.getElementById(id + '-body');
if(!body || body.querySelector('.legacy-tasks')) return;
body.insertAdjacentHTML('beforeend', _makeTaskBlock(id));
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_CH1_WIDGETS && window.PHYS9_CH1_WIDGETS[id]) window.PHYS9_CH1_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } }, 60);
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH1_WIDGETS && window.PHYS9_CH1_WIDGETS[id]) window.PHYS9_CH1_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } }, 60);
}
var _origEnsureBuilt = ensureBuilt;
ensureBuilt = function(id){ _origEnsureBuilt(id); _injectTasks(id); };
+2 -1
View File
@@ -17,6 +17,7 @@
<script src="/js/phys.js" defer></script>
<script src="/js/phys9_palette.js" defer></script>
<script src="/js/phys9_legacy.js" defer></script>
<script src="/js/phys9_finals.js" defer></script>
<script src="/js/phys9_ch2_widgets.js" defer></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
@@ -788,7 +789,7 @@ function _injectTasks(id){
var body = document.getElementById(id + '-body');
if(!body || body.querySelector('.legacy-tasks')) return;
body.insertAdjacentHTML('beforeend', _makeTaskBlock(id));
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_CH2_WIDGETS && window.PHYS9_CH2_WIDGETS[id]) window.PHYS9_CH2_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } }, 60);
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH2_WIDGETS && window.PHYS9_CH2_WIDGETS[id]) window.PHYS9_CH2_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } }, 60);
}
var _origEnsureBuilt = ensureBuilt;
ensureBuilt = function(id){ _origEnsureBuilt(id); _injectTasks(id); };
+2 -1
View File
@@ -17,6 +17,7 @@
<script src="/js/phys.js" defer></script>
<script src="/js/phys9_palette.js" defer></script>
<script src="/js/phys9_legacy.js" defer></script>
<script src="/js/phys9_finals.js" defer></script>
<script src="/js/phys9_ch3_widgets.js" defer></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
@@ -772,7 +773,7 @@ function _injectTasks(id){
var body = document.getElementById(id + '-body');
if(!body || body.querySelector('.legacy-tasks')) return;
body.insertAdjacentHTML('beforeend', _makeTaskBlock(id));
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_CH3_WIDGETS && window.PHYS9_CH3_WIDGETS[id]) window.PHYS9_CH3_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } }, 60);
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH3_WIDGETS && window.PHYS9_CH3_WIDGETS[id]) window.PHYS9_CH3_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } }, 60);
}
var _origEnsureBuilt = ensureBuilt;
ensureBuilt = function(id){ _origEnsureBuilt(id); _injectTasks(id); };
+2 -1
View File
@@ -17,6 +17,7 @@
<script src="/js/phys.js" defer></script>
<script src="/js/phys9_palette.js" defer></script>
<script src="/js/phys9_legacy.js" defer></script>
<script src="/js/phys9_finals.js" defer></script>
<script src="/js/phys9_ch4_widgets.js" defer></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
@@ -772,7 +773,7 @@ function _injectTasks(id){
var body = document.getElementById(id + '-body');
if(!body || body.querySelector('.legacy-tasks')) return;
body.insertAdjacentHTML('beforeend', _makeTaskBlock(id));
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_CH4_WIDGETS && window.PHYS9_CH4_WIDGETS[id]) window.PHYS9_CH4_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } }, 60);
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH4_WIDGETS && window.PHYS9_CH4_WIDGETS[id]) window.PHYS9_CH4_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } }, 60);
}
var _origEnsureBuilt = ensureBuilt;
ensureBuilt = function(id){ _origEnsureBuilt(id); _injectTasks(id); };
+2 -1
View File
@@ -17,6 +17,7 @@
<script src="/js/phys.js" defer></script>
<script src="/js/phys9_palette.js" defer></script>
<script src="/js/phys9_legacy.js" defer></script>
<script src="/js/phys9_finals.js" defer></script>
<script src="/js/phys9_ch5_widgets.js" defer></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
@@ -796,7 +797,7 @@ function _injectTasks(id){
var body = document.getElementById(id + '-body');
if(!body || body.querySelector('.legacy-tasks')) return;
body.insertAdjacentHTML('beforeend', _makeTaskBlock(id));
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_CH5_WIDGETS && window.PHYS9_CH5_WIDGETS[id]) window.PHYS9_CH5_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } }, 60);
setTimeout(function(){ try { if(window.renderTask) window.renderTask(id); if(window.renderNav) window.renderNav(id); } catch(e){} try { if(window.PHYS9_FINALS_INIT && /^final\d+$/.test(id)) window.PHYS9_FINALS_INIT(id); } catch(e){ console.warn("phys9 final init:", e.message); } try { if(window.PHYS9_CH5_WIDGETS && window.PHYS9_CH5_WIDGETS[id]) window.PHYS9_CH5_WIDGETS[id](); } catch(e){ console.warn('phys9 widget init:', e.message); } }, 60);
}
var _origEnsureBuilt = ensureBuilt;
ensureBuilt = function(id){ _origEnsureBuilt(id); _injectTasks(id); };