feat(geom10 W9): Финал Раздела 4 + МЕГА-АЧИВКА stereo10_master (Геометрия 10 пройдена!)

Финал R4:
- Босс 1 Координаты и расстояния (4 этапа, +30 XP)
- Босс 2 Векторы (4 этапа, +30 XP)
- Босс 3 Скалярное произведение (4 этапа, +35 XP)
- Босс 4 Сборная (5 этапов, +55 XP — диагональ куба √3, 2√3; cos углов диагоналей)
- Celebration: ачивка stereo10_r4_master + 120 XP бонус

ГЛАВНАЯ МЕХАНИКА: если в localStorage есть все 4 ачивки разделов
(stereo10_r1_master + stereo10_r2_master + stereo10_r3_master + stereo10_r4_master)
автоматически выдаётся МЕГА-АЧИВКА stereo10_master + 200 XP супер-бонус.
Если каких-то ачивок нет — celebration показывает список недостающих разделов.

sec-nav: финал-таб разблокирован, refreshTabs учитывает {f1..f4}.

ИТОГ: Геометрия 10 полностью завершена.
- 4 раздела, 14 параграфов
- ~140 интерактивов (квизы MC + input + tnp/слайдеры)
- 4 финала, 20+ боссов
- 5 ачивок: r1..r4 + master
- stereo3d.js (~650 строк) для всех 3D-рисунков
This commit is contained in:
Maxim Dolgolyov
2026-05-29 15:36:58 +03:00
parent c2a2497e49
commit 3869cebe95
+215 -6
View File
@@ -197,7 +197,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 80px}
<a class="sec-tab" data-tab="12" href="#para-12"><span class="dot"></span>§12 Векторы</a>
<a class="sec-tab" data-tab="13" href="#para-13"><span class="dot"></span>§13 a·b</a>
<a class="sec-tab" data-tab="14" href="#para-14"><span class="dot"></span>§14 Применение</a>
<a class="sec-tab locked" data-tab="final" href="#para-final"><span class="dot"></span>Финал</a>
<a class="sec-tab" data-tab="final" href="#para-final"><span class="dot"></span>Финал</a>
</div>
</nav>
@@ -761,19 +761,26 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 80px}
</div>
</section>
<!-- Final stub -->
<section id="para-final" class="para">
<div class="para-head">
<div class="para-num"></div>
<div class="para-h">
<h2>Финал раздела 4</h2>
<div class="para-h-sub">4 интегральных босса · ачивка «Геометрия 10 пройдена!» — главная награда курса</div>
<div class="para-h-sub">4 интегральных босса · ачивка «Геометрия 10 пройдена!»</div>
</div>
</div>
<div class="stub-card">
<b>Откроется в Волне W9</b>
Финальное испытание: 4 босса (координаты/расстояния, векторы, скалярное произведение, сборная) + спецачивка <code>stereo10_master</code> + 120 XP бонус.
<div class="viz" style="background:linear-gradient(135deg,var(--pri-soft),var(--pri-soft-2));border-color:var(--pri-l)">
<div class="viz-title" style="color:var(--pri-d)"><span class="badge">ФИНАЛЬНОЕ ИСПЫТАНИЕ</span> Победи 4 боссов подряд</div>
<div class="viz-cap" style="color:var(--text-2);margin-top:0">Каждый босс — на одну тему: <b>координаты и расстояния</b>, <b>векторы</b>, <b>скалярное произведение</b>, <b>сборная задача</b>. После победы — ачивка <code>stereo10_r4_master</code> и +120 XP. <b>Если у тебя уже есть все три ачивки</b> <code>stereo10_r1_master</code>, <code>stereo10_r2_master</code>, <code>stereo10_r3_master</code> — получишь <b>главную ачивку курса</b> <code>stereo10_master</code> «Геометрия 10 пройдена!» + 200 XP мега-бонус.</div>
</div>
<div class="boss" id="boss-f1"></div>
<div class="boss" id="boss-f2"></div>
<div class="boss" id="boss-f3"></div>
<div class="boss" id="boss-f4"></div>
<div id="celebration" style="display:none"></div>
</section>
</main>
@@ -855,6 +862,11 @@ function refreshTabs(){
if (n === '11' || n === '12' || n === '13' || n === '14'){
if (STATE.read.indexOf(parseInt(n,10)) >= 0) t.classList.add('read');
else t.classList.remove('read');
} else if (n === 'final'){
var allBeat = ['f1','f2','f3','f4'].every(function(k){
return STATE.bosses && STATE.bosses[k] && STATE.bosses[k].defeated;
});
if (allBeat) t.classList.add('read');
}
});
}
@@ -1266,6 +1278,198 @@ var BOSS_DEFS = {
}
};
var FINAL_BOSS_DEFS = {
f1: {
title:'Босс 1 · Координаты и расстояния',
xp:30,
stages:[
{ q:'$A(0;0;0)$, $B(2;2;1)$. $|AB|$ = ?', type:'input', a:'3', explain:'$\\sqrt{4+4+1}=3$.' },
{ q:'Точка $M(0;3;0)$ лежит на:', type:'mc', opts:['Оси $Ox$','Оси $Oy$','Оси $Oz$','В плоскости $Oxz$'], correct:1, explain:'На оси $Oy$.' },
{ q:'Середина $A(2;4;0)$ и $B(0;0;6)$ (формат x;y;z)?', type:'input', a:['1;2;3','1,2,3'], explain:'$(1;2;3)$.' },
{ q:'$|OM|$, если $M(1;2;2)$.', type:'input', a:'3', explain:'$\\sqrt{1+4+4}=3$.' }
]
},
f2: {
title:'Босс 2 · Векторы',
xp:30,
stages:[
{ q:'$\\vec{a}=(2;3;-1), \\vec{b}=(1;-2;4)$. $\\vec{a}+\\vec{b}$ (x;y;z)?', type:'input', a:['3;1;3'], explain:'$(3;1;3)$.' },
{ q:'$A(1;2;3), B(4;3;5)$. $\\vec{AB}$ (x;y;z)?', type:'input', a:['3;1;2'], explain:'$(4-1; 3-2; 5-3) = (3;1;2)$.' },
{ q:'$|\\vec{a}|$, если $\\vec{a}=(2;2;1)$.', type:'input', a:'3', explain:'$\\sqrt{4+4+1}=3$.' },
{ q:'$\\vec{a}=(2;4;6), \\vec{b}=(1;2;3)$. Коллинеарны?', type:'mc', opts:['Да','Нет'], correct:0, explain:'$\\vec{a}=2\\vec{b}$ — да.' }
]
},
f3: {
title:'Босс 3 · Скалярное произведение',
xp:35,
stages:[
{ q:'$\\vec{a}=(1;-2;3), \\vec{b}=(2;1;1)$. $\\vec{a}\\cdot\\vec{b}$ = ?', type:'input', a:'3', explain:'$2-2+3=3$.' },
{ q:'$\\vec{a}\\cdot\\vec{b}=0$ означает:', type:'mc', opts:['Коллинеарны','Перпендикулярны','Равны'], correct:1, explain:'$\\perp$.' },
{ q:'$\\vec{a}=(1;1;0), \\vec{b}=(1;0;1)$. $\\cos\\varphi$ = ?', type:'input', a:['1/2','0.5'], explain:'$\\cos = \\frac{1}{\\sqrt{2}\\cdot\\sqrt{2}} = \\frac{1}{2}$.' },
{ q:'$\\vec{a}\\cdot\\vec{a}$ при $\\vec{a}=(3;0;4)$ = ?', type:'input', a:'25', explain:'$9+0+16=25 = |\\vec{a}|^2$, $|\\vec{a}|=5$.' }
]
},
f4: {
title:'Босс 4 · Сборная',
xp:55,
stages:[
{ q:'Куб с ребром 1 в координатах: $A(0;0;0)$. Длина диагонали $AC_1$?', type:'input', a:['√3','sqrt(3)','sqrt3','1.73','1.732'], explain:'$\\sqrt{3}$.' },
{ q:'Куб (ребро 1). $\\cos$ угла между $\\vec{AB_1}=(1;0;1)$ и $\\vec{AD_1}=(0;1;1)$?', type:'input', a:['1/2','0.5'], explain:'$\\cos = \\frac{0+0+1}{\\sqrt{2}\\cdot\\sqrt{2}} = \\frac{1}{2}$.' },
{ q:'Угол между этими векторами в градусах?', type:'input', a:'60', explain:'$\\cos\\varphi = \\frac{1}{2} \\Rightarrow \\varphi = 60°$.' },
{ q:'Условие $\\vec{a}\\cdot\\vec{b} < 0$ означает, что угол $\\varphi$ …', type:'mc', opts:['Острый','Прямой','Тупой'], correct:2, explain:'Тупой ($\\cos < 0$).' },
{ q:'Куб (ребро 2), $A(0;0;0)$. $|AC_1|$?', type:'input', a:['2√3','2sqrt(3)','2sqrt3','3.46','3.464'], explain:'$\\sqrt{4+4+4}=2\\sqrt{3}$.' }
]
}
};
function renderFinalBoss(id){
var def = FINAL_BOSS_DEFS[id];
if (!def) return;
var el = document.getElementById('boss-' + id);
if (!el) return;
if (!STATE.bosses) STATE.bosses = {};
var st = STATE.bosses[id] || { stage:0, defeated:false };
STATE.bosses[id] = st;
if (st.defeated){
el.classList.add('victory');
el.innerHTML = '<div class="boss-defeated">'
+ '<div class="boss-defeated-title">' + def.title + ' побеждён!</div>'
+ '<span class="boss-defeated-xp">+' + def.xp + ' XP</span>'
+ '</div>';
checkFinalComplete();
return;
}
el.classList.remove('victory');
var total = def.stages.length;
var stage = def.stages[st.stage];
var hp = Math.round((1 - st.stage/total) * 100);
var optsHtml;
if (stage.type === 'mc'){
optsHtml = '<div class="boss-opts">';
for (var i = 0; i < stage.opts.length; i++){
optsHtml += '<button class="boss-opt" data-i="' + i + '">' + stage.opts[i] + '</button>';
}
optsHtml += '</div>';
} else {
optsHtml = '<div class="boss-input"><input type="text" id="boss-' + id + '-in" inputmode="text" placeholder="ответ"><button id="boss-' + id + '-go">Атака</button></div>';
}
el.innerHTML = '<div class="boss-h">'
+ '<span class="boss-badge">Финал</span>'
+ '<span class="boss-title">' + def.title + '</span>'
+ '</div>'
+ '<div class="boss-hp"><div class="boss-hp-label"><span>HP босса</span><span>' + hp + '%</span></div>'
+ '<div class="boss-hp-bar"><div class="boss-hp-fill" style="width:' + hp + '%"></div></div></div>'
+ '<div class="boss-question">'
+ '<div class="boss-stage-label">Этап ' + (st.stage+1) + ' / ' + total + '</div>'
+ '<div class="boss-q">' + stage.q + '</div>'
+ optsHtml + '</div>';
if (stage.type === 'mc'){
el.querySelectorAll('.boss-opt').forEach(function(btn){
btn.addEventListener('click', function(){
var i = parseInt(btn.getAttribute('data-i'), 10);
var ok = (i === stage.correct);
if (ok){
btn.classList.add('correct');
setTimeout(function(){ advanceFinalBoss(id); }, 600);
} else {
btn.classList.add('wrong');
setTimeout(function(){ btn.classList.remove('wrong'); }, 600);
}
});
});
} else {
var inEl = document.getElementById('boss-' + id + '-in');
var goEl = document.getElementById('boss-' + id + '-go');
var box = inEl.parentNode;
function attack(){
var v = normalizeAns(inEl.value);
var answers = Array.isArray(stage.a) ? stage.a.map(normalizeAns) : [normalizeAns(stage.a)];
if (answers.indexOf(v) >= 0){
inEl.style.background = 'rgba(34,197,94,.25)';
setTimeout(function(){ advanceFinalBoss(id); }, 500);
} else {
box.classList.add('wrong');
inEl.style.background = 'rgba(220,38,38,.25)';
setTimeout(function(){ box.classList.remove('wrong'); inEl.style.background=''; }, 600);
}
}
goEl.addEventListener('click', attack);
inEl.addEventListener('keydown', function(e){ if (e.key === 'Enter') attack(); });
}
tryKatex(el);
}
function advanceFinalBoss(id){
var st = STATE.bosses[id];
var def = FINAL_BOSS_DEFS[id];
st.stage++;
if (st.stage >= def.stages.length){
st.defeated = true;
saveState();
addXp(def.xp, def.title);
} else {
saveState();
}
renderFinalBoss(id);
}
function checkFinalComplete(){
var allBeat = ['f1','f2','f3','f4'].every(function(k){
return STATE.bosses[k] && STATE.bosses[k].defeated;
});
if (!allBeat) return;
var cel = document.getElementById('celebration');
if (!cel) return;
if (cel.dataset.shown === '1') return;
cel.dataset.shown = '1';
cel.style.display = 'block';
// Раздел 4 пройден
var achKey = 'geometry10_achievements';
var raw = localStorage.getItem(achKey);
var list = [];
try { list = raw ? JSON.parse(raw) : []; } catch(e){}
if (list.indexOf('stereo10_r4_master') < 0){
list.push('stereo10_r4_master');
localStorage.setItem(achKey, JSON.stringify(list));
addXp(120, 'ачивка: Раздел 4 пройден');
}
// Проверяем мега-достижение — все 4 ачивки разделов
var hasAll = ['stereo10_r1_master','stereo10_r2_master','stereo10_r3_master','stereo10_r4_master']
.every(function(a){ return list.indexOf(a) >= 0; });
var megaHtml = '';
if (hasAll && list.indexOf('stereo10_master') < 0){
list.push('stereo10_master');
localStorage.setItem(achKey, JSON.stringify(list));
addXp(200, 'МЕГА-АЧИВКА: Геометрия 10 пройдена!');
megaHtml = '<div style="margin-top:18px;padding:24px;background:linear-gradient(135deg,#fef3c7,#fde68a);border:2px solid #f59e0b;border-radius:14px">'
+ '<div style="font-family:Unbounded,sans-serif;font-size:1.5rem;font-weight:900;color:#92400e;margin-bottom:10px">★★★ ГЕОМЕТРИЯ 10 ПРОЙДЕНА! ★★★</div>'
+ '<div style="font-size:.95rem;color:#78350f;margin-bottom:14px">Все 4 раздела освоены. Стереометрия — твоя сила. Главная ачивка курса <code style="background:#fbbf24;padding:2px 8px;border-radius:5px">stereo10_master</code> + 200 XP мега-бонус.</div>'
+ '<span class="boss-defeated-xp" style="background:linear-gradient(135deg,#92400e,#fbbf24);padding:10px 22px">+ 200 XP МЕГА-БОНУС</span>'
+ '</div>';
} else if (hasAll) {
megaHtml = '<div style="margin-top:18px;padding:18px;background:linear-gradient(135deg,#fef3c7,#fde68a);border:2px solid #f59e0b;border-radius:14px;font-family:Unbounded,sans-serif;color:#92400e;font-weight:800">★ Главная ачивка <code style="background:#fbbf24;padding:2px 8px;border-radius:5px">stereo10_master</code> уже получена. Геометрия 10 пройдена!</div>';
} else {
var missing = [];
if (list.indexOf('stereo10_r1_master') < 0) missing.push('R1');
if (list.indexOf('stereo10_r2_master') < 0) missing.push('R2');
if (list.indexOf('stereo10_r3_master') < 0) missing.push('R3');
if (missing.length){
megaHtml = '<div style="margin-top:18px;padding:14px 18px;background:var(--card-2);border:1px dashed var(--pri-l);border-radius:11px;font-size:.88rem;color:var(--muted)">До главной ачивки <code>stereo10_master</code> осталось пройти финалы разделов: <b>' + missing.join(', ') + '</b>.</div>';
}
}
cel.innerHTML = '<div class="boss victory" style="text-align:center;padding:36px 24px">'
+ '<div style="font-family:Unbounded,sans-serif;font-size:1.8rem;font-weight:900;color:#fef3c7;letter-spacing:-.01em;margin-bottom:8px">★ Раздел 4 пройден! ★</div>'
+ '<div style="font-size:1rem;color:#dcfce7;margin-bottom:16px">Все 4 финальных босса побеждены. Координаты и векторы — освоены.</div>'
+ '<span class="boss-defeated-xp" style="font-size:1rem;padding:10px 22px">+ 120 XP бонус + ачивка «stereo10_r4_master»</span>'
+ '</div>' + megaHtml;
}
function renderBoss(num){
var def = BOSS_DEFS[num];
if (!def) return;
@@ -1402,6 +1606,11 @@ function start(){
renderBoss(12);
renderBoss(13);
renderBoss(14);
renderFinalBoss('f1');
renderFinalBoss('f2');
renderFinalBoss('f3');
renderFinalBoss('f4');
checkFinalComplete();
document.getElementById('mark-11').addEventListener('click', function(){ markRead(11); });
document.getElementById('mark-12').addEventListener('click', function(){ markRead(12); });