feat(textbooks): Wave Bosses — 7 битв-проверок (+971 строка)

В конце каждого § перед secNav добавлена карточка 'Босс §N: <тема>' с битвой из 5-7 задач.

7 битв:
- §1 «Знаток корней» (7 задач): √121, √50 vs 7, √(−9), (√5)², √(a²), √0.81, число корней из 100
- §2 «Эксперт по числам» (6): множество для 1/3, √7 рацион/иррац, поиск иррац., 0.(3)=1/3, ℕ⊂ℝ, целые между √51
- §3 «Свойства корней» (7): √(9·25), √a·√b формула, √(64/16), √(a²)=a (нет), √100·√4, √81/√9, √(36a²)
- §4 «Преобразования» (6): √72=?, 5√3=√?, освобождение 1/√3, 3√2 vs 2√3, √200=?, (√7+√7)²
- §5 «Числовые промежутки» (6): запись x>5, (2;6)∩[4;10], 3∈(2;5], (-∞;0)∪(0;+∞), [1;4)∪[4;8], целые в [-3;4]
- §6 «Системы» (6): {x>2;x≤5}, [x≤1;x>4], -2<3x+1≤7, целые {x≥0;x<4}, {x≥5;x≤3}, {x²>0}
- Финальный босс (7 комбинированных): √(15²+8²), √75−√12, x²=49 число корней, D(√(x-3)+√(7-x)), √(10−2√21), 0.5≤x/3<2, √(0.04·49)

Движок (универсальный):
- 3 типа: select (кнопки), yesno, input (числовой с Enter)
- Полоса прогресса 'N / total'
- 2 попытки → объяснение → опционально пропуск (-5 XP)
- Подсказка -3 XP
- Медали: golden 7/7 без ошибок и подсказок | silver ≥5 | bronze прошёл
- XP: 30 / 50 / 80
- Perfect → доп. ачивка boss_pN_perfect
- 3D-flip анимация медали при награде
- Confetti при ≥4 правильных
- Интеграция с streak, sounds, achievement
- STATE.bossResults сохранён в LocalStorage algebra8_ch1_bossResults
- После прохождения в заголовке карточки отображается медаль + счёт + 'Повторить'

CSS: 52 строки новых стилей через --sec-acc для цветового разделения

