Files
Learn_System/frontend/js/phys9_finals.js
T
Maxim Dolgolyov 5b075cde86 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>
2026-05-30 09:55:44 +03:00

126 lines
6.4 KiB
JavaScript

// 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);
};
})();