feat(geom10 W7): Финал Раздела 3 — 5 боссов + ачивка stereo10_r3_master

- Босс 1 Прямая ⊥ плоскость (4 этапа, +30 XP)
- Босс 2 Расстояния (4 этапа, +30 XP)
- Босс 3 Угол + ТТП (4 этапа, +35 XP, поддержка √2/2, 1/√3 и т.п.)
- Босс 4 ⊥-плоскости (4 этапа, +30 XP)
- Босс 5 Сборная (5 этапов, +45 XP — диагональ куба √3, sin угла 1/√3)
- Celebration: ачивка stereo10_r3_master + 130 XP бонус
- sec-nav: финал-таб разблокирован, refreshTabs учитывает {f1..f5}
- Состояние: STATE.bosses{f1..f5} + geometry10_achievements в localStorage
This commit is contained in:
Maxim Dolgolyov
2026-05-29 15:26:04 +03:00
parent cf4507a4d6
commit 7f045737d3
+206 -5
View File
@@ -194,7 +194,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 80px}
<a class="sec-tab" data-tab="8" href="#para-8"><span class="dot"></span>§8 Расстояния</a>
<a class="sec-tab" data-tab="9" href="#para-9"><span class="dot"></span>§9 Угол</a>
<a class="sec-tab" data-tab="10" href="#para-10"><span class="dot"></span>§10 Пл-сти ⊥</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>
@@ -731,7 +731,6 @@ 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>
@@ -740,10 +739,19 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 80px}
<div class="para-h-sub">5 интегральных боссов · ачивка «Перпендикулярность освоена»</div>
</div>
</div>
<div class="stub-card">
<b>Откроется в Волне W7</b>
Финал содержит 5 боссов и спецачивку <code>stereo10_r3_master</code>.
<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> Победи 5 боссов подряд</div>
<div class="viz-cap" style="color:var(--text-2);margin-top:0">Каждый босс — на одну тему: <b>прямая⊥плоскость</b>, <b>расстояния</b>, <b>угол наклонной + ТТП</b>, <b>⊥-плоскости</b>, <b>сборная задача</b>. После победы над всеми — ачивка <code>stereo10_r3_master</code> и +130 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 class="boss" id="boss-f5"></div>
<div id="celebration" style="display:none"></div>
</section>
</main>
@@ -825,6 +833,11 @@ function refreshTabs(){
if (n === '7' || n === '8' || n === '9' || n === '10'){
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','f5'].every(function(k){
return STATE.bosses && STATE.bosses[k] && STATE.bosses[k].defeated;
});
if (allBeat) t.classList.add('read');
}
});
}
@@ -1329,6 +1342,188 @@ var BOSS_DEFS = {
}
};
var FINAL_BOSS_DEFS = {
f1: {
title:'Босс 1 · Прямая ⊥ плоскость',
xp:30,
stages:[
{ q:'Признак $l \\perp \\alpha$: $l$ перпендикулярна двум $?$ прямым в $\\alpha$.', type:'mc', opts:['Параллельным','Пересекающимся','Произвольным'], correct:1, explain:'Двум ПЕРЕСЕКАЮЩИМСЯ прямым.' },
{ q:'Через точку проходит сколько прямых, перпендикулярных данной плоскости?', type:'input', a:'1', explain:'Единственная.' },
{ q:'Если $l \\perp \\alpha$ и $a \\subset \\alpha$, то $l$ и $a$…', type:'mc', opts:['Параллельны','Перпендикулярны','Скрещиваются'], correct:1, explain:'$l$ перпендикулярна любой прямой плоскости.' },
{ q:'Куб: ребро $DD_1$ перпендикулярно плоскости $ABCD$?', type:'mc', opts:['Да','Нет','Зависит'], correct:0, explain:'Все боковые рёбра ⊥ основанию.' }
]
},
f2: {
title:'Босс 2 · Расстояния',
xp:30,
stages:[
{ q:'Куб с ребром $3$. Расстояние от $A$ до плоскости $A_1B_1C_1D_1$?', type:'input', a:'3', explain:'Длина $AA_1 = 3$.' },
{ q:'Куб с ребром $1$. Расстояние между скрещ. рёбрами $AB$ и $D_1C_1$?', type:'input', a:['√2','sqrt(2)','sqrt2','1.41','1.414'], explain:'Диагональ грани $\\sqrt{2}$.' },
{ q:'Расстояние между скрещ. прямыми реализуется на каком отрезке?', type:'mc', opts:['Любом','Общем перпендикуляре','Биссектрисе','Касательной'], correct:1, explain:'Общий перпендикуляр.' },
{ q:'Перпендикуляр из точки на плоскость … любой наклонной из той же точки.', type:'mc', opts:['Длиннее','Короче','Равен'], correct:1, explain:'Короче (кратчайший отрезок).' }
]
},
f3: {
title:'Босс 3 · Угол + ТТП',
xp:35,
stages:[
{ q:'Что такое проекция наклонной $AB$ на плоскость $\\alpha$ (где $AH \\perp \\alpha$, $H \\in \\alpha$)?', type:'mc', opts:['$AH$','$HB$','$AB$'], correct:1, explain:'Проекция — отрезок $HB$.' },
{ q:'Угол между прямой и плоскостью — это угол между прямой и…', type:'mc', opts:['Перпендикуляром','Проекцией','Любой прямой'], correct:1, explain:'Наклонная и её проекция.' },
{ q:'ТТП: $AH \\perp \\alpha, BC \\subset \\alpha, HB \\perp BC$. Что верно?', type:'mc', opts:['$AB \\perp BC$','$AB \\parallel BC$','Ничего'], correct:0, explain:'$AB \\perp BC$.' },
{ q:'Куб (ребро 1): $\\tg$ угла между диагональю $AC_1$ и плоскостью $ABCD$?', type:'input', a:['1/√2','1/sqrt(2)','√2/2','sqrt(2)/2','0.707'], explain:'$\\tg \\varphi = \\frac{1}{\\sqrt{2}}$.' }
]
},
f4: {
title:'Босс 4 · ⊥-плоскости',
xp:30,
stages:[
{ q:'Двугранный угол — это…', type:'mc', opts:['Угол наклонной','2 полуплоскости с общим ребром','Угол прямых'], correct:1, explain:'Полуплоскости + общее ребро.' },
{ q:'Признак $\\alpha \\perp \\beta$: $\\alpha$ содержит прямую, $?$ к $\\beta$.', type:'mc', opts:['Параллельную','Перпендикулярную','Произвольную'], correct:1, explain:'Перпендикулярную $\\beta$.' },
{ q:'Сколько граней куба перпендикулярно одной заданной грани?', type:'input', a:'4', explain:'4 смежные.' },
{ q:'Линейный угол двугранного угла зависит от выбора точки на ребре?', type:'mc', opts:['Да','Нет'], correct:1, explain:'Не зависит.' }
]
},
f5: {
title:'Босс 5 · Сборная',
xp:45,
stages:[
{ q:'Куб с ребром $1$. Расстояние от $A$ до $C_1$? (длина диагонали куба)', type:'input', a:['√3','sqrt(3)','sqrt3','1.73','1.732'], explain:'Диагональ куба $= a\\sqrt{3}$.' },
{ q:'Куб (ребро 1): $\\sin$ угла между $AC_1$ и плоскостью $ABCD$?', type:'input', a:['1/√3','1/sqrt(3)','√3/3','sqrt(3)/3','0.577'], explain:'$\\sin \\varphi = \\frac{AA_1}{AC_1} = \\frac{1}{\\sqrt{3}}$.' },
{ q:'Если $l_1 \\perp \\alpha$ и $l_2 \\perp \\alpha$, то $l_1$ и $l_2$ — …', type:'mc', opts:['$\\parallel$','$\\perp$','Скрещиваются'], correct:0, explain:'Параллельны.' },
{ q:'В пирамиде высота $SO \\perp$ основание. Применяем ТТП: $OB \\perp BC \\Rightarrow$ ?', type:'mc', opts:['$SB \\perp BC$','$SB \\parallel BC$','Ничего'], correct:0, explain:'По ТТП: $SB \\perp BC$.' },
{ q:'Куб: диагональная плоскость $BDD_1B_1$ и грань $ABCD$ — это…', type:'mc', opts:['Параллельны','Перпендикулярны','Под $45°$'], correct:1, explain:'$BB_1 \\subset BDD_1B_1$ и $BB_1 \\perp ABCD$ ⇒ перпендикулярны.' }
]
}
};
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>';
function normalizeAns(s){
return String(s||'').toLowerCase()
.replace(/\s+/g,'')
.replace(/°/g,'')
.replace(/sqrt/g,'√')
.replace(/корень/g,'√')
.replace(/,/g,'.');
}
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','f5'].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';
cel.innerHTML = '<div class="boss victory" style="text-align:center;padding:40px 24px">'
+ '<div style="font-family:Unbounded,sans-serif;font-size:1.8rem;font-weight:900;color:#fef3c7;letter-spacing:-.01em;margin-bottom:8px">★ Раздел 3 пройден! ★</div>'
+ '<div style="font-size:1rem;color:#dcfce7;margin-bottom:16px">Все 5 финальных боссов побеждены. Перпендикулярность — освоена.</div>'
+ '<span class="boss-defeated-xp" style="font-size:1rem;padding:10px 22px">+ 130 XP бонус + ачивка «stereo10_r3_master»</span>'
+ '</div>';
var achKey = 'geometry10_achievements';
var raw = localStorage.getItem(achKey);
var list = [];
try { list = raw ? JSON.parse(raw) : []; } catch(e){}
if (list.indexOf('stereo10_r3_master') < 0){
list.push('stereo10_r3_master');
localStorage.setItem(achKey, JSON.stringify(list));
addXp(130, 'ачивка: Перпендикулярность освоена');
}
}
function renderBoss(num){
var def = BOSS_DEFS[num];
if (!def) return;
@@ -1476,6 +1671,12 @@ function start(){
renderBoss(8);
renderBoss(9);
renderBoss(10);
renderFinalBoss('f1');
renderFinalBoss('f2');
renderFinalBoss('f3');
renderFinalBoss('f4');
renderFinalBoss('f5');
checkFinalComplete();
document.getElementById('mark-7').addEventListener('click', function(){ markRead(7); });
document.getElementById('mark-8').addEventListener('click', function(){ markRead(8); });