Итог: 6829 строк, 11/11 JS-блоков валидны
This commit is contained in:
Maxim Dolgolyov
2026-05-27 13:49:12 +03:00
parent beebdadca0
commit 31fb5d7ab0
+526
View File
@@ -466,6 +466,45 @@ input,select,textarea{font-family:inherit}
@keyframes simpPop{0%{transform:scale(1)}50%{transform:scale(1.1)}100%{transform:scale(1)}}
@keyframes simpShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}}
/* ═══════════════════════════════════════════════
BOSS BATTLES — CSS
═══════════════════════════════════════════════ */
.boss-card{margin:30px 0 14px;padding:22px;background:linear-gradient(135deg,var(--card),var(--sec-acc-soft,var(--pri-soft)));border:2.5px solid var(--sec-acc,var(--pri));border-radius:18px;box-shadow:0 8px 32px rgba(0,0,0,.08);position:relative;overflow:hidden}
.boss-card::before{content:'';position:absolute;top:0;left:0;right:0;height:4px;background:linear-gradient(90deg,var(--sec-acc,var(--pri)),var(--acc));border-radius:18px 18px 0 0}
.boss-header{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.boss-header svg{color:var(--sec-acc,var(--pri))}
.boss-tag{font-family:'Unbounded',sans-serif;font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.1em;color:var(--sec-acc-d,var(--pri2))}
.boss-title{font-family:'Unbounded',sans-serif;font-size:1.3rem;font-weight:900;color:var(--text)}
.boss-header .btn{margin-left:auto}
.boss-arena{margin-top:18px;padding:18px;background:var(--card);border-radius:13px;border:1px solid var(--border)}
.boss-progress{display:flex;align-items:center;gap:12px;margin-bottom:18px}
.boss-progress-bar{flex:1;height:10px;background:rgba(0,0,0,.08);border-radius:6px;overflow:hidden}
.boss-progress-fill{height:100%;background:linear-gradient(90deg,var(--sec-acc,var(--pri)),var(--acc));border-radius:6px;width:0%;transition:width .4s}
.boss-progress-text{font-size:.85rem;font-weight:700;color:var(--muted);min-width:80px;text-align:right}
.boss-task{font-size:1.15rem;font-weight:600;color:var(--text);margin:18px 0;padding:18px;background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-radius:12px;text-align:center;min-height:80px;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:8px}
.boss-controls{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;margin-bottom:10px}
.boss-controls .btn{font-size:.95rem;padding:10px 18px;min-width:90px}
.boss-controls .b-act{background:var(--card);border:1.5px solid var(--border);font-weight:600}
.boss-controls .b-act:hover{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
.boss-controls .b-act.correct{background:var(--ok);color:#fff;border-color:var(--ok)}
.boss-controls .b-act.wrong{background:var(--fail);color:#fff;border-color:var(--fail);animation:simpShake .4s ease}
.boss-aux{margin-top:8px;display:flex;gap:8px;justify-content:center}
.boss-feedback{margin-top:10px}
.boss-inp{padding:9px 14px;border-radius:9px;background:var(--card);border:1.5px solid var(--border);font-size:1rem;color:var(--text);font-family:'JetBrains Mono',monospace;width:160px;text-align:center;outline:none}
.boss-inp:focus{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
.boss-result{padding:30px;text-align:center;animation:boxIn .5s cubic-bezier(.34,1.56,.64,1)}
@keyframes boxIn{from{opacity:0;transform:scale(.85) translateY(12px)}to{opacity:1;transform:none}}
.boss-medal{width:100px;height:100px;margin:0 auto 14px;border-radius:50%;display:flex;align-items:center;justify-content:center;animation:medalSpin .9s ease}
.boss-medal.gold{background:linear-gradient(135deg,#fbbf24,#f59e0b);box-shadow:0 0 0 6px rgba(245,158,11,.2)}
.boss-medal.silver{background:linear-gradient(135deg,#cbd5e1,#94a3b8);box-shadow:0 0 0 6px rgba(148,163,184,.2)}
.boss-medal.bronze{background:linear-gradient(135deg,#fb923c,#ea580c);box-shadow:0 0 0 6px rgba(234,88,12,.2)}
.boss-medal svg{width:50px;height:50px;color:#fff;stroke-width:2.5}
.boss-result-title{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;color:var(--sec-acc-d,var(--pri2));margin-bottom:8px}
.boss-result-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:18px 0;font-size:.95rem}
.boss-result-stat-num{font-size:1.6rem;font-weight:900;color:var(--sec-acc,var(--pri))}
.boss-result-stat-lab{font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}
@keyframes medalSpin{0%{transform:rotateY(0) scale(0.5)}50%{transform:rotateY(180deg) scale(1.1)}100%{transform:rotateY(360deg) scale(1)}}
/* Геометрическое доказательство §3 */
.geo-canvas-wrap{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:8px;margin-top:14px}
.geo-cell{filter:drop-shadow(0 1px 1px rgba(0,0,0,.12))}
@@ -942,6 +981,7 @@ const STATE = {
streak: 0,
maxStreak: 0,
dailyChallenge: { date: null, completed: false, taskIdx: 0 },
bossResults: {}, // secId → { passed, score, total, perfect }
};
/* Словарь имён достижений — используется и для отображения, и для retroactive-фикса старых записей */
@@ -993,6 +1033,8 @@ function loadProgress(){
if(sk){ const o = JSON.parse(sk); STATE.streak = o.streak||0; STATE.maxStreak = o.max||0; }
const dc = localStorage.getItem('algebra8_ch1_daily');
if(dc){ Object.assign(STATE.dailyChallenge, JSON.parse(dc)); }
const br = localStorage.getItem('algebra8_ch1_bossResults');
if(br){ Object.assign(STATE.bossResults, JSON.parse(br)); }
}catch(e){}
}
function saveProgress(){
@@ -1003,6 +1045,7 @@ function saveProgress(){
localStorage.setItem('algebra8_ch1_xp', String(STATE.xp));
localStorage.setItem('algebra8_ch1_streak', JSON.stringify({streak:STATE.streak, max:STATE.maxStreak}));
localStorage.setItem('algebra8_ch1_daily', JSON.stringify(STATE.dailyChallenge));
localStorage.setItem('algebra8_ch1_bossResults', JSON.stringify(STATE.bossResults));
}catch(e){}
}
function bumpProgress(key, delta){
@@ -1815,6 +1858,8 @@ function buildP1(){
</ol>
`)}
${bossWidget('p1')}
${secNav(null, 'p2')}
`;
renderMath(body);
@@ -2357,6 +2402,8 @@ function buildP2(){
</ol>
`)}
${bossWidget('p2')}
${secNav('p1', 'p3')}
`;
renderMath(body);
@@ -2768,6 +2815,8 @@ function buildP3(){
</ol>
`)}
${bossWidget('p3')}
${secNav('p2', 'p4')}
`;
renderMath(body);
@@ -3321,6 +3370,8 @@ function buildP4(){
</details>
`)}
${bossWidget('p4')}
${secNav('p3', 'p5')}
`;
renderMath(body);
@@ -3754,6 +3805,8 @@ function buildP5(){
</details>
`)}
${bossWidget('p5')}
${secNav('p4', 'p6')}
`;
renderMath(body);
@@ -4134,6 +4187,8 @@ function buildP6(){
</details>
`)}
${bossWidget('p6')}
${secNav('p5', 'final')}
`;
renderMath(body);
@@ -4596,6 +4651,8 @@ function buildFinal(){
</div>
</div>
${bossWidget('final')}
${secNav('p6', null)}
`;
renderMath(body);
@@ -4818,6 +4875,475 @@ function finalSummary(){
const sqBest = isFinite(STATE.squaresBest) ? STATE.squaresBest : '—';
alert(`Сводка по Главе 1:\n\nОбщий прогресс: ${total}%\nДостижений: ${ach}\nЛучшая «Таблица квадратов»: ${sqBest} очк.\n\nПродолжайте! Откройте Главу 2 «Квадратные уравнения» — там корни заработают на полную.`);
}
/* ════════════════════════════════════════════════════════
BOSS BATTLES ENGINE
════════════════════════════════════════════════════════ */
/* ── Task sets ── */
const BOSS_TASKS = {
p1: [
{ type:'input', q:'Найди $\\sqrt{121}$', a:'11', hint:'$11^2 = 121$', explanation:'$\\sqrt{121} = 11$, так как $11^2 = 121$.' },
{ type:'select', q:'Сравни $\\sqrt{50}$ и $7$', a:0, opts:['$\\sqrt{50} > 7$','$\\sqrt{50} < 7$','$\\sqrt{50} = 7$'], hint:'$7^2 = 49$, $50 > 49$', explanation:'$7^2 = 49 < 50$, значит $\\sqrt{50} > 7$.' },
{ type:'yesno', q:'Существует ли $\\sqrt{-9}$ среди действительных чисел?', a:false, hint:'Под корнем отрицательное', explanation:'Арифметический квадратный корень определён только для $a \\geq 0$.' },
{ type:'input', q:'Чему равно $(\\sqrt{5})^2$?', a:'5', hint:'$(\\sqrt{a})^2 = a$', explanation:'По определению: $(\\sqrt{a})^2 = a$ при $a \\geq 0$.' },
{ type:'select', q:'$\\sqrt{a^2}$ при $a = -7$ равно?', a:1, opts:['$-7$','$7$','$49$','$14$'], hint:'$\\sqrt{a^2} = |a|$', explanation:'$\\sqrt{(-7)^2} = \\sqrt{49} = 7 = |-7|$.' },
{ type:'input', q:'Найди $\\sqrt{0{,}81}$', a:'0.9', hint:'$0.9^2 = 0.81$', explanation:'$\\sqrt{0{,}81} = 0{,}9$, так как $0{,}9^2 = 0{,}81$.' },
{ type:'select', q:'Сколько квадратных корней из $100$?', a:2, opts:['$0$','$1$','$2$','Бесконечно'], hint:'Есть 10 и 10', explanation:'$10^2 = 100$ и $(-10)^2 = 100$. Два корня: $10$ и $-10$.' },
],
p2: [
{ type:'select', q:'К какому множеству принадлежит $\\dfrac{1}{3}$?', a:2, opts:['$\\mathbb{N}$','$\\mathbb{Z}$','$\\mathbb{Q}$','$\\mathbb{I}$ (иррац.)'], hint:'Это дробь', explanation:'$1/3$ — рациональное число ($\\mathbb{Q}$), так как это обыкновенная дробь.' },
{ type:'select', q:'$\\sqrt{7}$ — это число...', a:1, opts:['Рациональное','Иррациональное','Натуральное','Целое'], hint:'Нельзя записать в виде дроби', explanation:'$\\sqrt{7}$ — иррациональное, его нельзя представить в виде $m/n$.' },
{ type:'select', q:'Какое число иррационально?', a:1, opts:['$\\sqrt{16}$','$\\sqrt{7}$','$2/3$','$0{,}5$'], hint:'$\\sqrt{16} = 4$ — рациональное', explanation:'$\\sqrt{16} = 4$ — рациональное, $\\sqrt{7}$ — иррациональное.' },
{ type:'yesno', q:'Равны ли $0{,}(3)$ и $\\dfrac{1}{3}$?', a:true, hint:'$1/3 = 0.333...$', explanation:'$0{,}(3) = 0{,}333\\ldots = 1/3$. Да, равны.' },
{ type:'yesno', q:'Верно ли утверждение $\\mathbb{N} \\subset \\mathbb{R}$?', a:true, hint:'Натуральные входят в действительные', explanation:'$\\mathbb{N} \\subset \\mathbb{Z} \\subset \\mathbb{Q} \\subset \\mathbb{R}$. Да, верно.' },
{ type:'input', q:'Между какими целыми числами находится $\\sqrt{51}$? Введи наименьшее:', a:'7', hint:'$7^2=49$, $8^2=64$', explanation:'$7^2 = 49 < 51 < 64 = 8^2$, значит $7 < \\sqrt{51} < 8$.' },
],
p3: [
{ type:'select', q:'$\\sqrt{9 \\cdot 25} = ?$', a:0, opts:['$15$','$12$','$18$','$30$'], hint:'$\\sqrt{ab} = \\sqrt{a}\\cdot\\sqrt{b}$', explanation:'$\\sqrt{9 \\cdot 25} = \\sqrt{9}\\cdot\\sqrt{25} = 3\\cdot5 = 15$.' },
{ type:'select', q:'$\\sqrt{a} \\cdot \\sqrt{b}$ равно...', a:1, opts:['$\\sqrt{a+b}$','$\\sqrt{ab}$','$\\sqrt{a}+\\sqrt{b}$','$a \\cdot b$'], hint:'Свойство корня произведения', explanation:'По свойству: $\\sqrt{a}\\cdot\\sqrt{b} = \\sqrt{ab}$ при $a,b\\geq 0$.' },
{ type:'input', q:'$\\sqrt{64/16} = ?$', a:'2', hint:'$\\sqrt{a/b} = \\sqrt{a}/\\sqrt{b}$', explanation:'$\\sqrt{64/16} = \\sqrt{4} = 2$.' },
{ type:'yesno', q:'Верно ли: $\\sqrt{a^2} = a$ — всегда?', a:false, hint:'Попробуй $a = -3$', explanation:'Нет: $\\sqrt{a^2} = |a|$. При $a = -3$: $\\sqrt{9} = 3 \\neq -3$.' },
{ type:'input', q:'$\\sqrt{100} \\cdot \\sqrt{4} = ?$', a:'20', hint:'$\\sqrt{100}=10$, $\\sqrt{4}=2$', explanation:'$\\sqrt{100}\\cdot\\sqrt{4} = 10\\cdot2 = 20$.' },
{ type:'input', q:'$\\dfrac{\\sqrt{81}}{\\sqrt{9}} = ?$', a:'3', hint:'$\\sqrt{81/9} = \\sqrt{9}$', explanation:'$\\sqrt{81}/\\sqrt{9} = 9/3 = 3$.' },
{ type:'select', q:'Упрости $\\sqrt{36a^2}$ при $a \\geq 0$', a:0, opts:['$6a$','$36a$','$6a^2$','$\\sqrt{36}\\cdot a$'], hint:'$\\sqrt{36}=6$, $\\sqrt{a^2}=a$ при $a\\geq0$', explanation:'$\\sqrt{36a^2} = \\sqrt{36}\\cdot\\sqrt{a^2} = 6\\cdot a = 6a$.' },
],
p4: [
{ type:'input', q:'Вынеси из-под корня: $\\sqrt{72}$ — введи коэффициент (множитель вне корня):', a:'6', hint:'$72 = 36\\cdot2$', explanation:'$\\sqrt{72} = \\sqrt{36\\cdot2} = 6\\sqrt{2}$. Коэффициент равен 6.' },
{ type:'input', q:'Внеси под корень: $5\\sqrt{3} = \\sqrt{?}$. Введи подкоренное число:', a:'75', hint:'$5\\sqrt{3}=\\sqrt{25\\cdot3}$', explanation:'$5\\sqrt{3} = \\sqrt{5^2\\cdot3} = \\sqrt{75}$.' },
{ type:'select', q:'Освободи от иррациональности: $\\dfrac{1}{\\sqrt{3}} = ?$', a:1, opts:['$\\sqrt{3}$','$\\dfrac{\\sqrt{3}}{3}$','$\\dfrac{1}{3}$','$\\dfrac{3}{\\sqrt{3}}$'], hint:'Умножь числитель и знаменатель на $\\sqrt{3}$', explanation:'$\\dfrac{1}{\\sqrt{3}} = \\dfrac{\\sqrt{3}}{\\sqrt{3}\\cdot\\sqrt{3}} = \\dfrac{\\sqrt{3}}{3}$.' },
{ type:'select', q:'Что больше: $3\\sqrt{2}$ или $2\\sqrt{3}$?', a:0, opts:['$3\\sqrt{2} > 2\\sqrt{3}$','$3\\sqrt{2} < 2\\sqrt{3}$','Они равны'], hint:'$(3\\sqrt{2})^2 = 18$, $(2\\sqrt{3})^2 = 12$', explanation:'$(3\\sqrt{2})^2 = 18 > 12 = (2\\sqrt{3})^2$, значит $3\\sqrt{2} > 2\\sqrt{3}$.' },
{ type:'input', q:'Упрости $\\sqrt{200}$ — введи коэффициент вне корня:', a:'10', hint:'$200 = 100\\cdot2$', explanation:'$\\sqrt{200} = \\sqrt{100\\cdot2} = 10\\sqrt{2}$. Коэффициент 10.' },
{ type:'input', q:'$(\\sqrt{7} + \\sqrt{7})^2 = ?$', a:'28', hint:'$\\sqrt{7}+\\sqrt{7} = 2\\sqrt{7}$', explanation:'$\\sqrt{7}+\\sqrt{7} = 2\\sqrt{7}$; $(2\\sqrt{7})^2 = 4\\cdot7 = 28$.' },
],
p5: [
{ type:'select', q:'Запиши промежуток для $x > 5$', a:0, opts:['$(5; +\\infty)$','$[5; +\\infty)$','$(-\\infty; 5)$','$[5; +\\infty]$'], hint:'Строгое неравенство — открытая скобка', explanation:'$x > 5$ — строгое, значит 5 не включён: $(5; +\\infty)$.' },
{ type:'select', q:'$(2; 6) \\cap [4; 10] = ?$', a:0, opts:['$[4; 6)$','$(2; 10]$','$(4; 6)$','$(4; 10]$'], hint:'Пересечение берёт максимальную левую и минимальную правую', explanation:'Пересечение: $\\max(2,4)=4$ (включён у второго) и $\\min(6,10)=6$ (не включён у первого): $[4; 6)$.' },
{ type:'yesno', q:'Принадлежит ли $3$ промежутку $(2; 5]$?', a:true, hint:'$2 < 3 \\leq 5$?', explanation:'$2 < 3 \\leq 5$ да, $3 \\in (2; 5]$.' },
{ type:'select', q:'$(-\\infty; 0) \\cup (0; +\\infty)$ — это...', a:1, opts:['$\\mathbb{R}$','$\\mathbb{R} \\setminus \\{0\\}$','$\\varnothing$','$\\{0\\}$'], hint:'Вся прямая без нуля', explanation:'Объединение двух лучей без точки 0: $\\mathbb{R} \\setminus \\{0\\}$.' },
{ type:'select', q:'$[1; 4) \\cup [4; 8] = ?$', a:0, opts:['$[1; 8]$','$[1; 8)$','$(1; 8]$','$[1;4)\\cup[4;8]$'], hint:'Промежутки смыкаются в точке 4', explanation:'$[1;4)\\cup[4;8] = [1;8]$ — непрерывный отрезок.' },
{ type:'input', q:'Сколько целых чисел в $[-3; 4]$? Введи число:', a:'8', hint:'Считай: $-3, -2, -1, 0, 1, 2, 3, 4$', explanation:'Целые: $-3,-2,-1,0,1,2,3,4$ — всего 8.' },
],
p6: [
{ type:'select', q:'Решение системы $\\begin{cases}x>2\\\\x\\leq5\\end{cases}$ = ?', a:1, opts:['$(2;5)$','$(2;5]$','$[2;5]$','$(-\\infty;5]$'], hint:'x > 2 — открытая слева, x ≤ 5 — закрытая справа', explanation:'$x>2$ и $x\\leq5$: пересечение $(2;5]$.' },
{ type:'select', q:'Решение совокупности $\\left[\\begin{array}{l}x\\leq1\\\\x>4\\end{array}\\right.$ = ?', a:1, opts:['$(1;4]$','$(-\\infty;1]\\cup(4;+\\infty)$','$[1;4]$','$(1;4)$'], hint:'Совокупность — объединение', explanation:'Совокупность: $x\\leq1$ ИЛИ $x>4$ = $(-\\infty;1]\\cup(4;+\\infty)$.' },
{ type:'select', q:'$-2 < 3x + 1 \\leq 7$. Решение = ?', a:0, opts:['$(-1;2]$','$(-1;2)$','$[-1;2]$','$(-2;7]$'], hint:'Вычти 1, затем раздели на 3', explanation:'$-3 < 3x \\leq 6$; $-1 < x \\leq 2$; ответ: $(-1; 2]$.' },
{ type:'input', q:'Сколько целых решений у системы $\\begin{cases}x\\geq0\\\\x<4\\end{cases}$? Введи число:', a:'4', hint:'$0, 1, 2, 3$', explanation:'Целые числа: $0, 1, 2, 3$ четыре решения.' },
{ type:'yesno', q:'Имеет ли решение система $\\begin{cases}x\\geq5\\\\x\\leq3\\end{cases}$?', a:false, hint:'$x$ не может быть ≥5 и ≤3 одновременно', explanation:'Нет такого $x$. Система несовместна: $\\varnothing$.' },
{ type:'select', q:'Решение $\\{x^2 > 0\\}$ для $x \\in \\mathbb{R}$ = ?', a:1, opts:['$\\mathbb{R}$','$\\mathbb{R}\\setminus\\{0\\}$','$(0;+\\infty)$','$\\varnothing$'], hint:'$x^2 > 0$ при $x \\neq 0$', explanation:'$x^2 = 0$ только при $x = 0$, иначе $x^2 > 0$. Ответ: $\\mathbb{R}\\setminus\\{0\\}$.' },
],
final: [
{ type:'input', q:'$\\sqrt{15^2 + 8^2} = ?$ (теорема Пифагора)', a:'17', hint:'$15^2=225$, $8^2=64$, $225+64=289$', explanation:'$\\sqrt{225+64} = \\sqrt{289} = 17$.' },
{ type:'input', q:'Упрости $\\sqrt{75} - \\sqrt{12}$ — введи коэффициент при $\\sqrt{3}$:', a:'3', hint:'$\\sqrt{75}=5\\sqrt{3}$, $\\sqrt{12}=2\\sqrt{3}$', explanation:'$5\\sqrt{3} - 2\\sqrt{3} = 3\\sqrt{3}$. Коэффициент 3.' },
{ type:'select', q:'Реши $x^2 = 49$. Сколько корней?', a:2, opts:['$0$','$1$','$2$'], hint:'$x = \\pm 7$', explanation:'$x^2 = 49$ имеет два решения: $x = 7$ и $x = -7$.' },
{ type:'select', q:'Область определения $\\sqrt{x-3}+\\sqrt{7-x}$ = ?', a:0, opts:['$[3;7]$','$(3;7)$','$\\mathbb{R}$','$\\varnothing$'], hint:'Нужно $x-3\\geq0$ и $7-x\\geq0$', explanation:'$x\\geq3$ и $x\\leq7$ одновременно: $[3;7]$.' },
{ type:'select', q:'$\\sqrt{10 - 2\\sqrt{21}} = ?$', a:0, opts:['$\\sqrt{7}-\\sqrt{3}$','$\\sqrt{5}-\\sqrt{2}$','$\\sqrt{7}+\\sqrt{3}$','$\\sqrt{10}-1$'], hint:'$10-2\\sqrt{21} = 7-2\\sqrt{21}+3 = (\\sqrt{7}-\\sqrt{3})^2$', explanation:'$(\\sqrt{7}-\\sqrt{3})^2 = 7-2\\sqrt{21}+3 = 10-2\\sqrt{21}$. Ответ: $\\sqrt{7}-\\sqrt{3}$.' },
{ type:'select', q:'Реши $0{,}5 \\leq \\dfrac{x}{3} < 2$', a:0, opts:['$[1{,}5; 6)$','$(1{,}5; 6]$','$[1{,}5; 6]$','$(1{,}5; 6)$'], hint:'Умножь все части на 3', explanation:'$1{,}5 \\leq x < 6$: промежуток $[1{,}5; 6)$.' },
{ type:'input', q:'$\\sqrt{0{,}04 \\cdot 49} = ?$', a:'1.4', hint:'$\\sqrt{0.04}=0.2$, $\\sqrt{49}=7$', explanation:'$\\sqrt{0{,}04\\cdot49} = \\sqrt{0{,}04}\\cdot\\sqrt{49} = 0{,}2\\cdot7 = 1{,}4$.' },
],
};
/* ── Per-section metadata ── */
const BOSS_META = {
p1: { tag:'БОСС §1', title:'Знаток корней', ach:'boss_p1', achP:'boss_p1_perfect', sec:'p1' },
p2: { tag:'БОСС §2', title:'Эксперт по числам', ach:'boss_p2', achP:'boss_p2_perfect', sec:'p2' },
p3: { tag:'БОСС §3', title:'Свойства корней', ach:'boss_p3', achP:'boss_p3_perfect', sec:'p3' },
p4: { tag:'БОСС §4', title:'Преобразования', ach:'boss_p4', achP:'boss_p4_perfect', sec:'p4' },
p5: { tag:'БОСС §5', title:'Числовые промежутки', ach:'boss_p5', achP:'boss_p5_perfect', sec:'p5' },
p6: { tag:'БОСС §6', title:'Системы неравенств', ach:'boss_p6', achP:'boss_p6_perfect', sec:'p6' },
final: { tag:'ФИНАЛЬНЫЙ БОСС', title:'Глава 1', ach:'boss_final', achP:'boss_final_perfect', sec:'final' },
};
/* ── Runtime state ── */
const BOSS_STATE = {
current: null, // secId
idx: 0, // текущая задача
correct: 0,
hintsUsed: 0,
errors: 0,
t0: 0,
secondTry: false,
hintShown: false,
};
// Boss results are persisted in STATE.bossResults (initialized in STATE declaration)
/* ── HTML generator (called inside builders) ── */
function bossWidget(secId){
const meta = BOSS_META[secId];
const tasks = BOSS_TASKS[secId];
const tot = tasks.length;
return `<div id="boss-${secId}" class="boss-card">
<div class="boss-header">
<svg class="ic" viewBox="0 0 24 24" style="width:28px;height:28px;flex-shrink:0"><path d="M12 2L4 7v6c0 4.42 3.05 8.55 8 9.5 4.95-.95 8-5.08 8-9.5V7l-8-5z"/></svg>
<div>
<div class="boss-tag">${meta.tag}</div>
<div class="boss-title">${meta.title}</div>
</div>
${STATE.bossResults[secId] ? bossResultBadge(secId) : `<button class="btn primary" onclick="bossStart('${secId}')">Начать битву</button>`}
</div>
<div class="boss-arena" id="boss-arena-${secId}" style="display:none">
<div class="boss-progress">
<div class="boss-progress-bar"><div class="boss-progress-fill" id="boss-fill-${secId}"></div></div>
<div class="boss-progress-text">Задача <span id="boss-cur-${secId}">1</span> / <span id="boss-tot-${secId}">${tot}</span></div>
</div>
<div class="boss-task" id="boss-task-${secId}"></div>
<div class="boss-controls" id="boss-controls-${secId}"></div>
<div class="boss-aux" id="boss-aux-${secId}"></div>
<div class="boss-feedback feedback" id="boss-fb-${secId}"></div>
</div>
<div class="boss-result" id="boss-result-${secId}" style="display:none"></div>
</div>`;
}
function bossResultBadge(secId){
const r = STATE.bossResults[secId];
if(!r) return '';
const medal = r.perfect ? 'gold' : (r.score >= 5 ? 'silver' : 'bronze');
const medalLabel = r.perfect ? 'Идеально!' : (r.score >= 5 ? 'Серебро' : 'Бронза');
return `<div style="margin-left:auto;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<div class="boss-medal ${medal}" style="width:44px;height:44px;animation:none">
<svg class="ic" viewBox="0 0 24 24" style="width:24px;height:24px"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div style="font-size:.82rem;font-weight:700;color:var(--sec-acc-d,var(--pri2))">${medalLabel} · ${r.score}/${r.total}</div>
<button class="btn small" onclick="bossStart('${secId}')">Повторить</button>
</div>`;
}
/* ── Start battle ── */
function bossStart(secId){
const tasks = BOSS_TASKS[secId];
if(!tasks) return;
BOSS_STATE.current = secId;
BOSS_STATE.idx = 0;
BOSS_STATE.correct = 0;
BOSS_STATE.hintsUsed = 0;
BOSS_STATE.errors = 0;
BOSS_STATE.t0 = Date.now();
BOSS_STATE.secondTry = false;
BOSS_STATE.hintShown = false;
const arena = document.getElementById('boss-arena-' + secId);
const result = document.getElementById('boss-result-' + secId);
if(arena){ arena.style.display = ''; }
if(result){ result.style.display = 'none'; result.innerHTML = ''; }
// Update header button
const card = document.getElementById('boss-' + secId);
if(card){
const btn = card.querySelector('.boss-header .btn');
if(btn) btn.style.display = 'none';
}
bossRender();
}
/* ── Render current task ── */
function bossRender(){
const secId = BOSS_STATE.current;
const tasks = BOSS_TASKS[secId];
const idx = BOSS_STATE.idx;
const task = tasks[idx];
const tot = tasks.length;
// Progress bar
const fill = document.getElementById('boss-fill-' + secId);
if(fill) fill.style.width = (idx / tot * 100) + '%';
const cur = document.getElementById('boss-cur-' + secId);
if(cur) cur.textContent = idx + 1;
// Task text
const taskEl = document.getElementById('boss-task-' + secId);
if(taskEl){
taskEl.innerHTML = `<div>${task.q}</div>`;
renderMath(taskEl);
}
// Controls
const ctrl = document.getElementById('boss-controls-' + secId);
if(ctrl) ctrl.innerHTML = '';
const aux = document.getElementById('boss-aux-' + secId);
if(aux) aux.innerHTML = '';
BOSS_STATE.secondTry = false;
BOSS_STATE.hintShown = false;
if(task.type === 'select'){
task.opts.forEach((opt, i)=>{
const b = document.createElement('button');
b.className = 'btn b-act';
b.innerHTML = opt;
b.onclick = ()=>bossAnswer(i, b);
if(ctrl) ctrl.appendChild(b);
renderMath(b);
});
} else if(task.type === 'yesno'){
['Да','Нет'].forEach((label, i)=>{
const b = document.createElement('button');
b.className = 'btn b-act';
b.textContent = label;
b.onclick = ()=>bossAnswer(i === 0, b);
if(ctrl) ctrl.appendChild(b);
});
} else if(task.type === 'input'){
const inp = document.createElement('input');
inp.className = 'boss-inp';
inp.placeholder = 'Ответ';
inp.autocomplete = 'off';
inp.onkeydown = (e)=>{ if(e.key === 'Enter') bossSubmitInput(); };
if(ctrl){
ctrl.appendChild(inp);
const btn = document.createElement('button');
btn.className = 'btn primary';
btn.textContent = 'Проверить';
btn.onclick = bossSubmitInput;
ctrl.appendChild(btn);
}
setTimeout(()=>inp.focus(), 80);
}
// Aux: hint + skip
if(aux){
const hintBtn = document.createElement('button');
hintBtn.className = 'btn small';
hintBtn.innerHTML = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> Подсказка (3 XP)';
hintBtn.onclick = ()=>bossHint();
aux.appendChild(hintBtn);
const skipBtn = document.createElement('button');
skipBtn.className = 'btn small';
skipBtn.textContent = 'Пропустить (5 XP)';
skipBtn.onclick = ()=>bossSkip();
aux.appendChild(skipBtn);
}
// Clear feedback
const fb = document.getElementById('boss-fb-' + secId);
if(fb){ fb.className = 'boss-feedback feedback'; fb.innerHTML = ''; }
}
/* ── Submit input answer ── */
function bossSubmitInput(){
const secId = BOSS_STATE.current;
const ctrl = document.getElementById('boss-controls-' + secId);
if(!ctrl) return;
const inp = ctrl.querySelector('.boss-inp');
if(!inp) return;
const val = inp.value.trim();
bossAnswer(val, null);
}
/* ── Check answer ── */
function bossAnswer(value, btnEl){
const secId = BOSS_STATE.current;
const tasks = BOSS_TASKS[secId];
const task = tasks[BOSS_STATE.idx];
const fb = document.getElementById('boss-fb-' + secId);
let correct = false;
if(task.type === 'input'){
const norm = (s)=> String(s).replace(',','.').replace(/\s/g,'').toLowerCase();
correct = norm(value) === norm(task.a);
} else if(task.type === 'yesno'){
correct = (value === task.a);
} else {
correct = (value === task.a);
}
if(correct){
// Flash correct button
if(btnEl){ btnEl.classList.add('correct'); setTimeout(()=>{}, 400); }
else {
const ctrl = document.getElementById('boss-controls-' + secId);
if(ctrl) ctrl.querySelectorAll('.b-act').forEach(b=>b.disabled=true);
}
BOSS_STATE.correct++;
const xpGain = BOSS_STATE.secondTry ? 5 : (BOSS_STATE.hintShown ? 7 : 10);
addXp(xpGain, 'boss');
sounds.correct();
streakCorrect();
if(fb){
fb.className = 'boss-feedback feedback ok';
fb.innerHTML = '&#10003; Верно! +' + xpGain + ' XP · ' + task.explanation;
renderMath(fb);
}
// Disable all controls
const ctrl = document.getElementById('boss-controls-' + secId);
if(ctrl) ctrl.querySelectorAll('button,input').forEach(b=>b.disabled=true);
const aux = document.getElementById('boss-aux-' + secId);
if(aux) aux.querySelectorAll('button').forEach(b=>b.disabled=true);
setTimeout(()=>bossNext(), 1600);
} else {
if(BOSS_STATE.secondTry){
// Force fail
if(btnEl){ btnEl.classList.add('wrong'); }
if(fb){
fb.className = 'boss-feedback feedback fail';
fb.innerHTML = '&#10007; Пропускаем. ' + task.explanation;
renderMath(fb);
}
const ctrl = document.getElementById('boss-controls-' + secId);
if(ctrl) ctrl.querySelectorAll('button,input').forEach(b=>b.disabled=true);
const aux = document.getElementById('boss-aux-' + secId);
if(aux) aux.querySelectorAll('button').forEach(b=>b.disabled=true);
addXp(-5, 'boss-fail');
BOSS_STATE.errors++;
sounds.wrong();
streakWrong();
setTimeout(()=>bossNext(), 1800);
} else {
BOSS_STATE.secondTry = true;
BOSS_STATE.errors++;
sounds.wrong();
streakWrong();
if(btnEl){ btnEl.classList.add('wrong'); setTimeout(()=>btnEl.classList.remove('wrong'), 500); }
if(fb){
fb.className = 'boss-feedback feedback fail';
fb.innerHTML = '&#10007; Не точно — попробуй ещё раз!';
}
// Re-enable input if needed
const ctrl = document.getElementById('boss-controls-' + secId);
if(ctrl){
const inp = ctrl.querySelector('.boss-inp');
if(inp){ inp.value = ''; inp.focus(); }
}
}
}
}
/* ── Show hint ── */
function bossHint(){
const secId = BOSS_STATE.current;
const task = BOSS_TASKS[secId][BOSS_STATE.idx];
if(BOSS_STATE.hintShown) return;
BOSS_STATE.hintShown = true;
BOSS_STATE.hintsUsed++;
addXp(-3, 'boss-hint');
const fb = document.getElementById('boss-fb-' + secId);
if(fb){
fb.className = 'boss-feedback feedback ok';
fb.innerHTML = '<b>Подсказка</b> (3 XP): ' + task.hint;
renderMath(fb);
}
const aux = document.getElementById('boss-aux-' + secId);
if(aux){
const hintBtns = aux.querySelectorAll('button');
if(hintBtns[0]) hintBtns[0].disabled = true;
}
}
/* ── Skip ── */
function bossSkip(){
const secId = BOSS_STATE.current;
const task = BOSS_TASKS[secId][BOSS_STATE.idx];
addXp(-5, 'boss-skip');
BOSS_STATE.errors++;
const fb = document.getElementById('boss-fb-' + secId);
if(fb){
fb.className = 'boss-feedback feedback fail';
fb.innerHTML = 'Пропущено (5 XP). ' + task.explanation;
renderMath(fb);
}
const ctrl = document.getElementById('boss-controls-' + secId);
if(ctrl) ctrl.querySelectorAll('button,input').forEach(b=>b.disabled=true);
const aux = document.getElementById('boss-aux-' + secId);
if(aux) aux.querySelectorAll('button').forEach(b=>b.disabled=true);
setTimeout(()=>bossNext(), 1800);
}
/* ── Next task or finish ── */
function bossNext(){
const secId = BOSS_STATE.current;
const tasks = BOSS_TASKS[secId];
BOSS_STATE.idx++;
if(BOSS_STATE.idx >= tasks.length){
bossFinish();
} else {
bossRender();
}
}
/* ── Finish ── */
function bossFinish(){
const secId = BOSS_STATE.current;
const tasks = BOSS_TASKS[secId];
const meta = BOSS_META[secId];
const score = BOSS_STATE.correct;
const tot = tasks.length;
const elapsed = Math.round((Date.now() - BOSS_STATE.t0) / 1000);
const perfect = (score === tot && BOSS_STATE.errors === 0 && BOSS_STATE.hintsUsed === 0);
const hintsUsed = BOSS_STATE.hintsUsed;
// Determine medal
let medalClass, medalTitle;
if(score === tot && BOSS_STATE.errors === 0 && BOSS_STATE.hintsUsed === 0){
medalClass = 'gold'; medalTitle = 'Золото!';
} else if(score >= Math.ceil(tot * 5 / 7)){
medalClass = 'silver'; medalTitle = 'Серебро!';
} else {
medalClass = 'bronze'; medalTitle = 'Бронза!';
}
// XP reward
let xpReward = 0;
if(perfect){ xpReward = 80; }
else if(score >= Math.ceil(tot * 5 / 7)){ xpReward = 50; }
else if(score >= Math.ceil(tot * 4 / 7)){ xpReward = 30; }
else { xpReward = 10; }
addXp(xpReward, 'boss-finish');
// Save result
STATE.bossResults[secId] = { passed: score >= Math.ceil(tot * 4 / 7), score, total: tot, perfect };
saveProgress();
// Progress bump
const progMap = { p1:'p1', p2:'p2', p3:'p3', p4:'p4', p5:'p5', p6:'p6', final:'final' };
if(progMap[secId]) bumpProgress(progMap[secId], perfect ? 20 : (score >= Math.ceil(tot*5/7) ? 15 : 10));
// Achievements
achievement(meta.ach, `Босс «${meta.title}» пройден!`);
if(perfect) achievement(meta.achP, `Идеальная битва: «${meta.title}»!`);
// Hide arena, show result
const arena = document.getElementById('boss-arena-' + secId);
if(arena) arena.style.display = 'none';
const resultEl = document.getElementById('boss-result-' + secId);
if(!resultEl) return;
const timeStr = elapsed >= 60 ? Math.floor(elapsed/60) + ' мин ' + (elapsed%60) + ' сек' : elapsed + ' сек';
resultEl.style.display = '';
resultEl.innerHTML = `
<div class="boss-medal ${medalClass}">
<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div class="boss-result-title">${medalTitle} Битва пройдена!</div>
<div class="boss-result-stats">
<div>
<div class="boss-result-stat-num">${score}/${tot}</div>
<div class="boss-result-stat-lab">Правильных</div>
</div>
<div>
<div class="boss-result-stat-num">${tot - hintsUsed}</div>
<div class="boss-result-stat-lab">Без подсказки</div>
</div>
<div>
<div class="boss-result-stat-num">+${xpReward}</div>
<div class="boss-result-stat-lab">XP награда</div>
</div>
</div>
<div style="font-size:.88rem;color:var(--muted);margin-bottom:18px">Время: ${timeStr} · Ошибок: ${BOSS_STATE.errors} · Подсказок: ${hintsUsed}</div>
<button class="btn primary" onclick="bossStart('${secId}')">Повторить</button>
`;
if(score >= Math.ceil(tot * 4 / 7)) confetti();
}
/* ── Add boss_* labels to ACH_LABELS ── */
Object.entries(BOSS_META).forEach(([sid, m])=>{
ACH_LABELS[m.ach] = `Босс «${m.title}» пройден!`;
ACH_LABELS[m.achP] = `Идеальная битва: «${m.title}»!`;
});
</script>
<script>