From 31fb5d7ab0a2426475a9e311d95d92c096e360f5 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Wed, 27 May 2026 13:49:12 +0300 Subject: [PATCH] =?UTF-8?q?feat(textbooks):=20Wave=20Bosses=20=E2=80=94=20?= =?UTF-8?q?7=20=D0=B1=D0=B8=D1=82=D0=B2-=D0=BF=D1=80=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=BE=D0=BA=20(+971=20=D1=81=D1=82=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В конце каждого § перед 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-блоков валидны --- frontend/textbooks/algebra_8.html | 526 ++++++++++++++++++++++++++++++ 1 file changed, 526 insertions(+) diff --git a/frontend/textbooks/algebra_8.html b/frontend/textbooks/algebra_8.html index 2a2bd6d..bca69d6 100644 --- a/frontend/textbooks/algebra_8.html +++ b/frontend/textbooks/algebra_8.html @@ -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(){ `)} + ${bossWidget('p1')} + ${secNav(null, 'p2')} `; renderMath(body); @@ -2357,6 +2402,8 @@ function buildP2(){ `)} + ${bossWidget('p2')} + ${secNav('p1', 'p3')} `; renderMath(body); @@ -2768,6 +2815,8 @@ function buildP3(){ `)} + ${bossWidget('p3')} + ${secNav('p2', 'p4')} `; renderMath(body); @@ -3321,6 +3370,8 @@ function buildP4(){ `)} + ${bossWidget('p4')} + ${secNav('p3', 'p5')} `; renderMath(body); @@ -3754,6 +3805,8 @@ function buildP5(){ `)} + ${bossWidget('p5')} + ${secNav('p4', 'p6')} `; renderMath(body); @@ -4134,6 +4187,8 @@ function buildP6(){ `)} + ${bossWidget('p6')} + ${secNav('p5', 'final')} `; renderMath(body); @@ -4596,6 +4651,8 @@ function buildFinal(){ + ${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 `
+
+ +
+
${meta.tag}
+
${meta.title}
+
+ ${STATE.bossResults[secId] ? bossResultBadge(secId) : ``} +
+ + +
`; +} + +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 `
+
+ +
+
${medalLabel} · ${r.score}/${r.total}
+ +
`; +} + +/* ── 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 = `
${task.q}
`; + 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 = ' Подсказка (−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 = '✓ Верно! +' + 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 = '✗ Пропускаем. ' + 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 = '✗ Не точно — попробуй ещё раз!'; + } + // 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 = 'Подсказка (−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 = ` +
+ +
+
${medalTitle} Битва пройдена!
+
+
+
${score}/${tot}
+
Правильных
+
+
+
${tot - hintsUsed}
+
Без подсказки
+
+
+
+${xpReward}
+
XP награда
+
+
+
Время: ${timeStr} · Ошибок: ${BOSS_STATE.errors} · Подсказок: ${hintsUsed}
+ + `; + + 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}»!`; +});