feat(geom11 ch1 wave1): §1 «Призма» + 3D-конструктор + калькуляторы

This commit is contained in:
Maxim Dolgolyov
2026-05-29 14:07:51 +03:00
parent b771c3d497
commit b6bb1d9f48
+373 -3
View File
@@ -392,7 +392,7 @@ function buildParaSelector(){
}
const BUILT=new Set();
const BUILDERS = { p1:()=>buildStub('p1'), p2:()=>buildStub('p2'), final1:()=>buildStub('final1') };
const BUILDERS = { p1:()=>buildP1(), p2:()=>buildStub('p2'), final1:()=>buildStub('final1') };
function ensureBuilt(id){ if(BUILT.has(id)) return; const fn=BUILDERS[id]; if(fn){ fn(); BUILT.add(id); } }
function goTo(id){
STATE.current=id; ensureBuilt(id);
@@ -407,13 +407,13 @@ function goTo(id){
}
const SIDEBARS = {
p1:{title:"Шпаргалка § 1", rows:[["Тема", "Призма"],["Формула","$S_{бок}=Pl$, $V=S_{осн}h$"]]},
p1:{title:"Шпаргалка § 1", rows:[["Тема","Призма"],["Прямая","$S_{бок}=P_{осн}\\\\cdot h$"],["Наклонная","$S_{бок}=P_{пер}\\\\cdot l$"],["Объём","$V=S_{осн}\\\\cdot h$"],["Диагональ пар.","$d=\\\\sqrt{a^2+b^2+c^2}$"]]},
p2:{title:"Шпаргалка § 2", rows:[["Тема", "Цилиндр"],["Формула","$S_{бок}=2\\\\pi Rh$, $V=\\\\pi R^2h$"]]},
final1:{title:"Финал раздела 1", rows:[["§ 1–§ 2","теория раздела 1"],["Награда","+50 XP"]]}
};
const TIPS=[
{sec:'p1',html:"§ 1 «Призма» — содержание в разработке. $S_{бок}=Pl$, $V=S_{осн}h$"},
{sec:'p1',html:"§ 1 «Призма» — крути 3D-модель в интерактиве 1, проверь формулы в калькуляторе. Главное: $V=S_{осн}\\\\cdot h$, $S_{бок}=P_{осн}\\\\cdot h$ (для прямой)."},
{sec:'p2',html:"§ 2 «Цилиндр» — содержание в разработке. $S_{бок}=2\\\\\\\\pi Rh$, $V=\\\\\\\\pi R^2h$"},
{sec:'final1',html:"Финал раздела 1 — интегрированные задачи по разделу."}
];
@@ -619,6 +619,376 @@ function wireReadBtn(paraId){
});
}
/* ===== §1 «Призма» — Wave 1 ===== */
function buildP1(){
const box = document.getElementById('p1-body');
if(!box) return;
let html = '';
/* === ТЕОРИЯ === */
html += makeCard('theory', 'Определение и виды призм', '§ 1.1',
'<p><b>Призма</b> — многогранник, две грани которого (основания) — равные многоугольники, лежащие в параллельных плоскостях, а остальные грани (боковые) — параллелограммы.</p>'
+ '<p><b>Виды призм:</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li><b>Прямая</b> — боковые рёбра $\\perp$ основанию. Боковые грани — прямоугольники.</li>'
+ '<li><b>Наклонная</b> — боковые рёбра не перпендикулярны основанию.</li>'
+ '<li><b>Правильная</b> — прямая призма, основание которой — правильный многоугольник.</li>'
+ '</ul>'
+ '<p><b>Параллелепипед</b> — призма с параллелограммом в основании. У неё $6$ граней, $12$ рёбер, $8$ вершин.</p>'
+ '<p><b>Прямой параллелепипед</b> — боковые рёбра $\\perp$ основанию. <b>Прямоугольный параллелепипед</b> — прямой, у которого основание — прямоугольник (значит, все грани — прямоугольники). <b>Куб</b> — прямоугольный параллелепипед с равными рёбрами.</p>'
+ '<p style="margin-top:10px"><b>Свойства параллелепипеда:</b></p>'
+ '<ul style="margin:6px 0 0 22px;line-height:1.7">'
+ '<li>Противоположные грани равны и параллельны.</li>'
+ '<li>Все четыре диагонали пересекаются в одной точке и делятся ею пополам.</li>'
+ '<li>Диагональ прямоугольного параллелепипеда: $d=\\sqrt{a^2+b^2+c^2}$.</li>'
+ '</ul>');
html += makeCard('rule', 'Площадь поверхности и объём', '§ 1.2',
'<p><b>Боковая поверхность.</b></p>'
+ '<ul style="margin:6px 0 10px 22px;line-height:1.7">'
+ '<li>Прямой призмы: $S_{бок}=P_{осн}\\cdot h$, где $P_{осн}$ — периметр основания, $h$ — высота (= боковое ребро).</li>'
+ '<li>Наклонной призмы: $S_{бок}=P_{пер}\\cdot l$, где $P_{пер}$ — периметр <i>перпендикулярного сечения</i>, $l$ — боковое ребро.</li>'
+ '</ul>'
+ '<p><b>Полная поверхность:</b> $S_{полн}=S_{бок}+2\\cdot S_{осн}$.</p>'
+ '<p><b>Объём</b> (для любой призмы):</p>'
+ '<p style="text-align:center;margin:8px 0">$$V=S_{осн}\\cdot h$$</p>'
+ '<p style="font-size:.9rem;color:var(--muted)">где $h$ — высота призмы (расстояние между плоскостями оснований).</p>'
+ '<details class="spoiler"><summary>Пример: правильная 6-угольная призма</summary><div class="spoiler-body">'
+ '<p>Сторона основания $a=4$, высота $h=10$:</p>'
+ '<ul style="margin:6px 0 0 22px;line-height:1.7">'
+ '<li>$S_{осн}=\\dfrac{3\\sqrt{3}}{2}\\,a^2=24\\sqrt{3}\\approx 41{,}57$</li>'
+ '<li>$P_{осн}=6a=24$</li>'
+ '<li>$S_{бок}=24\\cdot 10=240$</li>'
+ '<li>$V=24\\sqrt{3}\\cdot 10=240\\sqrt{3}\\approx 415{,}7$</li>'
+ '</ul>'
+ '</div></details>');
html += makeCard('example', 'Диагональ прямоугольного параллелепипеда', '§ 1.3',
'<p>Пусть у прямоугольного параллелепипеда измерения $a$, $b$, $c$ (длина, ширина, высота).</p>'
+ '<p><b>Диагональ грани</b> (например, нижнего основания): $d_{грани}=\\sqrt{a^2+b^2}$.</p>'
+ '<p><b>Главная диагональ</b> — отрезок, соединяющий противоположные вершины. Тогда:</p>'
+ '<p style="text-align:center;margin:8px 0">$$d^2=a^2+b^2+c^2$$</p>'
+ '<p>Это <b>трёхмерная теорема Пифагора</b>.</p>'
+ '<details class="spoiler"><summary>Доказательство</summary><div class="spoiler-body">'
+ '<p>Возьмём вершину $A$ нижнего основания и противоположную $A_1$ верхнего. Соединим $A$ с вершиной $C$ нижнего основания, потом $C$ с $A_1$.</p>'
+ '<p>В треугольнике $ACA_1$ угол при $C$ прямой (ребро $CC_1=c$ перпендикулярно плоскости основания). По Пифагору в нижнем основании: $AC^2=a^2+b^2$. В $\\triangle ACA_1$: $AA_1^2=AC^2+CA_1^2=a^2+b^2+c^2$.</p>'
+ '</div></details>'
+ '<p style="margin-top:10px"><b>Пример:</b> куб со стороной $3 \\Rightarrow d=\\sqrt{9+9+9}=3\\sqrt{3}\\approx 5{,}20$.</p>');
/* === ИНТЕРАКТИВ 1 — 3D-конструктор === */
html += '<div class="wg" id="p1-iv1">'
+ '<div class="wg-header"><span class="wg-badge">3D · конструктор</span><div class="wg-title">Правильная n-угольная призма</div></div>'
+ '<div class="wg-help">Меняй число сторон основания $n$, радиус описанной окружности $R$ и высоту $h$. Вращай мышью или выбирай вид. После <b>4 разных конфигураций</b> — +10 XP.</div>'
+ '<div class="sliders">'
+ '<label>$n$ (сторон):<b id="p1-iv1-n-v">5</b><input type="range" id="p1-iv1-n" min="3" max="8" step="1" value="5"></label>'
+ '<label>$R$ (радиус):<b id="p1-iv1-R-v">1.5</b><input type="range" id="p1-iv1-R" min="1" max="4" step="0.1" value="1.5"></label>'
+ '<label>$h$ (высота):<b id="p1-iv1-h-v">2.4</b><input type="range" id="p1-iv1-h" min="1" max="6" step="0.1" value="2.4"></label>'
+ '</div>'
+ '<div class="g3d-tools">'
+ '<button class="btn" data-view="iso">Изо</button>'
+ '<button class="btn" data-view="front">Спереди</button>'
+ '<button class="btn" data-view="top">Сверху</button>'
+ '<button class="btn" data-view="side">Сбоку</button>'
+ '</div>'
+ '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:8px;text-align:center"><svg id="p1-iv1-svg" viewBox="0 0 480 400" width="100%" style="max-width:480px;height:auto"></svg></div>'
+ '<div class="score-display" style="margin-top:10px;flex-wrap:wrap">'
+ '<span>$P_{осн}=$<b id="p1-iv1-P">—</b></span>'
+ '<span>$S_{осн}=$<b id="p1-iv1-Sb">—</b></span>'
+ '<span>$S_{бок}=$<b id="p1-iv1-Ss">—</b></span>'
+ '<span>$S_{полн}=$<b id="p1-iv1-St">—</b></span>'
+ '<span>$V=$<b id="p1-iv1-V">—</b></span>'
+ '</div>'
+ '<div style="font-size:.78rem;color:var(--muted);margin-top:6px">Конфигураций изучено: <b id="p1-iv1-cnt">0</b> / 4</div>'
+ '</div>';
/* === ИНТЕРАКТИВ 2 — Калькулятор === */
html += '<div class="wg" id="p1-iv2">'
+ '<div class="wg-header"><span class="wg-badge">калькулятор</span><div class="wg-title">$V$, $S_{полн}$ и диагональ</div></div>'
+ '<div class="wg-help">Выбери тип призмы и задай размеры. Получишь полный разбор формул.</div>'
+ '<div style="display:flex;gap:14px;margin-bottom:10px;flex-wrap:wrap">'
+ '<label style="display:flex;align-items:center;gap:6px;cursor:pointer"><input type="radio" name="p1-iv2-type" value="reg" checked> Правильная $n$-угольная</label>'
+ '<label style="display:flex;align-items:center;gap:6px;cursor:pointer"><input type="radio" name="p1-iv2-type" value="rect"> Прямоугольный параллелепипед</label>'
+ '</div>'
+ '<div id="p1-iv2-fields-reg" class="sliders">'
+ '<label>$n$:<b id="p1-iv2-n-v">6</b><input type="range" id="p1-iv2-n" min="3" max="10" step="1" value="6"></label>'
+ '<label>$a$ (сторона):<b id="p1-iv2-a-v">4.0</b><input type="range" id="p1-iv2-a" min="1" max="8" step="0.1" value="4"></label>'
+ '<label>$h$ (высота):<b id="p1-iv2-h-v">10.0</b><input type="range" id="p1-iv2-h" min="1" max="15" step="0.1" value="10"></label>'
+ '</div>'
+ '<div id="p1-iv2-fields-rect" class="sliders" style="display:none">'
+ '<label>$a$:<b id="p1-iv2-ra-v">3.0</b><input type="range" id="p1-iv2-ra" min="1" max="6" step="0.1" value="3"></label>'
+ '<label>$b$:<b id="p1-iv2-rb-v">4.0</b><input type="range" id="p1-iv2-rb" min="1" max="6" step="0.1" value="4"></label>'
+ '<label>$c$:<b id="p1-iv2-rc-v">5.0</b><input type="range" id="p1-iv2-rc" min="1" max="6" step="0.1" value="5"></label>'
+ '</div>'
+ '<div class="actions"><button class="btn primary" id="p1-iv2-calc">Вычислить</button></div>'
+ '<div id="p1-iv2-out" style="margin-top:10px;font-size:.92rem;line-height:1.7"></div>'
+ '</div>';
/* === ИНТЕРАКТИВ 3 — DnD сортер === */
html += '<div class="wg" id="p1-iv3">'
+ '<div class="wg-header"><span class="wg-badge">сортер</span><div class="wg-title">Какая это призма?</div></div>'
+ '<div class="wg-help">Перетащи описание в нужный ящик. Кликни чип, затем ящик — тоже сработает.</div>'
+ '<div class="dnd-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/></svg>6 описаний → 3 типа</div>'
+ '<div id="p1-iv3-pool" class="dnd-pool"></div>'
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px">'
+ '<div class="drop-box"><h5>Прямая</h5><div class="drop-items" data-cat="straight"></div></div>'
+ '<div class="drop-box"><h5>Правильная</h5><div class="drop-items" data-cat="regular"></div></div>'
+ '<div class="drop-box"><h5>Наклонная</h5><div class="drop-items" data-cat="oblique"></div></div>'
+ '</div>'
+ '<div class="actions"><button class="btn primary" id="p1-iv3-check">Проверить</button><button class="btn" id="p1-iv3-reset">Сброс</button></div>'
+ '<div class="feedback" id="p1-iv3-fb"></div>'
+ '</div>';
/* === ИНТЕРАКТИВ 4 — Тренажёр === */
html += '<div class="wg" id="p1-iv4">'
+ '<div class="wg-header"><span class="wg-badge">тренажёр · 6 задач</span><div class="wg-title">$V$, $S$ и диагональ</div></div>'
+ '<div class="wg-help">Введи числовой ответ. Допуск $\\pm 0{,}05$ для дробных значений.</div>'
+ '<div id="p1-iv4-list"></div>'
+ '<div class="score-display" style="margin-top:10px">Решено: <b id="p1-iv4-score">0</b> / 6</div>'
+ '</div>';
html += secNavFor('p1');
html += readButton('p1');
box.innerHTML = html;
renderMath(box);
/* ====== JS-логика интерактивов ====== */
/* IV1 — 3D конструктор */
(function(){
if(!window.G3D) return;
const svg = document.getElementById('p1-iv1-svg');
const elN = document.getElementById('p1-iv1-n');
const elR = document.getElementById('p1-iv1-R');
const elH = document.getElementById('p1-iv1-h');
const vN = document.getElementById('p1-iv1-n-v');
const vR = document.getElementById('p1-iv1-R-v');
const vH = document.getElementById('p1-iv1-h-v');
const oP = document.getElementById('p1-iv1-P');
const oSb = document.getElementById('p1-iv1-Sb');
const oSs = document.getElementById('p1-iv1-Ss');
const oSt = document.getElementById('p1-iv1-St');
const oV = document.getElementById('p1-iv1-V');
const oCnt = document.getElementById('p1-iv1-cnt');
if(!svg) return;
const scene = G3D.createScene({W:480, H:400, scale:50, camDist:8, rotX:-0.35, rotY:0.7});
const seen = new Set();
let xpGiven = false;
function draw(){
const n = +elN.value, R = +elR.value, h = +elH.value;
vN.textContent = n;
vR.textContent = R.toFixed(1);
vH.textContent = h.toFixed(1);
const mesh = G3D.prismMesh(n, R, h);
const M = G3D.buildRotMatrix(scene);
svg.innerHTML = G3D.renderMesh(mesh, M, scene, {
fillBase:'rgba(252,231,243,.55)',
fillSide:'rgba(219,234,254,.55)',
strokeVisible:'#0f172a',
strokeHidden:'#94a3b8'
});
/* Формулы для правильного n-угольника, вписанного в окружность радиуса R */
const a = 2*R*Math.sin(Math.PI/n);
const P = n*a;
const Sb = 0.5*n*R*R*Math.sin(2*Math.PI/n);
const Ss = P*h;
const St = Ss + 2*Sb;
const V = Sb*h;
oP.textContent = P.toFixed(2);
oSb.textContent = Sb.toFixed(2);
oSs.textContent = Ss.toFixed(2);
oSt.textContent = St.toFixed(2);
oV.textContent = V.toFixed(2);
const key = n+'|'+R.toFixed(1)+'|'+h.toFixed(1);
seen.add(key);
oCnt.textContent = Math.min(seen.size, 4);
if(seen.size >= 4 && !xpGiven){
xpGiven = true;
addXp(10, 'p1-iv1');
bumpProgress('p1', 15);
const note = document.createElement('div');
note.className = 'feedback ok';
note.innerHTML = '&#10003; +10 XP за изучение 4 разных призм!';
note.style.cssText = 'display:block;margin-top:8px';
const host = document.getElementById('p1-iv1');
if(host) host.appendChild(note);
setTimeout(function(){ try{ note.remove(); }catch(e){} }, 3000);
}
}
draw();
G3D.attachOrbit(svg, scene, draw);
[elN, elR, elH].forEach(function(el){ el.addEventListener('input', draw); });
document.querySelectorAll('#p1-iv1 .g3d-tools .btn').forEach(function(b){
b.addEventListener('click', function(){ G3D.presetView(scene, b.dataset.view, draw); });
});
})();
/* IV2 — Калькулятор */
(function(){
const radios = document.querySelectorAll('input[name="p1-iv2-type"]');
const fldReg = document.getElementById('p1-iv2-fields-reg');
const fldRect = document.getElementById('p1-iv2-fields-rect');
const elN = document.getElementById('p1-iv2-n');
const elA = document.getElementById('p1-iv2-a');
const elH = document.getElementById('p1-iv2-h');
const elRa = document.getElementById('p1-iv2-ra');
const elRb = document.getElementById('p1-iv2-rb');
const elRc = document.getElementById('p1-iv2-rc');
const labels = {
'p1-iv2-n':'p1-iv2-n-v', 'p1-iv2-a':'p1-iv2-a-v', 'p1-iv2-h':'p1-iv2-h-v',
'p1-iv2-ra':'p1-iv2-ra-v', 'p1-iv2-rb':'p1-iv2-rb-v', 'p1-iv2-rc':'p1-iv2-rc-v'
};
Object.keys(labels).forEach(function(k){
const el = document.getElementById(k), lab = document.getElementById(labels[k]);
if(!el || !lab) return;
const upd = function(){ lab.textContent = el.step === '1' ? el.value : (+el.value).toFixed(1); };
el.addEventListener('input', upd); upd();
});
radios.forEach(function(r){ r.addEventListener('change', function(){
const v = document.querySelector('input[name="p1-iv2-type"]:checked').value;
fldReg.style.display = v === 'reg' ? '' : 'none';
fldRect.style.display = v === 'rect' ? '' : 'none';
}); });
let xpGiven = false;
document.getElementById('p1-iv2-calc').addEventListener('click', function(){
const out = document.getElementById('p1-iv2-out');
const v = document.querySelector('input[name="p1-iv2-type"]:checked').value;
let html = '';
if(v === 'reg'){
const n = +elN.value, a = +elA.value, h = +elH.value;
const P = n*a;
const apo = a / (2*Math.tan(Math.PI/n));
const Sb = 0.5 * P * apo;
const Ss = P * h;
const St = Ss + 2*Sb;
const V = Sb * h;
html = '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 14px">'
+ '<p><b>Правильная '+n+'-угольная призма, $a='+a.toFixed(2)+'$, $h='+h.toFixed(2)+'$</b></p>'
+ '<p>$P_{осн}=n\\cdot a='+n+'\\cdot '+a.toFixed(2)+'='+P.toFixed(2)+'$</p>'
+ '<p>$S_{осн}=\\dfrac{1}{2}P\\cdot a_{апо}=\\dfrac{n a^2}{4}\\cot\\dfrac{\\pi}{n}\\approx '+Sb.toFixed(2)+'$</p>'
+ '<p>$S_{бок}=P_{осн}\\cdot h='+P.toFixed(2)+'\\cdot '+h.toFixed(2)+'='+Ss.toFixed(2)+'$</p>'
+ '<p>$S_{полн}=S_{бок}+2S_{осн}\\approx '+St.toFixed(2)+'$</p>'
+ '<p style="font-weight:700;color:var(--pri2)">$V=S_{осн}\\cdot h\\approx '+V.toFixed(2)+'$</p>'
+ '</div>';
} else {
const a = +elRa.value, b = +elRb.value, c = +elRc.value;
const V = a*b*c;
const St = 2*(a*b + b*c + a*c);
const d = Math.sqrt(a*a + b*b + c*c);
html = '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 14px">'
+ '<p><b>Прямоугольный параллелепипед, $a='+a.toFixed(2)+'$, $b='+b.toFixed(2)+'$, $c='+c.toFixed(2)+'$</b></p>'
+ '<p>$V=abc='+a.toFixed(2)+'\\cdot '+b.toFixed(2)+'\\cdot '+c.toFixed(2)+'='+V.toFixed(2)+'$</p>'
+ '<p>$S_{полн}=2(ab+bc+ac)='+St.toFixed(2)+'$</p>'
+ '<p style="font-weight:700;color:var(--pri2)">$d=\\sqrt{a^2+b^2+c^2}=\\sqrt{'+(a*a+b*b+c*c).toFixed(2)+'}\\approx '+d.toFixed(2)+'$</p>'
+ '</div>';
}
out.innerHTML = html;
renderMath(out);
if(!xpGiven){
xpGiven = true;
addXp(10, 'p1-iv2');
bumpProgress('p1', 15);
}
});
})();
/* IV3 — DnD сортер */
(function(){
const items = [
{ id:'i1', html:'Основание — правильный 6-угольник, боковые рёбра $\\perp$ основанию', cat:'regular' },
{ id:'i2', html:'Боковые рёбра наклонены к основанию под углом 60°', cat:'oblique' },
{ id:'i3', html:'Все 6 граней — прямоугольники, все рёбра равны (куб)', cat:'straight' },
{ id:'i4', html:'Боковые рёбра $\\perp$ основанию, основание — параллелограмм', cat:'straight' },
{ id:'i5', html:'Основание — квадрат со стороной 3, высота 5, боковые рёбра $\\perp$', cat:'regular' },
{ id:'i6', html:'Боковые грани — параллелограммы, не прямоугольники', cat:'oblique' }
];
const sorter = setupSorter({
poolId:'p1-iv3-pool',
scopeSelector:'#p1-iv3',
items: items,
cats: ['straight','regular','oblique']
});
let xpGiven = false;
document.getElementById('p1-iv3-check').addEventListener('click', function(){
const fb = document.getElementById('p1-iv3-fb');
let correct = 0;
items.forEach(function(it){ if(sorter.placed[it.id] === it.cat) correct++; });
if(correct === items.length){
feedback(fb, true, '&#10003; Все 6 правильно! +15 XP');
if(!xpGiven){
xpGiven = true;
addXp(15, 'p1-iv3');
bumpProgress('p1', 25);
}
} else {
feedback(fb, false, '&#10007; Правильно: '+correct+' из '+items.length+'. Попробуй ещё раз.');
}
});
document.getElementById('p1-iv3-reset').addEventListener('click', function(){
sorter.reset();
const fb = document.getElementById('p1-iv3-fb'); fb.style.display = 'none';
});
})();
/* IV4 — Тренажёр */
(function(){
const tasks = [
{ q:'Прямоугольный параллелепипед $3\\times 4\\times 5$. $V=\\,?$', a:60, tol:0.05 },
{ q:'Куб со стороной 2. $V=\\,?$', a:8, tol:0.05 },
{ q:'Куб со стороной 3. Диагональ $d=\\,?$ (точность 0,01)', a:5.20, tol:0.05 },
{ q:'Прямоугольный параллелепипед $2\\times 3\\times 6$. Главная диагональ?', a:7, tol:0.05 },
{ q:'Правильная 4-угольная призма со стороной основания 4 и высотой 5. $V=\\,?$', a:80, tol:0.05 },
{ q:'Прямоугольный параллелепипед $3\\times 4\\times 5$. $S_{полн}=\\,?$', a:94, tol:0.05 }
];
const list = document.getElementById('p1-iv4-list');
const scoreEl = document.getElementById('p1-iv4-score');
const solved = new Set();
let xpGiven = false;
list.innerHTML = tasks.map(function(t, i){
return '<div style="background:var(--card);border:1px solid var(--border);border-radius:9px;padding:10px 12px;margin-bottom:8px">'
+ '<div style="margin-bottom:6px"><b>Задача '+(i+1)+'.</b> '+t.q+'</div>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">'
+ '<input type="text" class="tinp" id="p1-iv4-inp-'+i+'" placeholder="число" style="width:140px">'
+ '<button class="btn primary" data-i="'+i+'">Проверить</button>'
+ '</div>'
+ '<div class="feedback" id="p1-iv4-fb-'+i+'"></div>'
+ '</div>';
}).join('');
renderMath(list);
list.querySelectorAll('button[data-i]').forEach(function(b){
b.addEventListener('click', function(){
const i = +b.dataset.i, t = tasks[i];
const inp = document.getElementById('p1-iv4-inp-'+i);
const fb = document.getElementById('p1-iv4-fb-'+i);
const raw = (inp.value || '').replace(',', '.').trim();
const val = parseFloat(raw);
if(!isFinite(val)){ feedback(fb, false, '&#10007; Введи число'); return; }
if(Math.abs(val - t.a) <= t.tol){
feedback(fb, true, '&#10003; Верно!');
if(!solved.has(i)){
solved.add(i);
scoreEl.textContent = solved.size;
if(solved.size === tasks.length && !xpGiven){
xpGiven = true;
addXp(15, 'p1-iv4');
bumpProgress('p1', 25);
setTimeout(function(){ achievement('p1_done'); }, 400);
}
}
} else {
feedback(fb, false, '&#10007; Не точно. Пересчитай аккуратно.');
}
});
});
})();
wireReadBtn('p1');
}
/* ===== STUB BUILDER — единый для всех параграфов раздела (Phase 0) ===== */
function buildStub(id){