feat(geom10 W4): Финал Раздела 2 — 4 босса + ачивка stereo10_r2_master

- Босс 1 Прямые в пространстве (4 этапа, +35 XP)
- Босс 2 Прямая и плоскость (4 этапа, +35 XP)
- Босс 3 Две плоскости (4 этапа, +35 XP)
- Босс 4 Сборная (5 этапов, +45 XP)
- Celebration: ачивка stereo10_r2_master + 100 XP бонус
- sec-nav: финал-таб разблокирован, отмечается при победе над всеми 4 боссами
- Состояние: STATE.bosses{f1..f4} + geometry10_achievements в localStorage
This commit is contained in:
Maxim Dolgolyov
2026-05-29 15:08:52 +03:00
parent eb19ce3cf9
commit 87a057f5b9
+185 -6
View File
@@ -193,7 +193,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 80px}
<a class="sec-tab active" data-tab="4" href="#para-4"><span class="dot"></span>§4 Прямые</a>
<a class="sec-tab" data-tab="5" href="#para-5"><span class="dot"></span>§5 Прямая и плоск.</a>
<a class="sec-tab" data-tab="6" href="#para-6"><span class="dot"></span>§6 Две плоскости</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>
@@ -561,7 +561,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>
@@ -570,11 +569,18 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 80px}
<div class="para-h-sub">4 интегральных босса · ачивка «Параллельность освоена»</div>
</div>
</div>
<div class="stub-card">
<b>Откроется в Волне W4</b>
Финал содержит 4 босса (прямые в пространстве, прямая и плоскость, две плоскости, сборная задача) и спецачивку <code>stereo10_r2_master</code>.
<small>До этого момента — побеждай боссов §4, §5, §6, чтобы заработать XP.</small>
<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>. После победы над всеми 4 — получишь ачивку <code>stereo10_r2_master</code> и +100 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>
@@ -656,6 +662,11 @@ function refreshTabs(){
if (n === '4' || n === '5' || n === '6'){
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');
}
});
}
@@ -1031,6 +1042,169 @@ var BOSS_DEFS = {
}
};
var FINAL_BOSS_DEFS = {
f1: {
title:'Босс 1 · Прямые в пространстве',
xp:35,
stages:[
{ q:'Куб: пара $A_1B_1$ и $D_1C_1$ — это…', type:'mc', opts:['Параллельные','Пересекающиеся','Скрещивающиеся'], correct:0, explain:'Противоположные стороны верхней грани — параллельны.' },
{ q:'Куб: пара $AD$ и $B_1C_1$ — это…', type:'mc', opts:['Параллельные','Пересекающиеся','Скрещивающиеся'], correct:0, explain:'$AD \\parallel BC \\parallel B_1C_1$ (параллельные).' },
{ q:'Угол между рёбрами куба $AB$ и $A_1D_1$?', type:'input', a:'90', explain:'Сдвинем $A_1D_1$ в нижнюю грань — получим $AD \\perp AB$. Угол $90°$.' },
{ q:'Сколько пар скрещивающихся рёбер можно найти у куба, выходящих из одной вершины $A$? (учитывая только пары без общей точки и не параллельные)', type:'mc', opts:['0','1','2','3'], correct:0, explain:'Из одной вершины все 3 ребра пересекаются в этой вершине — скрещивающихся пар нет.' }
]
},
f2: {
title:'Босс 2 · Прямая и плоскость',
xp:35,
stages:[
{ q:'Сколько случаев расположения прямой и плоскости?', type:'input', a:'3', explain:'Лежит / пересекает / параллельна.' },
{ q:'Прямая $a \\parallel \\alpha$. Сколько у них общих точек?', type:'input', a:'0', explain:'Параллельность — отсутствие общих точек.' },
{ q:'Куб: прямая $AC$ и плоскость грани $A_1B_1C_1D_1$. Расположение?', type:'mc', opts:['Лежит','Пересекает','Параллельна'], correct:2, explain:'$AC \\parallel A_1C_1 \\subset$ верх. грани ⇒ параллельна.' },
{ q:'$a \\parallel b \\subset \\alpha$, $a \\not\\subset \\alpha$. Вывод?', type:'mc', opts:['$a \\cap \\alpha$','$a \\parallel \\alpha$','$a \\subset \\alpha$'], correct:1, explain:'Признак параллельности прямой и плоскости.' }
]
},
f3: {
title:'Босс 3 · Две плоскости',
xp:35,
stages:[
{ q:'Сколько случаев расположения двух различных плоскостей?', type:'input', a:'2', explain:'Параллельны или пересекаются по прямой.' },
{ q:'$\\alpha$ содержит 2 пересекающиеся прямые $a, b \\parallel \\beta$. Вывод?', type:'mc', opts:['$\\alpha = \\beta$','$\\alpha \\parallel \\beta$','$\\alpha \\cap \\beta = c$'], correct:1, explain:'Признак параллельности плоскостей.' },
{ q:'Куб: плоскости верхней и нижней граней — это…', type:'mc', opts:['Параллельны','Пересекаются','Совпадают'], correct:0, explain:'Расстояние = ребру куба, плоскости параллельны.' },
{ q:'$\\alpha \\parallel \\beta$, $\\gamma$ пересекает $\\alpha$ по $a$. Линия пересечения $\\gamma$ и $\\beta$ — это $b$. Тогда $a$ и $b$…', type:'mc', opts:['Скрещиваются','Параллельны','Перпендикулярны'], correct:1, explain:'$a \\parallel b$ — линии пересечения параллельных плоскостей третьей плоскостью.' }
]
},
f4: {
title:'Босс 4 · Сборная',
xp:45,
stages:[
{ q:'Через 2 параллельные прямые проходит ровно … плоскостей.', type:'input', a:'1', explain:'Единственная — следствие из A1.' },
{ q:'Если $a \\parallel \\alpha$ и $b \\parallel a$, то $b$ относительно $\\alpha$…', type:'mc', opts:['$\\parallel \\alpha$ или $\\subset \\alpha$','Пересекает','Скрещивается'], correct:0, explain:'Транзитивность параллельности.' },
{ q:'Куб $ABCDA_1B_1C_1D_1$: сколько рёбер параллельно плоскости $ABCD$ и не лежит в ней?', type:'input', a:'4', explain:'4 ребра верхней грани $A_1B_1C_1D_1$ параллельны нижней плоскости.' },
{ q:'Скрещивающиеся прямые лежат в одной плоскости?', type:'mc', opts:['Да','Нет','Иногда'], correct:1, explain:'По определению — нет.' },
{ q:'Сколько прямых в плоскости $\\alpha$ можно построить, параллельных данной прямой $a \\parallel \\alpha$?', type:'mc', opts:['1','2','Бесконечно'], correct:2, explain:'Все прямые в $\\alpha$, параллельные $a$, — целое семейство (через каждую точку $\\alpha$).' }
]
}
};
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 = (inEl.value || '').trim().toLowerCase().replace(/\s+/g,'').replace('°','');
var a = String(stage.a).toLowerCase().replace(/\s+/g,'');
if (v === a){
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';
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">★ Раздел 2 пройден! ★</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">+ 100 XP бонус + ачивка «stereo10_r2_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_r2_master') < 0){
list.push('stereo10_r2_master');
localStorage.setItem(achKey, JSON.stringify(list));
addXp(100, 'ачивка: Параллельность освоена');
}
}
function renderBoss(num){
var def = BOSS_DEFS[num];
if (!def) return;
@@ -1162,6 +1336,11 @@ function start(){
renderBoss(4);
renderBoss(5);
renderBoss(6);
renderFinalBoss('f1');
renderFinalBoss('f2');
renderFinalBoss('f3');
renderFinalBoss('f4');
checkFinalComplete();
document.getElementById('mark-4').addEventListener('click', function(){ markRead(4); });
document.getElementById('mark-5').addEventListener('click', function(){ markRead(5); });