Files
Learn_System/frontend/textbooks/algebra_8_ch2.html
T
Maxim Dolgolyov 660e7e2747 feat(gamification): Phase 1 — full kill-switch + textbook XP wrapping
Until now the 'gamification' feature flag did nothing: it had no row in
app_settings, the admin couldn't toggle it, awardXP/awardCoins ignored
it, and the CSS only hid three dashboard widgets — XP bars in textbooks
stayed visible regardless.

Phase 1 closes every hole.

Backend (source of truth):
  • migration 029 seeds feature_gamification_enabled=1
  • new isGamificationEnabled() helper in gamification/_shared.js with a
    30s cache + invalidateGamificationCache() for instant admin toggles
  • awardXP / awardCoins / updateStreak / unlockAchievement /
    checkAchievements all bail out when the flag is off
  • /api/gamification/* and /api/shop/* (user routes) return 404 when
    disabled; admin routes remain open so the switch itself is reachable
  • adminController.updateFeatures gains 'gamification' in the allow-list
    and invalidates the cache on flip

Frontend:
  • LS.isGamificationEnabled() (synchronous, populated by loadFeatures)
    so xp.js + applyCosmetics can bail without a round-trip
  • xp.js load/add/flush become no-ops when the flag is off
  • applyCosmetics skips the round-trip when off
  • CSS .no-gamification rule expanded to cover .hero-xp-badge, .po-xp,
    .xp-card, .xp-bar, #frames-section, and a universal [data-gamified]
    hook for future blocks

Textbooks (Variant 2 of the plan):
  • backend/scripts/wrap_textbook_xp.py — idempotent script that adds
    data-gamified to 167 XP tags across 63 textbook files (chapters +
    hubs, all subjects/grades). Single CSS rule now hides everything.

Verified end-to-end: with the flag off, awardXP/awardCoins write nothing;
flipping back restores normal behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:43:24 +03:00

3374 lines
215 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Алгебра 8 · Глава 2 · Квадратные уравнения</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Manrope:wght@400;500;600;700;800&family=Unbounded:wght@400;700;800;900&display=swap" rel="stylesheet">
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<style>
:root{
--pri:#e91e63; --pri2:#c2185b; --pri-soft:#fce7f3;
--acc:#03a9f4; --acc2:#0288d1; --acc-soft:#e0f4ff;
--ok:#10b981; --ok-bg:#d1fae5;
--fail:#ef4444; --fail-bg:#fee2e2;
--warn:#f59e0b; --warn-bg:#fef3c7;
--bg:#fdf2f8; --card:#fff;
--text:#1a1a2e; --muted:#6b5b73;
--border:#fce7f3;
--sh:0 2px 12px rgba(233,30,99,.07);
--sh2:0 6px 24px rgba(233,30,99,.10);
}
.dark{
--bg:#1a0f1a; --card:#2a1929;
--text:#f5e6f0; --muted:#b0a0b0;
--border:#5a2a5a; --pri-soft:#4a1a3a;
--acc-soft:#1a3a4a;
--sh:0 2px 12px rgba(0,0,0,.4);
--sh2:0 6px 24px rgba(0,0,0,.5);
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{font-family:'Inter','Manrope',system-ui,sans-serif;background:var(--bg);color:var(--text);display:flex;flex-direction:column;min-height:100vh;line-height:1.5;transition:background .25s,color .25s}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
input,select,textarea{font-family:inherit}
.ic{width:1em;height:1em;display:inline-block;vertical-align:-.125em;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
/* HEADER */
.hdr{position:relative;background:linear-gradient(110deg,#c2185b 0%,#e91e63 55%,#ec407a 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(255,180,210,.2);min-height:130px}
.hdr::before{content:'ГЛАВА 2';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(5rem,15vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,220,235,.08);line-height:1;pointer-events:none;user-select:none;z-index:0}
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
.hdr-sub{font-size:.85rem;opacity:.88;margin-top:6px;font-weight:500;line-height:1.4}
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center}
.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.14);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;text-decoration:none}
.hdr-btn:hover{background:rgba(255,255,255,.24)}
/* MAIN */
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
.col-main{min-width:0}
/* HERO */
.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);background-size:200% 200%;animation:heroShift 12s ease-in-out infinite;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
.hero::before{content:'ax² + bx + c = 0';position:absolute;right:-10px;top:-20px;font-family:'JetBrains Mono',monospace;font-size:clamp(2rem,8vw,5.5rem);font-weight:900;color:var(--pri);opacity:.08;line-height:1;pointer-events:none}
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(233,30,99,.28)}
.hero-progress{flex:1;min-width:200px;max-width:280px}
.hp-label{font-size:.7rem;font-weight:700;color:var(--pri2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px;display:block}
.hp-bar{height:8px;background:rgba(233,30,99,.12);border-radius:6px;overflow:hidden}
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:6px;width:0%;transition:width .4s}
.hp-text{font-size:.72rem;color:var(--muted);margin-top:3px;display:block}
/* PARA SELECTOR */
.psel{margin-bottom:24px}
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px}
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
.psel-name{font-size:.88rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(233,30,99,.10);border-radius:3px;overflow:hidden}
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
.psel-card.final{background:linear-gradient(135deg,#fff5e1,#fce7f3)}
.dark .psel-card.final{background:linear-gradient(135deg,#3a2818,#4a1a3a)}
.psel-card.final .psel-num{color:var(--warn)}
/* SECTION COLORS */
.sec[id="sec-p7"] { --sec-acc:#f43f5e; --sec-acc-d:#be123c; --sec-acc-soft:#ffe4e6; }
.sec[id="sec-p8"] { --sec-acc:#06b6d4; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p9"] { --sec-acc:#d97706; --sec-acc-d:#b45309; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p10"] { --sec-acc:#6366f1; --sec-acc-d:#4338ca; --sec-acc-soft:#e0e7ff; }
.sec[id="sec-p11"] { --sec-acc:#059669; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
.sec[id="sec-p12"] { --sec-acc:#a21caf; --sec-acc-d:#86198f; --sec-acc-soft:#fae8ff; }
.sec[id="sec-final2"] { --sec-acc:#d97706; --sec-acc-d:#b45309; --sec-acc-soft:#fef3c7; }
/* SECTIONS */
.sec{display:none;position:relative;animation:fadeIn .35s ease}
.sec.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.sec::before{content:attr(data-watermark);position:absolute;right:-20px;top:10%;font-family:'Unbounded',sans-serif;font-size:clamp(6rem,18vw,14rem);font-weight:900;color:transparent;-webkit-text-stroke:1.5px var(--sec-acc-soft,var(--pri-soft));line-height:1;pointer-events:none;user-select:none;z-index:0;opacity:.35}
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.7rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
/* CARDS */
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(233,30,99,.06);position:relative;z-index:1;transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
.card:hover{transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(233,30,99,.12)}
.card-header{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px dashed var(--border)}
.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff;outline:2px solid var(--sec-acc-soft,transparent);outline-offset:1px}
.card-icon.repeat{background:#0ea5e9}
.card-icon.theory{background:#8b5cf6}
.card-icon.algo{background:#f59e0b}
.card-icon.rule{background:#ec4899}
.card-icon.example{background:#10b981}
.card-icon.oral{background:#06b6d4}
.card-icon.class{background:#3b82f6}
.card-icon.home{background:#f97316}
.card-icon.prev{background:#6366f1}
.card-icon .ic{width:18px;height:18px}
.card-title{font-family:'Unbounded',sans-serif;font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
.card-body{font-size:.95rem;line-height:1.65}
.card-body p{margin-bottom:10px}
.card-body p:last-child{margin-bottom:0}
.card-body b{color:var(--sec-acc-d,var(--pri2));font-weight:700}
.card-body ul,.card-body ol{padding-left:22px;margin:8px 0}
.card-body li{margin-bottom:4px}
.formula-box{background:var(--sec-acc-soft,var(--pri-soft));border-left:4px solid var(--sec-acc,var(--pri));border-radius:8px;padding:12px 16px;margin:10px 0;font-size:1.05rem}
.def-box{background:linear-gradient(135deg,var(--acc-soft),var(--card));border:1.5px solid var(--acc);border-radius:11px;padding:14px 16px;margin:12px 0}
.def-box-title{font-size:.78rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px;font-family:'Unbounded',sans-serif}
.note-warn{background:var(--warn-bg);border-left:4px solid var(--warn);border-radius:7px;padding:10px 14px;font-size:.9rem;margin:10px 0}
.dark .note-warn{background:rgba(245,158,11,.15);color:#fef3c7}
/* WIDGETS */
.wg{background:linear-gradient(135deg,var(--card),var(--sec-acc-soft,var(--pri-soft)));border:1.5px solid var(--sec-acc,var(--pri));border-radius:14px;padding:18px 20px;margin-bottom:18px;box-shadow:var(--sh2);position:relative;z-index:1;transition:box-shadow .25s}
.wg:hover{box-shadow:0 4px 12px rgba(0,0,0,.08),0 16px 40px rgba(233,30,99,.18)}
.wg-header{display:flex;align-items:center;gap:8px;margin-bottom:14px}
.wg-badge{padding:4px 9px;background:var(--sec-acc,var(--pri));color:#fff;border-radius:6px;font-family:'Unbounded',sans-serif;font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em}
.wg-title{font-family:'Unbounded',sans-serif;font-size:1.05rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));flex:1}
.wg-help{font-size:.88rem;color:var(--text);margin-bottom:12px;line-height:1.55;background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--sec-acc-soft,var(--pri-soft)));border-left:4px solid var(--warn,#f59e0b);padding:9px 14px;border-radius:9px;position:relative}
.wg-help::before{content:'?';position:absolute;left:-13px;top:50%;transform:translateY(-50%);width:22px;height:22px;border-radius:50%;background:var(--warn,#f59e0b);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:900;font-size:.78rem;box-shadow:0 2px 6px rgba(0,0,0,.18)}
.dark .wg-help{background:linear-gradient(135deg,rgba(245,158,11,.18),rgba(233,30,99,.10))}
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.btn:active{transform:scale(.96)}
.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
.btn.acc{background:var(--acc);color:#fff;border-color:var(--acc)}
.btn.ok{background:var(--ok);color:#fff;border-color:var(--ok)}
.btn.small{padding:5px 11px;font-size:.78rem}
.inp{padding:7px 12px;border-radius:8px;background:var(--card);border:1.5px solid var(--border);font-size:.92rem;color:var(--text);font-family:'JetBrains Mono',monospace;width:auto;min-width:60px}
.inp:focus{outline:none;border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
.inp.num{width:100px;text-align:center}
.slider{appearance:none;-webkit-appearance:none;height:6px;background:rgba(233,30,99,.18);border-radius:5px;width:100%;outline:none}
.slider::-webkit-slider-thumb{appearance:none;-webkit-appearance:none;width:18px;height:18px;background:var(--sec-acc,var(--pri));border-radius:50%;cursor:pointer;box-shadow:0 0 0 3px rgba(233,30,99,.25),0 2px 5px rgba(0,0,0,.18)}
.slider::-moz-range-thumb{width:18px;height:18px;background:var(--sec-acc,var(--pri));border-radius:50%;cursor:pointer;border:none}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:10px}
.row-c{display:flex;gap:10px;align-items:center;justify-content:center;flex-wrap:wrap;margin-bottom:10px}
.lab{font-size:.85rem;font-weight:600;color:var(--text);margin-right:4px}
.lab-mono{font-family:'JetBrains Mono',monospace;font-weight:700;color:var(--sec-acc-d,var(--pri2))}
.chip{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:7px;font-size:.84rem;font-weight:600;color:var(--sec-acc-d,var(--pri2))}
.chip.acc{background:var(--acc-soft);color:var(--acc2)}
.chip.ok{background:var(--ok-bg);color:#065f46}
.chip.fail{background:var(--fail-bg);color:#7f1d1d}
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
/* DRAG-DROP */
.dz{min-height:60px;padding:12px;border:2px dashed var(--border);border-radius:10px;background:var(--card);display:flex;flex-wrap:wrap;gap:6px;align-content:flex-start;transition:border-color .15s,background .15s}
.dz.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
.dz-label{font-size:.78rem;font-weight:700;color:var(--muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:.06em}
.drag-item{padding:6px 12px;background:linear-gradient(135deg,var(--acc),var(--acc2));color:#fff;border-radius:8px;font-weight:600;font-size:.88rem;cursor:grab;user-select:none;font-family:'JetBrains Mono',monospace;box-shadow:var(--sh);transition:transform .12s}
.drag-item:hover{transform:translateY(-1px)}
.drag-item:active{cursor:grabbing}
.drag-item.dragging{opacity:.5}
.drag-item.selected{outline:3px solid #FFD166;outline-offset:2px}
/* SIDEBAR */
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
.sidecard-row b{color:var(--pri);font-weight:700}
@media(max-width:980px){.col-side{position:static;max-height:none}}
/* ACHIEVEMENT POPUP */
.ach-popup{position:fixed;top:18px;right:18px;background:linear-gradient(135deg,#fcd34d,#f59e0b);color:#451a03;padding:13px 18px;border-radius:11px;font-weight:700;font-size:.9rem;font-family:'Unbounded',sans-serif;box-shadow:0 8px 32px rgba(245,158,11,.4);z-index:1000;display:none;align-items:center;gap:10px;animation:slideIn .35s ease}
.ach-popup.show{display:flex}
@keyframes slideIn{from{transform:translateX(110%);opacity:0}to{transform:none;opacity:1}}
.spoiler{margin:10px 0;border:1px dashed var(--sec-acc,var(--pri));border-radius:8px;overflow:hidden}
.spoiler summary{padding:8px 14px;background:var(--sec-acc-soft,var(--pri-soft));font-weight:700;cursor:pointer;font-size:.88rem;color:var(--sec-acc-d,var(--pri2));list-style:none;display:flex;align-items:center;gap:8px}
.spoiler summary::-webkit-details-marker{display:none}
.spoiler summary::before{content:'+';font-size:1.2rem;font-weight:900;color:var(--sec-acc,var(--pri));width:18px}
.spoiler[open] summary::before{content:''}
.spoiler-body{padding:10px 14px;font-size:.92rem;line-height:1.6}
.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
/* QUIZ */
.quiz{background:var(--card);border:1.5px solid var(--border);border-radius:12px;padding:14px 16px;margin-bottom:12px}
.quiz-q{font-weight:600;margin-bottom:10px;font-size:.94rem}
.quiz-opts{display:flex;flex-wrap:wrap;gap:6px}
.quiz-opt{padding:6px 12px;border:1.5px solid var(--border);border-radius:8px;font-size:.86rem;font-weight:600;cursor:pointer;background:var(--card);transition:background .12s,border-color .12s}
.quiz-opt:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.quiz-opt.correct{background:var(--ok-bg);border-color:var(--ok);color:#065f46}
.quiz-opt.wrong{background:var(--fail-bg);border-color:var(--fail);color:#7f1d1d}
.tbl{width:100%;border-collapse:collapse;margin:12px 0;font-size:.88rem}
.tbl th,.tbl td{padding:7px 10px;border:1px solid var(--border);text-align:center;font-family:'JetBrains Mono',monospace}
.tbl th{background:var(--sec-acc-soft,var(--pri-soft));color:var(--sec-acc-d,var(--pri2));font-weight:700}
/* SPECIFIC WIDGETS — added per § */
.live-ind{display:inline-block;margin-left:6px;font-weight:900;font-size:1.1rem}
.live-ind.ok{color:var(--ok)}
.live-ind.fail{color:var(--fail)}
/* CONSTRUCTOR uravnenia (§7) */
.uconstr-display{font-family:'JetBrains Mono',monospace;font-size:1.8rem;font-weight:800;text-align:center;padding:18px;background:var(--card);border-radius:11px;border:1px solid var(--border);margin:14px 0;color:var(--sec-acc-d,var(--pri2))}
.uconstr-type{display:inline-block;padding:6px 14px;border-radius:99px;font-size:.85rem;font-weight:700;margin-top:8px}
.uconstr-type.full{background:var(--ok-bg);color:#065f46}
.uconstr-type.incomp{background:var(--warn-bg);color:#92400e}
.uconstr-type.notq{background:var(--fail-bg);color:#7f1d1d}
/* BOOK PAGE задача */
.bookpage-svg{display:block;margin:0 auto;max-width:280px}
/* PIPELINE — пошаговый решатель */
.pipe-step{margin:8px 0;padding:10px 14px;background:var(--card);border-left:3px solid var(--sec-acc,var(--pri));border-radius:7px;font-family:'JetBrains Mono',monospace;font-size:.95rem;opacity:0;transform:translateX(-12px);transition:opacity .35s,transform .35s}
.pipe-step.show{opacity:1;transform:none}
.pipe-step b{color:var(--sec-acc-d,var(--pri2));font-family:'Inter',sans-serif;display:block;font-size:.78rem;margin-bottom:4px}
/* PARABOLA (§8) */
.parab-wrap{background:var(--card);border-radius:11px;border:1px solid var(--border);padding:8px;margin:14px 0}
/* CASES (§8) */
.case-card{background:var(--card);border:1.5px solid var(--border);border-radius:11px;padding:12px;text-align:center;cursor:pointer;transition:transform .15s,box-shadow .15s,border-color .15s}
.case-card:hover{transform:translateY(-2px);border-color:var(--sec-acc,var(--pri));box-shadow:var(--sh)}
.case-card.active{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));box-shadow:var(--sh2)}
.case-card h5{font-family:'Unbounded',sans-serif;font-size:.85rem;color:var(--sec-acc-d,var(--pri2));margin-bottom:4px}
/* SCORE display */
.score-display{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:10px 14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;margin-bottom:12px}
.score-display b{color:var(--sec-acc-d,var(--pri2));font-size:1.15rem}
/* SLIDERS / DROP / ACTIONS */
.sliders{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:10px}
.sliders label{display:block;font-size:.92rem;color:var(--muted);background:var(--card);padding:8px 12px;border-radius:8px;border:1px solid var(--border);line-height:1.5}
.sliders label .katex{font-size:1em}
.sliders label b{font-family:'JetBrains Mono',monospace;font-size:1.05rem;color:var(--sec-acc-d,var(--pri2));margin-left:4px}
.sliders label input[type="range"]{display:block;width:100%;margin-top:6px;accent-color:var(--sec-acc,var(--pri))}
.drop-box{background:var(--card);border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:90px;transition:border-color .15s,background .15s}
.drop-box:hover{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
.drop-box h5{font-family:'Unbounded',sans-serif;font-size:.78rem;color:var(--sec-acc-d,var(--pri2));margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em}
.drop-items{display:flex;flex-wrap:wrap;gap:6px;min-height:32px}
.drop-items .chip{cursor:pointer}
#p7s-pool .chip{cursor:pointer}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.eq-show{font-family:'JetBrains Mono',monospace}
.pipe-tabs .btn.active{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
/* XP badge in hero */
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(233,30,99,.22);font-family:'Unbounded',sans-serif}
.hero-xp-badge svg{flex-shrink:0}
/* XP card — единый стиль с главой 1 */
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
.xp-bar{height:9px;background:rgba(3,169,244,.15);border-radius:6px;overflow:hidden;margin:7px 0}
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
/* GLOSSARY tooltip */
.gloss-term{border-bottom:1.5px dotted var(--sec-acc,var(--pri));cursor:help;color:var(--sec-acc-d,var(--pri2));font-weight:600;padding:0 1px}
.gloss-term:hover{background:var(--sec-acc-soft,var(--pri-soft));border-radius:3px}
.gloss-tip{position:fixed;max-width:320px;padding:11px 14px;background:var(--card);border:1.5px solid var(--sec-acc,var(--pri));border-radius:11px;font-size:.84rem;line-height:1.55;box-shadow:0 12px 32px rgba(0,0,0,.18);z-index:9994;display:none;pointer-events:none;color:var(--text)}
.gloss-tip.show{display:block;animation:tipIn .15s ease}
.gloss-tip b{color:var(--sec-acc-d,var(--pri2));font-size:.92rem}
@keyframes tipIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
/* SEARCH MODAL */
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
.search-modal.show{display:flex;animation:fadeIn .15s ease}
.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
.search-results{flex:1;overflow-y:auto;padding:6px 0}
.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border-left:0;border-right:0;border-top:0;width:100%;color:var(--text)}
.search-row:hover,.search-row.active{background:var(--sec-acc-soft,var(--pri-soft))}
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px;background:var(--card-soft,transparent)}
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
/* DRAG & DROP — sortable chips */
.dnd-pool{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px;padding:10px;border:1.5px dashed var(--border);border-radius:10px;min-height:54px;transition:border-color .18s,background .18s}
.dnd-pool.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid}
.dnd-pool.col{flex-direction:column;align-items:stretch}
.dnd-pool.col .dnd-chip{width:auto}
.dnd-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--card);border:1.5px solid var(--border);border-radius:10px;cursor:grab;user-select:none;font-size:.92rem;line-height:1.4;transition:transform .12s,box-shadow .12s,border-color .12s;touch-action:none;max-width:100%}
.dnd-chip:hover{transform:translateY(-1px);border-color:var(--sec-acc,var(--pri));box-shadow:var(--sh)}
.dnd-chip:active{cursor:grabbing}
.dnd-chip.armed{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));box-shadow:0 0 0 3px rgba(244,63,94,.22);transform:translateY(-1px)}
.dnd-chip.dragging{opacity:.28}
.dnd-chip.placed{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.dnd-chip .dnd-x{padding:0 5px;color:var(--muted);font-weight:700;font-size:1.05rem;border-radius:4px;cursor:pointer}
.dnd-chip .dnd-x:hover{color:var(--bad,var(--fail));background:var(--fail-bg)}
.drop-box.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid;transform:scale(1.015)}
.dnd-hint{font-size:.78rem;color:var(--muted);margin-bottom:8px;display:flex;align-items:center;gap:6px}
.dnd-hint svg{width:14px;height:14px;flex-shrink:0}
/* SIDEBAR DRAWER for narrow viewports */
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none;animation:fadeIn .18s ease}
.col-side-backdrop.show{display:block}
@media(max-width:980px){
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
.col-side.open{transform:none}
}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Алгебра 8 · Глава 2</h1>
<div class="hdr-sub">Квадратные уравнения</div>
</div>
<div class="hdr-side">
<a href="/textbook/algebra-8" class="hdr-btn" title="К Алгебре 8 — все главы">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К алгебре 8
</a>
<button id="search-btn" class="hdr-btn" title="Поиск (Ctrl+K)">
<svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg>
<span>Поиск</span>
</button>
<button id="sidebar-btn" class="hdr-btn" title="Шпаргалка">
<svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg>
Шпаргалка
</button>
<button id="theme-btn" class="hdr-btn" title="Сменить тему">
<svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
<span id="theme-lab">Тёмная</span>
</button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<section class="hero">
<h2>Самый знаменитый тип уравнений</h2>
<p>Квадратные уравнения встречаются везде: от расчёта траектории брошенного камня до планировки сада. Эта глава покажет полный арсенал: <b>дискриминант</b>, <b>теорему Виета</b>, разложение на множители, решение через подстановку. После неё вы сможете решить любое квадратное уравнение.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p7')">
<svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg>
Начать § 7
</button>
<div class="hero-progress">
<span class="hp-label">Прогресс по главе</span>
<div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div>
<span id="hero-hp-text" class="hp-text">0%</span>
</div>
<div id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
</div>
</section>
<section class="psel">
<div class="psel-title">Параграфы главы</div>
<div id="psel-grid" class="psel-grid"></div>
</section>
<section id="sec-p7" class="sec" data-watermark="ax²">
<div class="sec-header"><span class="sec-num">§ 7</span><h2 class="sec-h">Квадратные уравнения. Решение неполных квадратных уравнений</h2></div>
<div id="p7-body"></div>
</section>
<section id="sec-p8" class="sec" data-watermark="D">
<div class="sec-header"><span class="sec-num">§ 8</span><h2 class="sec-h">Формулы корней квадратного уравнения</h2></div>
<div id="p8-body"></div>
</section>
<section id="sec-p9" class="sec" data-watermark="Виета">
<div class="sec-header"><span class="sec-num">§ 9</span><h2 class="sec-h">Теорема Виета</h2></div>
<div id="p9-body"></div>
</section>
<section id="sec-p10" class="sec" data-watermark="( )( )">
<div class="sec-header"><span class="sec-num">§ 10</span><h2 class="sec-h">Квадратный трёхчлен. Разложение на множители</h2></div>
<div id="p10-body"></div>
</section>
<section id="sec-p11" class="sec" data-watermark="задачи">
<div class="sec-header"><span class="sec-num">§ 11</span><h2 class="sec-h">Решение текстовых задач с помощью квадратных уравнений</h2></div>
<div id="p11-body"></div>
</section>
<section id="sec-p12" class="sec" data-watermark="t = x²">
<div class="sec-header"><span class="sec-num">§ 12</span><h2 class="sec-h">Целые рациональные уравнения, сводящиеся к квадратным</h2></div>
<div id="p12-body"></div>
</section>
<section id="sec-final2" class="sec" data-watermark="★">
<div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#f59e0b,#ec4899)">Финал главы</span><h2 class="sec-h">Итоги. Практическая и увлекательная математика</h2></div>
<div id="final2-body"></div>
</section>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot">Интерактивный учебник «Алгебра 8» · Глава 2 · LearnSpace · версия 1.0</footer>
<div id="ach-popup" class="ach-popup">
<svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><circle cx="12" cy="8" r="5"/><path d="M8 13l-2 8 6-4 6 4-2-8"/></svg>
<span id="ach-text">Достижение!</span>
</div>
<div id="gloss-tip" class="gloss-tip"></div>
<div id="search-modal" class="search-modal" role="dialog" aria-label="Поиск по главе">
<div class="search-box">
<input type="text" id="search-input" class="search-input" placeholder="Поиск: понятие, формула, параграф…" autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-foot">
<span><kbd>↑↓</kbd> навигация</span>
<span><kbd>Enter</kbd> открыть</span>
<span><kbd>Esc</kbd> закрыть</span>
</div>
</div>
</div>
<script>
'use strict';
/* STATE & PROGRESS */
const STATE = {
current: 'p7',
progress: { p7:0, p8:0, p9:0, p10:0, p11:0, p12:0, final2:0 },
achievements: new Map(),
xp: 0,
level: 1,
};
/* Уровни — единая формула с сервером (xpToLevel из _shared.js) */
const XP_LEVELS = null; // legacy — теперь уровень считается формулой
function calcLevel(xp){ return Math.floor(Math.sqrt((xp || 0) / 100)) + 1; }
function _xpForLevel(lv){ return (lv - 1) * (lv - 1) * 100; }
const ACH_LABELS = {
start: 'Начало главы 2!',
p7_constr: 'Конструктор уравнений',
p7_sort: 'Сортировка по типам',
p7_pipeline: 'Решил неполное по шагам',
p7_book: 'Задача про страницу',
p7_train: 'Тренажёр неполных',
p7_roots: 'Угадал есть ли корни',
p8_disc: 'Калькулятор дискриминанта',
p8_parab: 'Парабола и корни',
p8_cases: '3 случая дискриминанта',
p8_disc_train: 'Тренажёр дискриминанта',
p8_steps: 'Пошаговое решение',
p8_graph: 'Знак D по графику',
p9_vieta: 'Подбор по Виета',
p9_constr: 'Корни → уравнение',
p9_signs: 'Знаки корней',
p9_check: 'Проверка по Виета',
p9_nonpriv: 'Виета для a≠1',
p10_constr: 'Разложение на множители',
p10_steps: 'Шаговое разложение',
p10_train: 'Тренажёр разложения',
p10_fraction: 'Сокращение дробей',
p10_sort: 'Разложимо или нет',
p11_steps: 'Решил по 4 шагам',
p11_train: 'Тренажёр текстовых задач',
p11_move: 'Движение по реке',
p11_digit: 'Двузначное число',
p11_class: 'Классификатор задач',
p12_bi: 'Биквадратное решено',
p12_train: 'Тренажёр биквадратных',
p12_frac: 'Дробное → квадратное',
p12_subst: 'Замена переменной',
p12_odz: 'Посторонний корень',
boss_b1: 'Босс §7 повержен',
boss_b2: 'Босс §8 повержен',
boss_b3: 'Босс §9 повержен',
boss_b4: 'Босс §10 повержен',
boss_b5: 'Босс §11 повержен',
boss_b6: 'Босс §12 повержен',
boss_b7: 'Магистр алгебры',
all_bosses: 'Все 7 боссов побеждены!',
prac_streak: 'Серия из 5 верных',
};
function loadProgress(){
try{
const s = localStorage.getItem('algebra8_ch2_progress');
if(s) Object.assign(STATE.progress, JSON.parse(s));
const a = localStorage.getItem('algebra8_ch2_achievements');
if(a){
const p = JSON.parse(a);
if(Array.isArray(p)) p.forEach(id => STATE.achievements.set(id, ACH_LABELS[id] || id));
else if(p && typeof p === 'object'){
for(const [id, t] of Object.entries(p)) STATE.achievements.set(id, (t && t !== id) ? t : (ACH_LABELS[id] || id));
}
}
// Общий XP для всех глав. Если ещё нет — собираем из старых ch1/ch2 ключей (single-shot миграция).
let xp = localStorage.getItem('algebra8_xp');
if(xp === null){
const c1 = +(localStorage.getItem('algebra8_ch1_xp') || 0);
const c2 = +(localStorage.getItem('algebra8_ch2_xp') || 0);
xp = c1 + c2;
try { localStorage.setItem('algebra8_xp', String(xp)); } catch(e){}
}
STATE.xp = +xp || 0; STATE.level = calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('algebra8_ch2_progress', JSON.stringify(STATE.progress));
localStorage.setItem('algebra8_ch2_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('algebra8_xp', String(STATE.xp));
}catch(e){}
}
function bumpProgress(key, delta){
STATE.progress[key] = Math.max(0, Math.min(100, (STATE.progress[key]||0) + delta));
saveProgress();
refreshProgressUI();
if(STATE.progress[key] >= 50) markParaRead(key);
}
/* Server sync of read/last_para — пишем в БД, чтобы каталог /textbooks показывал прогресс */
const _TB_SLUG = 'algebra-8-ch2';
const _markedRead = new Set();
let _pendingProgressBody = null, _progressTimer = null;
function _flushProgress(){
const body = _pendingProgressBody;
_pendingProgressBody = null;
if(!body) return;
const tok = (window.LS && LS.getToken) ? LS.getToken() : '';
if(!tok) return;
fetch('/api/textbooks/' + _TB_SLUG + '/progress', {
method:'POST',
headers:{ 'Content-Type':'application/json', 'Authorization':'Bearer ' + tok },
body: JSON.stringify(body),
keepalive: true,
}).catch(()=>{});
}
function _queueProgress(patch){
_pendingProgressBody = Object.assign(_pendingProgressBody || {}, patch);
if(_progressTimer) clearTimeout(_progressTimer);
_progressTimer = setTimeout(_flushProgress, 600);
}
function markLastPara(id){ _queueProgress({ last_para: id }); }
function markParaRead(id){
if(_markedRead.has(id)) return;
_markedRead.add(id);
_queueProgress({ mark_read: id });
}
window.addEventListener('beforeunload', _flushProgress);
function loadServerReadState(){
const tok = (window.LS && LS.getToken) ? LS.getToken() : '';
if(!tok) return;
fetch('/api/textbooks/' + _TB_SLUG, { headers:{ 'Authorization':'Bearer ' + tok } })
.then(r => r.ok ? r.json() : null)
.then(d => {
if(!d || !d.progress) return;
(d.progress.read || []).forEach(k => {
_markedRead.add(k);
if((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100;
});
saveProgress(); refreshProgressUI();
})
.catch(()=>{});
}
function addXp(n, src){
if(!n) return;
const prev = STATE.level;
STATE.xp = Math.max(0, (STATE.xp || 0) + n);
STATE.level = calcLevel(STATE.xp);
saveProgress();
if(window.LS && window.LS.xp) window.LS.xp.add(n, 'algebra8-ch2-' + (src || 'misc'));
refreshProgressUI();
if(STATE.level > prev){
const pop = document.getElementById('ach-popup');
if(pop){
document.getElementById('ach-text').textContent = 'Уровень ' + STATE.level + '!';
pop.classList.add('show');
setTimeout(()=>pop.classList.remove('show'), 2600);
}
if(window.confetti) try { confetti(); } catch(e){}
}
}
function refreshProgressUI(){
const total = Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0) / 7);
const f = document.getElementById('hero-hp-fill');
if(f) f.style.width = total + '%';
const t = document.getElementById('hero-hp-text');
if(t) t.textContent = total + '% пройдено';
document.querySelectorAll('[data-prog-card]').forEach(el=>{
const k = el.dataset.progCard;
const fl = el.querySelector('.psel-prog-fill');
if(fl) fl.style.width = (STATE.progress[k]||0) + '%';
});
const xpBadge = document.getElementById('hero-xp-badge');
if(xpBadge){
xpBadge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><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> Ур. ' + STATE.level + ' · ' + (STATE.xp || 0) + ' XP';
}
// sidebar XP card sync
if(STATE.current && document.getElementById('sidebar-content')){
try { buildSidebar(STATE.current); } catch(e){}
}
}
function achievement(id, text){
if(STATE.achievements.has(id)) return;
STATE.achievements.set(id, text || ACH_LABELS[id] || id);
saveProgress();
const pop = document.getElementById('ach-popup');
if(pop){
document.getElementById('ach-text').textContent = text || ACH_LABELS[id] || id;
pop.classList.add('show');
setTimeout(()=>pop.classList.remove('show'), 3300);
}
addXp(20, 'ach');
}
/* PARA SELECTOR */
const PARAS = [
{ id:'p7', num:'§ 7', name:'Квадратные уравнения', sub:'Неполные' },
{ id:'p8', num:'§ 8', name:'Формулы корней', sub:'Дискриминант' },
{ id:'p9', num:'§ 9', name:'Теорема Виета', sub:'Подбор корней' },
{ id:'p10', num:'§ 10', name:'Квадратный трёхчлен', sub:'Разложение' },
{ id:'p11', num:'§ 11', name:'Текстовые задачи', sub:'4 шага' },
{ id:'p12', num:'§ 12', name:'Сводящиеся к квадратным', sub:'Биквадратные' },
{ id:'final2', num:'★', name:'Финал главы', sub:'Итоги · Практика', final:true },
];
function buildParaSelector(){
const g = document.getElementById('psel-grid');
g.innerHTML = '';
PARAS.forEach(p=>{
const card = document.createElement('div');
card.className = 'psel-card' + (p.final ? ' final' : '');
card.dataset.id = p.id;
card.dataset.progCard = p.id;
card.innerHTML = `
<div class="psel-num">${p.num}</div>
<div class="psel-name">${p.name}</div>
<div class="psel-prog"><div class="psel-prog-fill"></div></div>`;
card.addEventListener('click', ()=>goTo(p.id));
g.appendChild(card);
});
}
const BUILT = new Set();
const BUILDERS = {
p7:()=>buildP7(), p8:()=>buildP8(),
p9:()=>buildP9stub(), p10:()=>buildP10stub(),
p11:()=>buildP11stub(), p12:()=>buildP12stub(),
final2:()=>buildFinal2stub(),
};
function ensureBuilt(id){
if(BUILT.has(id)) return;
const fn = BUILDERS[id];
if(fn){ fn(); BUILT.add(id); }
}
function goTo(id){
STATE.current = id;
ensureBuilt(id);
document.querySelectorAll('.sec').forEach(s=>s.classList.remove('active'));
const el = document.getElementById('sec-' + id);
if(el) el.classList.add('active');
document.querySelectorAll('.psel-card').forEach(c=>c.classList.toggle('active', c.dataset.id === id));
buildSidebar(id);
window.scrollTo({top:0, behavior:'smooth'});
if((STATE.progress[id]||0) < 10) bumpProgress(id, 10);
if(window.renderMathInElement) setTimeout(()=>renderMath(el), 0);
// glossary wrap — пройти по тексту секции и обернуть термины
setTimeout(()=>{ try { wrapGlossary(el); } catch(e){} }, 60);
markLastPara(id);
}
/* SIDEBAR */
const SIDEBARS = {
p7: { title:'Шпаргалка § 7', rows:[
['$ax^2+bx+c=0$','квадратное, $a \\neq 0$'],
['$ax^2+bx=0$','неполное: $x(ax+b)=0$'],
['$ax^2+c=0$','неполное: $x^2 = -c/a$'],
['$ax^2=0$','неполное: $x=0$'],
['Свойство','$ab=0 \\Leftrightarrow a=0$ или $b=0$'],
]},
p8: { title:'Шпаргалка § 8', rows:[
['$D = b^2 - 4ac$','дискриминант'],
['$D > 0$','два корня: $x_{1,2} = \\dfrac{-b \\pm \\sqrt{D}}{2a}$'],
['$D = 0$','один корень: $x = \\dfrac{-b}{2a}$'],
['$D < 0$','корней нет'],
]},
p9: { title:'Шпаргалка § 9', rows:[
['Виета','$x_1+x_2=-p,\\ x_1 x_2 = q$ для $x^2+px+q=0$'],
['Общий','$x_1+x_2 = -b/a,\\ x_1 x_2 = c/a$'],
['$q > 0$','корни одного знака'],
['$q < 0$','корни разных знаков'],
['Обратная','если $x_1+x_2=-p$ и $x_1 x_2=q$ — это корни'],
]},
p10:{ title:'Шпаргалка § 10', rows:[
['Разложение','$ax^2+bx+c = a(x-x_1)(x-x_2)$'],
['Условие','$D \\geq 0$ (иначе нельзя)'],
['Корни','через дискриминант или Виета'],
['Сокращение','через разложение числителя и знаменателя'],
]},
p11:{ title:'Шпаргалка § 11', rows:[
['4 шага','анализ → модель → решение → проверка'],
['Движение','$s = v t$; общее $\\dfrac{s_1}{v_1} + \\dfrac{s_2}{v_2}$'],
['Работа','$A = p \\cdot t$; вместе: $p_1 + p_2 = \\dfrac{1}{t}$'],
['Числа','$\\overline{ab} = 10a + b$'],
]},
p12:{ title:'Шпаргалка § 12', rows:[
['Биквадр','$ax^4+bx^2+c=0$ → $t=x^2$'],
['Условие','$t \\geq 0$'],
['$x = \\pm\\sqrt{t}$','для каждого $t \\geq 0$'],
['Дробное','умножить на ОЗ, проверить ОДЗ'],
['ОДЗ','знаменатель $\\neq 0$'],
]},
final2:{ title:'Финал главы', rows:[
['7 боссов','один на каждый параграф + общий'],
['Тип задач','select / yes-no / input'],
['Награда','«Магистр квадратных уравнений»'],
['Практика','случайные задачи всей главы'],
['Серия 5×','+ достижение «Серия из 5 верных»'],
]},
};
const TIPS = [
{ sec:'p7', html:'Если в неполном <b>ax² + c = 0</b> знаки <i>a</i> и <i>c</i> одинаковые — корней нет. Проверяй знак $-c/a$.' },
{ sec:'p8', html:'$D = b^2 - 4ac$ — даже не считай корни, если $D < 0$.' },
{ sec:'p9', html:'Перед использованием Виета убедись, что <b>a = 1</b>. Иначе формулы дают $-b/a$ и $c/a$.' },
{ sec:'p10', html:'Если $D < 0$, на множители первой степени <b>не</b> раскладывается. Не пытайся!' },
{ sec:'p11', html:'В задачах на движение часто помогает «общее время»: $\\dfrac{s_1}{v_1} + \\dfrac{s_2}{v_2}$.' },
{ sec:'p12', html:'После $t = x^2$ всегда проверяй $t \\geq 0$ — отрицательные $t$ дают пустое множество.' },
{ sec:'final2', html:'Не бойся пробовать боссов несколько раз: ошибка не «съедает» прогресс.' },
];
function buildSidebar(id){
const box = document.getElementById('sidebar-content');
const sb = SIDEBARS[id] || SIDEBARS.p7;
let html = '';
// XP card — единый стиль с главой 1
const xpForLv = _xpForLevel(STATE.level);
const xpNext = _xpForLevel(STATE.level + 1);
const xpInLv = STATE.xp - xpForLv;
const xpRange = xpNext - xpForLv;
const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
html += `<div class="xp-card" data-gamified>
<div class="xp-card-title" data-gamified>
<span>XP-прогресс</span>
<span class="xp-level">Ур. ${STATE.level}</span>
</div>
<div class="xp-bar"><div class="xp-fill" style="width:${xpPct}%"></div></div>
<div class="xp-nums"><span>${STATE.xp} XP</span><span>${STATE.level < 30 ? xpNext + ' XP' : 'MAX'}</span></div>
</div>`;
// Шпаргалка
html += `<div class="sidecard"><h4>${sb.title}</h4>`;
sb.rows.forEach(([k,v])=>{
html += `<div class="sidecard-row"><b>${k}</b> ${v ? '— ' + v : ''}</div>`;
});
html += '</div>';
// Совет дня
const tip = TIPS.find(t => t.sec === id) || TIPS[0];
html += `<div class="sidecard tip-card" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)">
<h4 style="color:#92400e;display:flex;align-items:center;gap:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/><circle cx="12" cy="12" r="4"/></svg>
Подсказка
</h4>
<div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">${tip.html}</div>
</div>`;
// Достижения
if(STATE.achievements.size > 0){
html += `<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">${STATE.achievements.size}</span></h4>`;
[...STATE.achievements.values()].slice(-4).forEach(text=>{
html += `<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ${text}</div>`;
});
html += '</div>';
}
box.innerHTML = html;
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
}
/* THEME */
function initTheme(){
const t = localStorage.getItem('algebra8_ch2_theme') || 'light';
if(t === 'dark') document.documentElement.classList.add('dark');
document.getElementById('theme-lab').textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
document.getElementById('theme-btn').addEventListener('click', ()=>{
document.documentElement.classList.toggle('dark');
const dark = document.documentElement.classList.contains('dark');
localStorage.setItem('algebra8_ch2_theme', dark ? 'dark' : 'light');
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
});
}
/* HELPERS */
function $(sel, root){ return (root||document).querySelector(sel); }
function $$(sel, root){ return [...(root||document).querySelectorAll(sel)]; }
function el(tag, attrs, html){
const e = document.createElement(tag);
if(attrs) Object.entries(attrs).forEach(([k,v])=>{
if(k === 'class') e.className = v;
else if(k === 'style') e.style.cssText = v;
else if(k.startsWith('on') && typeof v === 'function') e.addEventListener(k.slice(2), v);
else e.setAttribute(k, v);
});
if(html != null) e.innerHTML = html;
return e;
}
function renderMath(root){
if(window.renderMathInElement){
try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){}
}
}
function feedback(elm, ok, text){
elm.className = 'feedback ' + (ok ? 'ok' : 'fail');
elm.innerHTML = text || (ok ? '&#10003; Верно!' : '&#10007; Неверно');
}
function sleep(ms){ return new Promise(r => setTimeout(r, ms)); }
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n - Math.round(n)) < 1e-9 ? String(Math.round(n)) : (+n.toFixed(4)).toString(); }
const ICONS = {
repeat: '<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
theory: '<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
algo: '<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
rule: '<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
example: '<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
oral: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
class: '<svg class="ic" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="14" rx="1"/><line x1="3" y1="21" x2="21" y2="21"/><polyline points="7 14 10 11 13 14 17 10"/></svg>',
home: '<svg class="ic" viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
};
function makeCard(kind, title, num, body){
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно',class:'Класс',home:'Домашка'};
return `<div class="card">
<div class="card-header">
<div class="card-icon ${kind}">${ICONS[kind]}</div>
<div class="card-title">${labels[kind] || ''} ${title && title !== labels[kind] ? '· ' + title : ''}</div>
${num ? `<div class="card-num">${num}</div>` : ''}
</div>
<div class="card-body">${body}</div>
</div>`;
}
function widget(title, badge, helpText, body){
return `<div class="wg">
<div class="wg-header"><span class="wg-badge">${badge||'INTERACT'}</span><div class="wg-title">${title}</div></div>
${helpText ? `<div class="wg-help">${helpText}</div>` : ''}
${body}
</div>`;
}
function secNav(prev, next){
const NAMES = {p7:'§7',p8:'§8',p9:'§9',p10:'§10',p11:'§11',p12:'§12',final2:'Финал'};
let h = '<div class="sec-nav">';
h += prev ? `<button class="btn" onclick="goTo('${prev}')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> ${NAMES[prev]}</button>` : '<span></span>';
h += next ? `<button class="btn primary" onclick="goTo('${next}')">${NAMES[next]} <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>` : '<span></span>';
h += '</div>';
return h;
}
/* CONFETTI (simplified) */
let _confettiCanvas = null, _confettiParticles = [], _confettiRaf = null;
function confetti(){
if(!_confettiCanvas){
_confettiCanvas = document.createElement('canvas');
_confettiCanvas.id = 'confetti-canvas';
_confettiCanvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999';
document.body.appendChild(_confettiCanvas);
}
const c = _confettiCanvas;
c.width = window.innerWidth; c.height = window.innerHeight;
const ctx = c.getContext('2d');
const colors = ['#e91e63','#03a9f4','#f59e0b','#10b981','#a855f7'];
for(let i = 0; i < 80; i++){
_confettiParticles.push({
x: window.innerWidth/2 + (Math.random()-0.5)*200,
y: window.innerHeight/2,
vx: (Math.random()-0.5)*14,
vy: -10 - Math.random()*10,
g: 0.4, life: 100, color: colors[i%colors.length], r: 4+Math.random()*4, rot: 0, vRot: (Math.random()-0.5)*0.3,
});
}
if(_confettiRaf) cancelAnimationFrame(_confettiRaf);
function frame(){
ctx.clearRect(0,0,c.width,c.height);
_confettiParticles = _confettiParticles.filter(p=>{
p.x += p.vx; p.y += p.vy; p.vy += p.g; p.life--; p.rot += p.vRot;
ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot);
ctx.fillStyle = p.color;
ctx.fillRect(-p.r, -p.r/2, p.r*2, p.r);
ctx.restore();
return p.life > 0 && p.y < c.height + 50;
});
if(_confettiParticles.length > 0) _confettiRaf = requestAnimationFrame(frame);
else { ctx.clearRect(0,0,c.width,c.height); _confettiRaf = null; }
}
frame();
}
/* ============================================================
DRAG & DROP SORTER — shared helper
============================================================
- desktop: drag chip → drop on .drop-box (or back on .dnd-pool)
- mobile/tap: tap chip → armed; tap a box → placed; tap × → remove
- re-rendering keeps it simple; setupSorter returns { placed, render } */
function setupSorter(cfg){
// cfg: { poolId, cats:[...], items:[{id,html,cat}], scopeSelector, columnLayout?:bool }
const placed = {};
const pool = document.getElementById(cfg.poolId);
const scope = document.querySelector(cfg.scopeSelector);
if(!pool || !scope) return { placed, render: ()=>{} };
pool.classList.add('dnd-pool');
if(cfg.columnLayout) pool.classList.add('col');
let armed = null;
function buildChip(it, isPlaced){
const el = document.createElement('div');
el.className = 'dnd-chip' + (isPlaced ? ' placed' : '');
el.dataset.id = it.id;
el.innerHTML = '<span class="dnd-txt">' + it.html + '</span><span class="dnd-x" title="Убрать">×</span>';
attachHandlers(el, it.id);
return el;
}
function attachHandlers(el, itId){
let ghost = null, dragging = false, startX = 0, startY = 0, captured = false;
el.addEventListener('pointerdown', ev => {
if(ev.button !== undefined && ev.button !== 0) return;
if(ev.target.classList && ev.target.classList.contains('dnd-x')){
ev.stopPropagation();
if(placed[itId]){ delete placed[itId]; render(); }
else if(armed === itId){ armed = null; render(); }
return;
}
startX = ev.clientX; startY = ev.clientY;
const rect = el.getBoundingClientRect();
const ox = ev.clientX - rect.left, oy = ev.clientY - rect.top;
try { el.setPointerCapture(ev.pointerId); captured = true; } catch(e){}
function onMove(e){
const dx = e.clientX - startX, dy = e.clientY - startY;
if(!dragging && Math.hypot(dx, dy) > 8){
dragging = true;
ghost = el.cloneNode(true);
ghost.classList.remove('armed');
ghost.style.cssText = 'position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:' + rect.width + 'px;left:' + (e.clientX - ox) + 'px;top:' + (e.clientY - oy) + 'px';
document.body.appendChild(ghost);
el.classList.add('dragging');
}
if(dragging && ghost){
ghost.style.left = (e.clientX - ox) + 'px';
ghost.style.top = (e.clientY - oy) + 'px';
const under = document.elementsFromPoint(e.clientX, e.clientY);
scope.querySelectorAll('.drop-box.over, .dnd-pool.over').forEach(n => n.classList.remove('over'));
const tgt = under.find(n => n.classList && (n.classList.contains('drop-box') || n.classList.contains('dnd-pool')));
if(tgt) tgt.classList.add('over');
}
}
function onUp(e){
el.removeEventListener('pointermove', onMove);
el.removeEventListener('pointerup', onUp);
el.removeEventListener('pointercancel', onUp);
if(captured){ try { el.releasePointerCapture(ev.pointerId); } catch(_){} }
el.classList.remove('dragging');
if(ghost){ ghost.remove(); ghost = null; }
scope.querySelectorAll('.drop-box.over, .dnd-pool.over').forEach(n => n.classList.remove('over'));
if(dragging){
const under = document.elementsFromPoint(e.clientX, e.clientY);
const box = under.find(n => n.classList && n.classList.contains('drop-box'));
const pl = under.find(n => n.classList && n.classList.contains('dnd-pool'));
if(box){
const di = box.querySelector('[data-cat]');
if(di){ placed[itId] = di.dataset.cat; armed = null; render(); return; }
} else if(pl){ delete placed[itId]; armed = null; render(); return; }
// fell outside — revert
} else {
// tap fallback
if(placed[itId]){ delete placed[itId]; armed = null; render(); }
else { armed = (armed === itId) ? null : itId; render(); }
}
dragging = false;
}
el.addEventListener('pointermove', onMove);
el.addEventListener('pointerup', onUp);
el.addEventListener('pointercancel', onUp);
});
}
function attachBoxTaps(){
scope.querySelectorAll('.drop-box').forEach(box => {
box.addEventListener('click', ev => {
if(!armed) return;
if(ev.target.closest('.dnd-chip')) return;
const di = box.querySelector('[data-cat]');
if(di){ placed[armed] = di.dataset.cat; armed = null; render(); }
});
});
pool.addEventListener('click', ev => {
if(!armed) return;
if(ev.target.closest('.dnd-chip')) return;
// empty pool click also de-arms
armed = null; render();
});
}
function render(){
pool.innerHTML = '';
cfg.items.forEach(it => {
if(placed[it.id]) return;
const chip = buildChip(it, false);
if(armed === it.id) chip.classList.add('armed');
pool.appendChild(chip);
});
cfg.cats.forEach(cat => {
const box = scope.querySelector('.drop-items[data-cat="' + cat + '"]');
if(!box) return;
box.innerHTML = '';
cfg.items.forEach(it => {
if(placed[it.id] !== cat) return;
box.appendChild(buildChip(it, true));
});
});
if(window.renderMathInElement) try { renderMath(scope); } catch(_){}
}
attachBoxTaps();
render();
return { placed, render, reset(){ for(const k in placed) delete placed[k]; armed = null; render(); } };
}
const DND_HINT_HTML = '<div class="dnd-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11V6a3 3 0 0 1 6 0v5"/><path d="M9 11h6v8a4 4 0 0 1-8 0z"/></svg> Перетащите карточку или нажмите её, затем — на нужный ящик.</div>';
/* ============================================================
GLOSSARY — term → definition map, hover-tooltip on .gloss-term
============================================================ */
const GLOSSARY = [
{ term:'квадратное уравнение', def:'Уравнение вида $ax^2+bx+c=0$ с $a \\neq 0$.', sec:'p7', aliases:['квадратное уравнение','квадратного уравнения','квадратные уравнения','квадратных уравнений'] },
{ term:'неполное квадратное уравнение', def:'Уравнение $ax^2+bx+c=0$, у которого $b=0$ или $c=0$ (или оба).', sec:'p7', aliases:['неполное квадратное','неполные квадратные','неполное уравнение','неполного уравнения','неполных уравнений'] },
{ term:'приведённое уравнение', def:'Квадратное уравнение со старшим коэффициентом 1: $x^2+px+q=0$.', sec:'p9', aliases:['приведённое','приведённого','приведённые','приведённых'] },
{ term:'дискриминант', def:'$D = b^2 - 4ac$. По знаку $D$ — число корней: $D>0$ — два, $D=0$ — один, $D<0$ — нет.', sec:'p8', aliases:['дискриминант','дискриминанта','дискриминантом','дискриминанте'] },
{ term:'теорема Виета', def:'Для $x^2+px+q=0$: $x_1+x_2=-p,\\ x_1 x_2 = q$. Для $ax^2+bx+c=0$: $-b/a,\\ c/a$.', sec:'p9', aliases:['теорема Виета','теоремы Виета','теореме Виета','Виета'] },
{ term:'корень уравнения', def:'Значение переменной, при котором уравнение становится верным равенством.', sec:'p7', aliases:['корень уравнения','корни уравнения','корня уравнения'] },
{ term:'квадратный трёхчлен', def:'Многочлен $ax^2+bx+c$ при $a \\neq 0$.', sec:'p10', aliases:['квадратный трёхчлен','квадратного трёхчлена','квадратные трёхчлены','квадратных трёхчленов'] },
{ term:'разложение на множители', def:'$ax^2+bx+c = a(x-x_1)(x-x_2)$ при $D \\geq 0$.', sec:'p10', aliases:['разложение на множители','разложения на множители','разложить на множители'] },
{ term:'биквадратное уравнение', def:'Уравнение $ax^4+bx^2+c=0$. Решается заменой $t=x^2,\\ t \\geq 0$.', sec:'p12', aliases:['биквадратное','биквадратных','биквадратные','биквадратного'] },
{ term:'ОДЗ', def:'Область допустимых значений. Для дробей — знаменатель $\\neq 0$.', sec:'p12', aliases:['ОДЗ','область допустимых значений'] },
{ term:'посторонний корень', def:'Корень, полученный при решении, но не удовлетворяющий ОДЗ исходного уравнения.', sec:'p12', aliases:['посторонний корень','посторонние корни','постороннего корня'] },
{ term:'свободный член', def:'Коэффициент $c$ в $ax^2+bx+c$. Член без переменной.', sec:'p7', aliases:['свободный член','свободного члена'] },
{ term:'старший коэффициент', def:'Коэффициент $a$ при $x^2$ в квадратном уравнении.', sec:'p7', aliases:['старший коэффициент','старшего коэффициента'] },
];
function wrapGlossary(root){
if(!root || root.__glossDone) return;
const allAliases = [];
GLOSSARY.forEach((g, i) => g.aliases.forEach(a => allAliases.push({ a, i })));
allAliases.sort((x, y) => y.a.length - x.a.length);
const re = new RegExp('(?<![\\w-])(' + allAliases.map(x => x.a.replace(/[.*+?^${}()|[\\]\\\\]/g,'\\$&')).join('|') + ')(?![\\w-])', 'iu');
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node){
const p = node.parentElement;
if(!p) return NodeFilter.FILTER_REJECT;
if(p.closest('.katex, .gloss-term, button, input, select, .wg-badge, .card-icon, .sec-num, .psel-num, .hdr, .ach-popup, script, style, .search-modal, .sidecard, .gloss-tip')) return NodeFilter.FILTER_REJECT;
if(!re.test(node.nodeValue)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
const nodes = [];
let n; while((n = walker.nextNode())) nodes.push(n);
nodes.forEach(node => {
const text = node.nodeValue;
const out = document.createDocumentFragment();
let cursor = 0;
const global = new RegExp(re.source, 'giu');
let m;
while((m = global.exec(text)) !== null){
if(m.index > cursor) out.appendChild(document.createTextNode(text.slice(cursor, m.index)));
const found = m[0].toLowerCase();
const hit = allAliases.find(x => x.a.toLowerCase() === found);
const g = hit ? GLOSSARY[hit.i] : null;
const sp = document.createElement('span');
sp.className = 'gloss-term';
sp.dataset.gloss = g ? g.term : '';
sp.textContent = m[0];
out.appendChild(sp);
cursor = m.index + m[0].length;
}
if(cursor < text.length) out.appendChild(document.createTextNode(text.slice(cursor)));
node.parentNode.replaceChild(out, node);
});
root.__glossDone = true;
}
function initGlossaryTip(){
const tip = document.getElementById('gloss-tip');
if(!tip) return;
let lockOpen = null;
function show(el){
const g = GLOSSARY.find(x => x.term === el.dataset.gloss);
if(!g) return;
tip.innerHTML = '<b>' + g.term[0].toUpperCase() + g.term.slice(1) + '</b><div style="margin-top:4px">' + g.def + '</div><div style="margin-top:6px;font-size:.72rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em">См. §&nbsp;' + g.sec.replace('p','') + '</div>';
if(window.renderMathInElement) renderMath(tip);
const r = el.getBoundingClientRect();
tip.classList.add('show');
const tw = tip.offsetWidth, th = tip.offsetHeight;
let left = r.left, top = r.bottom + 8;
if(left + tw > window.innerWidth - 12) left = window.innerWidth - tw - 12;
if(top + th > window.innerHeight - 12) top = r.top - th - 8;
tip.style.left = Math.max(8, left) + 'px';
tip.style.top = Math.max(8, top) + 'px';
}
function hide(){ tip.classList.remove('show'); }
document.addEventListener('mouseover', e => {
const el = e.target.closest && e.target.closest('.gloss-term');
if(el && !lockOpen) show(el);
});
document.addEventListener('mouseout', e => {
const el = e.target.closest && e.target.closest('.gloss-term');
if(el && !lockOpen) hide();
});
document.addEventListener('click', e => {
const el = e.target.closest && e.target.closest('.gloss-term');
if(el){
if(lockOpen === el){ lockOpen = null; hide(); }
else { lockOpen = el; show(el); }
} else if(lockOpen && !e.target.closest('.gloss-tip')){
lockOpen = null; hide();
}
});
}
/* ============================================================
SEARCH — Ctrl+K modal across glossary + sections + formulas
============================================================ */
const SEARCH_INDEX = (function(){
const arr = [];
PARAS.forEach(p => arr.push({ kind:'Параграф', title:p.num + ' ' + p.name, desc:p.sub || '', sec:p.id }));
GLOSSARY.forEach(g => arr.push({ kind:'Понятие', title:g.term, desc:g.def.replace(/\$/g,''), sec:g.sec, gloss:g.term }));
[
['Формула','D = b² 4ac','§8 — дискриминант','p8'],
['Формула','x = (b ± √D) / 2a','§8 — корни через дискриминант','p8'],
['Формула','x₁ + x₂ = p, x₁·x₂ = q','§9 — теорема Виета','p9'],
['Формула','ax² + bx + c = a(x x₁)(x x₂)','§10 — разложение трёхчлена','p10'],
['Формула','t = x², t ≥ 0','§12 — замена для биквадратного','p12'],
].forEach(([k,t,d,s]) => arr.push({ kind:k, title:t, desc:d, sec:s }));
arr.push({ kind:'Финал', title:'Боссы главы', desc:'7 проверочных боссов', sec:'final2' });
return arr;
})();
function initSearch(){
const modal = document.getElementById('search-modal');
const inp = document.getElementById('search-input');
const out = document.getElementById('search-results');
const btn = document.getElementById('search-btn');
if(!modal || !inp || !out) return;
let cur = 0, rows = [];
function rank(q){
q = q.trim().toLowerCase();
if(!q) return SEARCH_INDEX.slice(0, 12);
return SEARCH_INDEX
.map(it => ({ it, s: score(q, it) }))
.filter(x => x.s > 0)
.sort((a,b) => b.s - a.s)
.slice(0, 20)
.map(x => x.it);
}
function score(q, it){
const t = (it.title + ' ' + it.desc).toLowerCase();
if(t.includes(q)) return 100 + (it.title.toLowerCase().startsWith(q) ? 50 : 0);
let s = 0;
q.split(/\s+/).forEach(w => { if(w && t.includes(w)) s += 10; });
return s;
}
function render(){
cur = 0;
if(!rows.length){ out.innerHTML = '<div class="search-empty">Ничего не найдено</div>'; return; }
out.innerHTML = rows.map((r, i) => `<button class="search-row${i === 0 ? ' active' : ''}" data-i="${i}"><div class="sr-kind">${r.kind}</div><div class="sr-title">${r.title}</div>${r.desc ? `<div class="sr-desc">${r.desc.length > 90 ? r.desc.slice(0, 90) + '…' : r.desc}</div>` : ''}</button>`).join('');
out.querySelectorAll('.search-row').forEach(b => b.addEventListener('click', ()=>{ cur = +b.dataset.i; pick(); }));
}
function pick(){
const r = rows[cur]; if(!r) return;
close();
goTo(r.sec);
if(r.gloss){
setTimeout(()=>{
const sec = document.getElementById('sec-' + r.sec);
const el = sec && sec.querySelector('[data-gloss="' + r.gloss + '"]');
if(el){ el.scrollIntoView({ behavior:'smooth', block:'center' }); el.style.transition = 'background .3s'; el.style.background = 'var(--warn,#f59e0b)'; setTimeout(()=>{ el.style.background = ''; }, 1400); }
}, 400);
}
}
function move(d){
const items = out.querySelectorAll('.search-row'); if(!items.length) return;
items[cur] && items[cur].classList.remove('active');
cur = (cur + d + items.length) % items.length;
items[cur].classList.add('active');
items[cur].scrollIntoView({ block:'nearest' });
}
function open(){ modal.classList.add('show'); inp.value = ''; rows = rank(''); render(); setTimeout(()=>inp.focus(), 50); }
function close(){ modal.classList.remove('show'); }
btn && btn.addEventListener('click', open);
modal.addEventListener('click', e => { if(e.target === modal) close(); });
inp.addEventListener('input', ()=>{ rows = rank(inp.value); render(); });
inp.addEventListener('keydown', e => {
if(e.key === 'ArrowDown'){ e.preventDefault(); move(1); }
else if(e.key === 'ArrowUp'){ e.preventDefault(); move(-1); }
else if(e.key === 'Enter'){ e.preventDefault(); pick(); }
else if(e.key === 'Escape'){ e.preventDefault(); close(); }
});
document.addEventListener('keydown', e => {
if((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')){
e.preventDefault();
if(modal.classList.contains('show')) close(); else open();
}
});
}
/* INIT */
function initSidebarToggle(){
const side = document.getElementById('col-side');
const back = document.getElementById('col-side-backdrop');
const btn = document.getElementById('sidebar-btn');
if(!side || !btn) return;
function open(){ side.classList.add('open'); back.classList.add('show'); }
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
btn.addEventListener('click', ()=>{
if(side.classList.contains('open')) close();
else open();
});
back.addEventListener('click', close);
document.addEventListener('keydown', e=>{ if(e.key === 'Escape') close(); });
}
function init(){
loadProgress();
initTheme();
initSidebarToggle();
initGlossaryTip();
initSearch();
buildParaSelector();
refreshProgressUI();
loadServerReadState();
goTo('p7');
setTimeout(()=>achievement('start','Начало главы 2!'), 600);
// Sync XP с сервером: если серверный XP выше — обновляем локальный прогресс
if(window.LS && window.LS.xp){
window.LS.xp.load().then(function(s){
if(s && s.xp > STATE.xp){
STATE.xp = s.xp;
STATE.level = calcLevel(STATE.xp);
saveProgress();
refreshProgressUI();
if(STATE.current) buildSidebar(STATE.current);
}
});
}
}
document.addEventListener('DOMContentLoaded', init);
/* STUBS for paragraphs not yet implemented */
function buildP9stub(){ buildP9(); }
function buildP9(){
const box = document.getElementById('p9-body');
let html = '';
html += makeCard('repeat','Повторение',null,`
<ul style="margin-left:18px;line-height:1.7">
<li><b>Приведённое</b> уравнение: $x^2 + px + q = 0$ — старший коэффициент равен $1$.</li>
<li>Полное: $ax^2 + bx + c = 0$ &rarr; делим на $a$, получим $x^2 + \\dfrac{b}{a}x + \\dfrac{c}{a} = 0$.</li>
<li>Формулы корней через дискриминант — из § 8.</li>
</ul>`);
html += makeCard('theory','Теорема Виета','9.1',`
<p>Пусть $x_1$ и $x_2$ — корни приведённого уравнения $x^2 + px + q = 0$. Тогда:</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:14px;margin:10px 0;text-align:center;font-size:1.15rem">$$x_1 + x_2 = -p,\\qquad x_1 \\cdot x_2 = q$$</div>
<p><b>Обратная теорема.</b> Если для чисел $x_1, x_2$ выполнено $x_1+x_2=-p$ и $x_1 x_2=q$, то они — корни $x^2+px+q=0$.</p>`);
html += makeCard('rule','Общий случай (a ≠ 1)','9.2',`
<p>Для уравнения $ax^2 + bx + c = 0$:</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:14px;margin:10px 0;text-align:center">$$x_1 + x_2 = -\\dfrac{b}{a},\\qquad x_1 \\cdot x_2 = \\dfrac{c}{a}$$</div>
<p><b>Анализ знаков</b> (если корни вещественные):</p>
<ul style="margin-left:18px;line-height:1.7">
<li>$q > 0$ &rarr; корни одного знака; если ещё $-p > 0$ — оба положительные.</li>
<li>$q < 0$ &rarr; корни разных знаков.</li>
<li>$q = 0$ &rarr; один из корней равен нулю.</li>
</ul>`);
html += makeCard('example','Пример',null,`
<p><b>Подбор:</b> $x^2 - 7x + 12 = 0$. Ищем $x_1, x_2$: сумма $7$, произведение $12$. Это $3$ и $4$. Ответ: $\\{3;\\ 4\\}$.</p>
<p style="margin-top:6px"><b>Составление:</b> найти уравнение с корнями $-2$ и $5$. Сумма $= 3$, произведение $= -10$. Уравнение: $x^2 - 3x - 10 = 0$.</p>`);
/* ===== INTERACTIVE 1: Подбор корней ===== */
html += widget('Тренажёр Виета: устный подбор','INTERACT 1','Дано приведённое уравнение. Подберите корни в уме и введите через точку с запятой.',`
<div class="score-display"><span>Задача <b id="p9v-i">1</b> / 10</span><span>Очки: <b id="p9v-score">0</b></span></div>
<div id="p9v-task" style="font-size:1.4rem;text-align:center;padding:18px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
<input type="text" id="p9v-inp" placeholder="например: 3; 4" style="width:200px;padding:8px;border:1.5px solid var(--border);border-radius:8px">
<button class="btn primary" id="p9v-go">Ответ</button>
<button class="btn" id="p9v-skip">Пропустить</button>
</div>
<div class="feedback" id="p9v-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p9v-start" style="margin-top:10px">Начать</button>`);
/* ===== INTERACTIVE 2: Составить уравнение ===== */
html += widget('Конструктор: корни → уравнение','INTERACT 2','Выберите корни — система соберёт приведённое уравнение по Виета.',`
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;justify-content:center;margin-bottom:10px">
<label>$x_1$ = <input type="number" id="p9c-r1" value="2" style="width:80px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>$x_2$ = <input type="number" id="p9c-r2" value="-3" style="width:80px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
</div>
<div id="p9c-out" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;line-height:1.8"></div>`);
/* ===== INTERACTIVE 3: Знаки корней ===== */
html += widget('Знаки корней по Виета','INTERACT 3','По уравнению определите знаки корней (если они существуют).',`
<div class="score-display"><span>Раунд <b id="p9z-i">1</b> / 8</span><span>Правильно: <b id="p9z-score">0</b></span></div>
<div id="p9z-task" style="font-size:1.3rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:center">
<button class="btn" data-z="pp">Оба «+»</button>
<button class="btn" data-z="nn">Оба «−»</button>
<button class="btn" data-z="pn">Разных знаков</button>
<button class="btn" data-z="none">Корней нет</button>
</div>
<div class="feedback" id="p9z-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p9z-start" style="margin-top:10px">Начать</button>`);
/* ===== INTERACTIVE 4: Проверка корней по Виета ===== */
html += widget('Быстрая проверка корней','INTERACT 4','Подставьте найденные корни — проверьте через Виета мгновенно.',`
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px">
<label>Уравнение: $x^2$ + <input type="number" id="p9p-p" value="-5" style="width:60px;padding:5px;border:1.5px solid var(--border);border-radius:6px">$x$ + <input type="number" id="p9p-q" value="6" style="width:60px;padding:5px;border:1.5px solid var(--border);border-radius:6px"> = 0</label>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
<label>Ваши $x_1, x_2$: <input type="number" id="p9p-r1" value="2" style="width:70px;padding:5px;border:1.5px solid var(--border);border-radius:6px">, <input type="number" id="p9p-r2" value="3" style="width:70px;padding:5px;border:1.5px solid var(--border);border-radius:6px"></label>
<button class="btn primary" id="p9p-check">Проверить</button>
</div>
<div id="p9p-out" style="margin-top:10px;padding:10px 14px;border-radius:8px"></div>`);
/* ===== INTERACTIVE 5: Виета для непривед. ===== */
html += widget('Виета для $a \\neq 1$','INTERACT 5','Уравнение не приведённое. Введите сумму и произведение корней.',`
<div id="p9n-task" style="font-size:1.2rem;text-align:center;padding:14px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;justify-content:center">
<label>$x_1+x_2$ = <input type="text" id="p9n-s" style="width:80px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>$x_1 x_2$ = <input type="text" id="p9n-p" style="width:80px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<button class="btn primary" id="p9n-go">Проверить</button>
<button class="btn" id="p9n-next">Следующее</button>
</div>
<div class="feedback" id="p9n-fb" style="display:none;margin-top:10px"></div>`);
html += makeCard('oral','Устные вопросы',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Сформулируйте теорему Виета для приведённого уравнения.</li>
<li>Что говорит обратная теорема?</li>
<li>Корни уравнения $x^2 - 9x + 20 = 0$ — целые? Найдите устно.</li>
<li>Какого знака произведение корней уравнения $x^2 + 3x - 10 = 0$?</li>
</ol>`);
html += makeCard('class','Класс — решите по Виета',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$x^2 - 11x + 28 = 0$</li>
<li>$x^2 + x - 12 = 0$</li>
<li>$x^2 + 10x + 21 = 0$</li>
<li>Составьте приведённое уравнение с корнями $-4$ и $7$.</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$x^2 - 8x + 15 = 0$</li>
<li>$x^2 + 2x - 35 = 0$</li>
<li>Составьте уравнение с корнями $\\dfrac{1}{2}$ и $-3$.</li>
<li>Найдите $p$, если корни $x^2 + px - 6 = 0$ — это $2$ и $-3$.</li>
</ol>`);
html += secNav('p8','p10');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Тренажёр Виета */
(function(){
let cur = null, i = 1, score = 0;
function gen(){
const r1 = -7 + Math.floor(Math.random()*15);
const r2 = -7 + Math.floor(Math.random()*15);
if(r1 === r2) return gen();
const p = -(r1 + r2), q = r1*r2;
return { r1, r2, p, q };
}
function show(){
cur = gen();
document.getElementById('p9v-i').textContent = i;
const t = 'x^2 ' + (cur.p >= 0 ? '+ ' + cur.p : '- ' + Math.abs(cur.p)) + 'x ' + (cur.q >= 0 ? '+ ' + cur.q : '- ' + Math.abs(cur.q)) + ' = 0';
document.getElementById('p9v-task').innerHTML = '$' + t + '$'; renderMath(document.getElementById('p9v-task'));
document.getElementById('p9v-inp').value = '';
document.getElementById('p9v-fb').style.display = 'none';
}
function check(){
const fb = document.getElementById('p9v-fb');
fb.style.display = 'block';
const u = document.getElementById('p9v-inp').value.replace(/,/g,';').split(/[;\s]+/).filter(Boolean).map(Number).sort((a,b)=>a-b);
const a = [cur.r1, cur.r2].sort((a,b)=>a-b);
const ok = u.length === 2 && u[0] === a[0] && u[1] === a[1];
if(ok){ score++; feedback(fb, true, '&#10003; Верно: ' + a.join(' и ')); }
else feedback(fb, false, 'Не то. Правильно: ' + a.join(' и '));
document.getElementById('p9v-score').textContent = score;
if(i >= 10){ setTimeout(()=>{ feedback(fb, score >= 7, 'Итог: ' + score + '/10'); if(score >= 7){ achievement('p9_vieta'); bumpProgress('p9', 16); confetti(); } }, 600); }
else { i++; setTimeout(show, 800); }
}
document.getElementById('p9v-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p9v-score').textContent = 0; show(); });
document.getElementById('p9v-go').addEventListener('click', check);
document.getElementById('p9v-inp').addEventListener('keyup', e=>{ if(e.key === 'Enter') check(); });
document.getElementById('p9v-skip').addEventListener('click', ()=>{ if(i < 10){ i++; show(); } });
})();
/* INIT 2 — Конструктор уравнения */
(function(){
const r1E = document.getElementById('p9c-r1'), r2E = document.getElementById('p9c-r2');
const out = document.getElementById('p9c-out');
let done = false;
function refresh(){
const r1 = +r1E.value, r2 = +r2E.value;
const p = -(r1 + r2), q = r1 * r2;
let s = '<div><b>Сумма:</b> $x_1 + x_2 = ' + r1 + ' + (' + r2 + ') = ' + (r1+r2) + ' \\Rightarrow -p = ' + (r1+r2) + ' \\Rightarrow p = ' + p + '$</div>';
s += '<div><b>Произведение:</b> $x_1 \\cdot x_2 = ' + r1 + ' \\cdot (' + r2 + ') = ' + q + ' \\Rightarrow q = ' + q + '$</div>';
s += '<div style="margin-top:8px;font-size:1.15rem"><b>Уравнение:</b> $x^2 ' + (p >= 0 ? '+ ' + p : '- ' + Math.abs(p)) + 'x ' + (q >= 0 ? '+ ' + q : '- ' + Math.abs(q)) + ' = 0$</div>';
out.innerHTML = s; renderMath(out);
if(!done){ done = true; setTimeout(()=>{ achievement('p9_constr'); bumpProgress('p9', 14); }, 300); }
}
r1E.addEventListener('input', refresh); r2E.addEventListener('input', refresh);
refresh();
})();
/* INIT 3 — Знаки корней */
(function(){
let cur = null, i = 1, score = 0;
function gen(){
const t = Math.floor(Math.random()*4);
if(t === 0){
const r1 = 1 + Math.floor(Math.random()*5), r2 = 1 + Math.floor(Math.random()*5);
return { p: -(r1+r2), q: r1*r2, kind: 'pp' };
}
if(t === 1){
const r1 = -1 - Math.floor(Math.random()*5), r2 = -1 - Math.floor(Math.random()*5);
return { p: -(r1+r2), q: r1*r2, kind: 'nn' };
}
if(t === 2){
const r1 = 1 + Math.floor(Math.random()*5), r2 = -1 - Math.floor(Math.random()*5);
return { p: -(r1+r2), q: r1*r2, kind: 'pn' };
}
const p = -5 + Math.floor(Math.random()*11), q = 4 + Math.floor(Math.random()*8);
const D = p*p - 4*q;
if(D >= 0) return gen();
return { p, q, kind:'none' };
}
function show(){
cur = gen();
document.getElementById('p9z-i').textContent = i;
const t = 'x^2 ' + (cur.p >= 0 ? '+ ' + cur.p : '- ' + Math.abs(cur.p)) + 'x ' + (cur.q >= 0 ? '+ ' + cur.q : '- ' + Math.abs(cur.q)) + ' = 0';
document.getElementById('p9z-task').innerHTML = '$' + t + '$'; renderMath(document.getElementById('p9z-task'));
document.getElementById('p9z-fb').style.display = 'none';
}
function ans(k){
const fb = document.getElementById('p9z-fb');
fb.style.display = 'block';
if(k === cur.kind){ score++; feedback(fb, true, '&#10003; Верно'); }
else feedback(fb, false, 'Не то. Правильно: ' + ({pp:'оба «+»',nn:'оба «−»',pn:'разных знаков',none:'корней нет'})[cur.kind]);
document.getElementById('p9z-score').textContent = score;
if(i >= 8){ setTimeout(()=>{ feedback(fb, score >= 6, 'Итог: ' + score + '/8'); if(score >= 6){ achievement('p9_signs'); bumpProgress('p9', 14); confetti(); } }, 600); }
else { i++; setTimeout(show, 800); }
}
document.getElementById('p9z-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p9z-score').textContent = 0; show(); });
document.querySelectorAll('[data-z]').forEach(b => b.addEventListener('click', ()=>ans(b.dataset.z)));
})();
/* INIT 4 — Проверка корней */
(function(){
document.getElementById('p9p-check').addEventListener('click', ()=>{
const p = +document.getElementById('p9p-p').value, q = +document.getElementById('p9p-q').value;
const r1 = +document.getElementById('p9p-r1').value, r2 = +document.getElementById('p9p-r2').value;
const sum = r1 + r2, prod = r1 * r2;
const out = document.getElementById('p9p-out');
const okS = sum === -p, okP = prod === q;
let html = '<div>$x_1 + x_2 = ' + sum + '$ vs $-p = ' + (-p) + '$ &mdash; ' + (okS ? '<span style="color:var(--ok)">&#10003;</span>' : '<span style="color:var(--bad)">&#10007;</span>') + '</div>';
html += '<div>$x_1 \\cdot x_2 = ' + prod + '$ vs $q = ' + q + '$ &mdash; ' + (okP ? '<span style="color:var(--ok)">&#10003;</span>' : '<span style="color:var(--bad)">&#10007;</span>') + '</div>';
if(okS && okP){ html += '<div style="margin-top:8px;color:var(--ok);font-weight:700">Корни верные!</div>'; achievement('p9_check'); bumpProgress('p9', 12); }
else html += '<div style="margin-top:8px;color:var(--bad);font-weight:700">Корни ошибочны.</div>';
out.style.background = (okS && okP) ? 'var(--ok-bg)' : 'var(--fail-bg)';
out.innerHTML = html; renderMath(out);
});
})();
/* INIT 5 — Виета для непривед. */
(function(){
let cur = null;
function gen(){
const a = 2 + Math.floor(Math.random()*3);
const r1 = -3 + Math.floor(Math.random()*7), r2 = -3 + Math.floor(Math.random()*7);
if(r1 === r2) return gen();
const b = -a*(r1+r2), c = a*r1*r2;
return { a, b, c, sum:-b/a, prod:c/a };
}
function show(){
cur = gen();
document.getElementById('p9n-task').innerHTML = '$' + cur.a + 'x^2 ' + (cur.b >= 0 ? '+ ' + cur.b : '- ' + Math.abs(cur.b)) + 'x ' + (cur.c >= 0 ? '+ ' + cur.c : '- ' + Math.abs(cur.c)) + ' = 0$';
renderMath(document.getElementById('p9n-task'));
document.getElementById('p9n-s').value = '';
document.getElementById('p9n-p').value = '';
document.getElementById('p9n-fb').style.display = 'none';
}
function parse(s){ s = s.trim().replace(/,/g,'.'); if(s.includes('/')){ const [a,b] = s.split('/').map(Number); return a/b; } return +s; }
document.getElementById('p9n-go').addEventListener('click', ()=>{
const us = parse(document.getElementById('p9n-s').value), up = parse(document.getElementById('p9n-p').value);
const fb = document.getElementById('p9n-fb'); fb.style.display = 'block';
const okS = Math.abs(us - cur.sum) < 1e-6, okP = Math.abs(up - cur.prod) < 1e-6;
if(okS && okP){ feedback(fb, true, '&#10003; Верно! $\\dfrac{-b}{a}=' + fmt(cur.sum) + ',\\ \\dfrac{c}{a}=' + fmt(cur.prod) + '$'); renderMath(fb); achievement('p9_nonpriv'); bumpProgress('p9', 14); }
else feedback(fb, false, 'Правильно: сумма = ' + fmt(cur.sum) + ', произведение = ' + fmt(cur.prod));
});
document.getElementById('p9n-next').addEventListener('click', show);
show();
})();
}
function buildP10stub(){ buildP10(); }
function buildP10(){
const box = document.getElementById('p10-body');
let html = '';
html += makeCard('repeat','Повторение',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Дискриминант $D = b^2 - 4ac$. Корни через формулу или Виета.</li>
<li><b>Формулы сокр. умножения:</b> $(a-b)(a+b) = a^2 - b^2$, $a^2 \\pm 2ab + b^2 = (a \\pm b)^2$.</li>
<li>Сокращение дробей: сокращаются равные множители числителя и знаменателя.</li>
</ul>`);
html += makeCard('theory','Что такое квадратный трёхчлен','10.1',`
<p><b>Квадратный трёхчлен</b> — это многочлен $ax^2 + bx + c$, где $a \\neq 0$. Числа $x_1, x_2$, при которых он равен нулю, называются <b>корнями</b> трёхчлена.</p>
<p style="margin-top:6px">Если $D \\geq 0$, трёхчлен раскладывается:</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:14px;margin:10px 0;text-align:center;font-size:1.15rem">$$ax^2 + bx + c = a(x - x_1)(x - x_2)$$</div>
<p>Если $D < 0$ — на множители первой степени с действительными коэффициентами не раскладывается.</p>`);
html += makeCard('algo','Алгоритм разложения',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Найти $D$ и корни $x_1, x_2$.</li>
<li>Если $D \\geq 0$, записать $a(x - x_1)(x - x_2)$.</li>
<li>Раскрыть скобки и проверить.</li>
</ol>`);
html += makeCard('example','Примеры',null,`
<p><b>1)</b> $x^2 - 5x + 6 = (x-2)(x-3)$.</p>
<p><b>2)</b> $2x^2 - 7x + 3$: $D = 49 - 24 = 25$, $x_1 = 3$, $x_2 = 0{,}5$. Разложение: $2(x-3)(x-0{,}5) = (x-3)(2x-1)$.</p>
<p><b>3)</b> $x^2 + 1$: $D = -4 < 0$ &rarr; не раскладывается.</p>
<p><b>4)</b> Сокращение: $\\dfrac{x^2 - 9}{x^2 - 5x + 6} = \\dfrac{(x-3)(x+3)}{(x-3)(x-2)} = \\dfrac{x+3}{x-2}$.</p>`);
/* INT 1 — Конструктор разложения */
html += widget('Конструктор разложения','INTERACT 1','Введите $a$, $x_1$, $x_2$ — получите трёхчлен и его разложение.',`
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;justify-content:center;margin-bottom:10px">
<label>$a$ = <input type="number" id="p10c-a" value="1" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>$x_1$ = <input type="number" id="p10c-x1" value="2" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>$x_2$ = <input type="number" id="p10c-x2" value="-3" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
</div>
<div id="p10c-out" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;line-height:1.8"></div>`);
/* INT 2 — Шаговый разлагатель */
html += widget('Пошаговый разлагатель','INTERACT 2','Введите $a$, $b$, $c$ и нажимайте «Дальше» — каждый шаг отдельно.',`
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:10px">
<label>$a$ = <input type="number" id="p10s-a" value="1" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>$b$ = <input type="number" id="p10s-b" value="-7" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>$c$ = <input type="number" id="p10s-c" value="12" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<button class="btn primary" id="p10s-go">Старт</button>
<button class="btn" id="p10s-next" style="display:none">Дальше</button>
<button class="btn" id="p10s-reset" style="display:none">Сначала</button>
</div>
<div id="p10s-stage" style="padding:14px;background:var(--card-soft);border-radius:10px;min-height:80px"></div>`);
/* INT 3 — Тренажёр разложения */
html += widget('Тренажёр разложения','INTERACT 3','Введите разложение в формате $(x-?)(x-?)$ — корни через точку с запятой, со знаками.',`
<div class="score-display"><span>Задача <b id="p10t-i">1</b> / 8</span><span>Очки: <b id="p10t-score">0</b></span></div>
<div id="p10t-task" style="font-size:1.3rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
<input type="text" id="p10t-inp" placeholder="корни: например 2; -3" style="width:200px;padding:8px;border:1.5px solid var(--border);border-radius:8px">
<button class="btn primary" id="p10t-go">Ответ</button>
</div>
<div class="feedback" id="p10t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p10t-start" style="margin-top:10px">Начать</button>`);
/* INT 4 — Сокращение дробей */
html += widget('Сокращение дробей','INTERACT 4','Дробь содержит квадратные трёхчлены. Разложите устно и введите сокращённую форму.',`
<div class="score-display"><span>Задача <b id="p10f-i">1</b> / 5</span><span>Очки: <b id="p10f-score">0</b></span></div>
<div id="p10f-task" style="font-size:1.2rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div id="p10f-opts" style="display:flex;flex-direction:column;gap:6px"></div>
<div class="feedback" id="p10f-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p10f-start" style="margin-top:10px">Начать</button>`);
/* INT 5 — Drag: разложимо или нет */
html += widget('Разложимо или нет?','INTERACT 5','По знаку дискриминанта разнесите трёхчлены в нужные ящики.',`
${DND_HINT_HTML}
<div id="p10z-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="drop-box"><h5>Раскладывается ($D \\geq 0$)</h5><div class="drop-items" data-cat="yes"></div></div>
<div class="drop-box"><h5>Не раскладывается ($D < 0$)</h5><div class="drop-items" data-cat="no"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p10z-check">Проверить</button><button class="btn" id="p10z-reset">Сначала</button></div>
<div class="feedback" id="p10z-fb" style="display:none"></div>`);
html += makeCard('oral','Устно',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>При каком условии трёхчлен раскладывается?</li>
<li>Запишите разложение $x^2 - 4x + 3$.</li>
<li>Разложим ли $x^2 + 9$?</li>
</ol>`);
html += makeCard('class','Класс — разложите',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$x^2 - 8x + 15$</li>
<li>$2x^2 + 5x - 3$</li>
<li>$x^2 + 6x + 9$</li>
<li>Сократите: $\\dfrac{x^2 - 4}{x^2 + x - 6}$.</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$x^2 - 9x + 20$</li>
<li>$3x^2 - 8x + 4$</li>
<li>$x^2 + 4x + 7$ — раскладывается ли?</li>
<li>Сократите: $\\dfrac{x^2 - 25}{x^2 - 7x + 10}$.</li>
</ol>`);
html += secNav('p9','p11');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 */
(function(){
const aE = document.getElementById('p10c-a'), x1E = document.getElementById('p10c-x1'), x2E = document.getElementById('p10c-x2');
const out = document.getElementById('p10c-out');
let done = false;
function refresh(){
const a = +aE.value, x1 = +x1E.value, x2 = +x2E.value;
if(!a){ out.innerHTML = 'Введите $a \\neq 0$'; renderMath(out); return; }
const b = -a*(x1+x2), c = a*x1*x2;
const eq = (a === 1 ? '' : (a === -1 ? '-' : a)) + 'x^2 ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + 'x ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c));
const fact = (a === 1 ? '' : (a === -1 ? '-' : a)) + '(x ' + (x1 >= 0 ? '- ' + x1 : '+ ' + Math.abs(x1)) + ')(x ' + (x2 >= 0 ? '- ' + x2 : '+ ' + Math.abs(x2)) + ')';
out.innerHTML = '<div><b>Трёхчлен:</b> $' + eq + '$</div><div><b>Разложение:</b> $' + fact + '$</div>';
renderMath(out);
if(!done){ done = true; setTimeout(()=>{ achievement('p10_constr'); bumpProgress('p10', 14); }, 300); }
}
[aE,x1E,x2E].forEach(e => e.addEventListener('input', refresh));
refresh();
})();
/* INIT 2 — пошаговый */
(function(){
const stage = document.getElementById('p10s-stage');
const goBtn = document.getElementById('p10s-go');
const nextBtn = document.getElementById('p10s-next');
const resetBtn = document.getElementById('p10s-reset');
let steps = [], idx = 0, awarded = false;
function build(a, b, c){
const D = b*b - 4*a*c;
const arr = [];
arr.push('<b>Шаг 1.</b> Исходный трёхчлен: $' + a + 'x^2 ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + 'x ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c)) + '$');
arr.push('<b>Шаг 2.</b> $D = b^2 - 4ac = ' + (b*b) + ' - ' + (4*a*c) + ' = ' + D + '$');
if(D < 0){
arr.push('<b>Шаг 3.</b> $D < 0$ — на множители первой степени с действительными коэффициентами не раскладывается.');
} else {
const x1 = (-b - Math.sqrt(D))/(2*a), x2 = (-b + Math.sqrt(D))/(2*a);
arr.push('<b>Шаг 3.</b> Корни: $x_1 = ' + fmt(x1) + ',\\ x_2 = ' + fmt(x2) + '$');
arr.push('<b>Шаг 4.</b> Подставляем в формулу $a(x-x_1)(x-x_2)$:');
arr.push('<b>Ответ:</b> $' + (a === 1 ? '' : a) + '(x ' + (x1 >= 0 ? '- ' + fmt(x1) : '+ ' + fmt(-x1)) + ')(x ' + (x2 >= 0 ? '- ' + fmt(x2) : '+ ' + fmt(-x2)) + ')$');
}
return arr;
}
function render(){
stage.innerHTML = steps.slice(0, idx + 1)
.map(s => `<div style="margin:8px 0;padding:10px 12px;background:var(--card);border-left:3px solid var(--sec-acc);border-radius:7px;animation:fadeIn .3s ease">${s}</div>`)
.join('');
renderMath(stage);
if(idx >= steps.length - 1){
nextBtn.disabled = true; nextBtn.textContent = 'Готово';
if(!awarded){ awarded = true; achievement('p10_steps'); bumpProgress('p10', 14); confetti(); }
} else {
nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')';
}
}
goBtn.addEventListener('click', ()=>{
const a = +document.getElementById('p10s-a').value, b = +document.getElementById('p10s-b').value, c = +document.getElementById('p10s-c').value;
if(!a){ stage.innerHTML = '<p>$a$ не может быть нулём.</p>'; renderMath(stage); return; }
steps = build(a, b, c); idx = 0; awarded = false;
goBtn.style.display = 'none'; nextBtn.style.display = ''; resetBtn.style.display = '';
render();
});
nextBtn.addEventListener('click', ()=>{ if(idx < steps.length - 1){ idx++; render(); } });
resetBtn.addEventListener('click', ()=>{
idx = 0; steps = []; awarded = false; stage.innerHTML = '';
goBtn.style.display = ''; nextBtn.style.display = 'none'; resetBtn.style.display = 'none';
});
})();
/* INIT 3 */
(function(){
let cur = null, i = 1, score = 0;
function gen(){
const r1 = -5 + Math.floor(Math.random()*11), r2 = -5 + Math.floor(Math.random()*11);
if(r1 === r2 || r1 === 0 && r2 === 0) return gen();
const b = -(r1+r2), c = r1*r2;
return { r1, r2, eq: 'x^2 ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + 'x ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c)) };
}
function show(){
cur = gen();
document.getElementById('p10t-i').textContent = i;
document.getElementById('p10t-task').innerHTML = '$' + cur.eq + '$';
renderMath(document.getElementById('p10t-task'));
document.getElementById('p10t-inp').value = '';
document.getElementById('p10t-fb').style.display = 'none';
}
function check(){
const fb = document.getElementById('p10t-fb');
fb.style.display = 'block';
const u = document.getElementById('p10t-inp').value.replace(/,/g,';').split(/[;\s]+/).filter(Boolean).map(Number).sort((a,b)=>a-b);
const a = [cur.r1, cur.r2].sort((a,b)=>a-b);
const ok = u.length === 2 && u[0] === a[0] && u[1] === a[1];
if(ok){ score++; feedback(fb, true, '&#10003; $(x ' + (cur.r1 >= 0 ? '- ' + cur.r1 : '+ ' + Math.abs(cur.r1)) + ')(x ' + (cur.r2 >= 0 ? '- ' + cur.r2 : '+ ' + Math.abs(cur.r2)) + ')$'); renderMath(fb); }
else feedback(fb, false, 'Правильно: ' + a.join(' и '));
document.getElementById('p10t-score').textContent = score;
if(i >= 8){ setTimeout(()=>{ feedback(fb, score >= 6, 'Итог: ' + score + '/8'); if(score >= 6){ achievement('p10_train'); bumpProgress('p10', 16); confetti(); } }, 600); }
else { i++; setTimeout(show, 800); }
}
document.getElementById('p10t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p10t-score').textContent = 0; show(); });
document.getElementById('p10t-go').addEventListener('click', check);
document.getElementById('p10t-inp').addEventListener('keyup', e=>{ if(e.key === 'Enter') check(); });
})();
/* INIT 4 — Сокращение */
(function(){
const tasks = [
{ n:'x^2 - 9', d:'x^2 - 5x + 6', ans:'\\dfrac{x+3}{x-2}', opts:['\\dfrac{x+3}{x-2}','\\dfrac{x-3}{x+2}','\\dfrac{x+3}{x+2}','\\dfrac{1}{x-2}'] },
{ n:'x^2 - 4', d:'x^2 + x - 6', ans:'\\dfrac{x-2}{x+3}', opts:['\\dfrac{x+2}{x+3}','\\dfrac{x-2}{x+3}','\\dfrac{x-2}{x-3}','\\dfrac{x+2}{x-3}'] },
{ n:'x^2 - 25', d:'x^2 - 7x + 10', ans:'\\dfrac{x+5}{x-2}', opts:['\\dfrac{x+5}{x-2}','\\dfrac{x-5}{x-2}','\\dfrac{x-5}{x+2}','\\dfrac{x+5}{x+2}'] },
{ n:'x^2 - 5x + 6', d:'x^2 - 4', ans:'\\dfrac{x-3}{x+2}', opts:['\\dfrac{x-3}{x+2}','\\dfrac{x+3}{x+2}','\\dfrac{x-3}{x-2}','\\dfrac{x+2}{x-3}'] },
{ n:'x^2 - 1', d:'x^2 + 3x - 4', ans:'\\dfrac{x+1}{x+4}', opts:['\\dfrac{x+1}{x+4}','\\dfrac{x-1}{x+4}','\\dfrac{x-1}{x-4}','\\dfrac{x+1}{x-4}'] },
];
let cur = null, i = 1, score = 0, shuffled = [];
function show(){
cur = shuffled[i-1];
document.getElementById('p10f-i').textContent = i;
document.getElementById('p10f-task').innerHTML = '$\\dfrac{' + cur.n + '}{' + cur.d + '} = \\ ?$';
renderMath(document.getElementById('p10f-task'));
const opts = document.getElementById('p10f-opts');
opts.innerHTML = '';
[...cur.opts].sort(()=>Math.random()-0.5).forEach(o=>{
const b = document.createElement('button');
b.className = 'btn';
b.innerHTML = '$' + o + '$';
b.style.cssText = 'text-align:left;padding:10px 14px';
b.addEventListener('click', ()=>{
const fb = document.getElementById('p10f-fb'); fb.style.display = 'block';
if(o === cur.ans){ score++; b.classList.add('ok'); feedback(fb, true, '&#10003;'); }
else { b.classList.add('fail'); feedback(fb, false, 'Правильно: $' + cur.ans + '$'); renderMath(fb); }
document.getElementById('p10f-score').textContent = score;
if(i >= tasks.length){ setTimeout(()=>{ feedback(fb, score >= 4, 'Итог: ' + score + '/' + tasks.length); if(score >= 4){ achievement('p10_fraction'); bumpProgress('p10', 14); confetti(); } }, 600); }
else { i++; setTimeout(show, 900); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p10f-fb').style.display = 'none';
}
document.getElementById('p10f-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p10f-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
})();
/* INIT 5 — Drag */
(function(){
const items = [
{ id:1, html:'$x^2 - 5x + 6$', cat:'yes' },
{ id:2, html:'$x^2 + 1$', cat:'no' },
{ id:3, html:'$2x^2 + 5x - 3$', cat:'yes' },
{ id:4, html:'$x^2 - 2x + 5$', cat:'no' },
{ id:5, html:'$x^2 - 9$', cat:'yes' },
{ id:6, html:'$3x^2 + x + 1$', cat:'no' },
{ id:7, html:'$x^2 + 6x + 9$', cat:'yes' },
{ id:8, html:'$x^2 + 4$', cat:'no' },
];
const sorter = setupSorter({ poolId:'p10z-pool', cats:['yes','no'], items, scopeSelector:'#p10-body' });
document.getElementById('p10z-check').addEventListener('click', ()=>{
const fb = document.getElementById('p10z-fb');
fb.style.display = 'block';
const placedCount = Object.keys(sorter.placed).length;
if(placedCount < items.length){ feedback(fb, false, '&#9888; Разложите все ' + items.length + ' трёхчленов.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === items.length){ feedback(fb, true, '&#10003; Все ' + items.length + ' верно!'); achievement('p10_sort'); bumpProgress('p10', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p10z-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p10z-fb').style.display='none'; });
})();
}
function buildP11stub(){ buildP11(); }
function buildP11(){
const box = document.getElementById('p11-body');
let html = '';
html += makeCard('repeat','Повторение',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Дискриминант, формулы корней, теорема Виета — из § 8, § 9.</li>
<li><b>Движение:</b> $s = v \\cdot t$; средняя скорость на двух участках через общее $s$ и общее $t$.</li>
<li><b>Работа:</b> $A = p \\cdot t$, где $p$ — производительность.</li>
<li><b>Двузначное число</b> с цифрами $a$ (десятки) и $b$ (единицы): $\\overline{ab} = 10a + b$.</li>
</ul>`);
html += makeCard('theory','4 шага решения','11.1',`
<ol style="margin-left:18px;line-height:1.9">
<li><b>Анализ.</b> Прочесть условие, выделить величины, связи, искомое.</li>
<li><b>Модель.</b> Обозначить неизвестное буквой; выразить остальные величины через неё; составить уравнение.</li>
<li><b>Решение.</b> Решить уравнение — обычно квадратное.</li>
<li><b>Анализ ответа.</b> Проверить смысл (положительные ли значения, целые ли — если требуется), записать ответ.</li>
</ol>`);
html += makeCard('example','Образец',null,`
<p><b>Задача.</b> Произведение двух последовательных натуральных чисел равно 132. Найдите эти числа.</p>
<p><b>Решение.</b> Пусть меньшее $= x$. Тогда $x(x+1) = 132 \\Rightarrow x^2 + x - 132 = 0$. $D = 1 + 528 = 529$, $\\sqrt{D}=23$. $x = (-1+23)/2 = 11$ или $x = -12$. Натуральное только $11$.</p>
<p><b>Ответ:</b> $11$ и $12$.</p>`);
/* INT 1 — Шаблон «4 шага» */
html += widget('Шаблон «4 шага»','INTERACT 1','Решим задачу пошагово. На каждом шаге выберите правильный вариант.',`
<p style="margin-bottom:10px"><b>Задача.</b> Площадь прямоугольника равна 60 см², а одна сторона на 7 см больше другой. Найдите стороны.</p>
<div id="p11s-step" data-st="1" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem"></div>
<div id="p11s-opts" style="display:flex;flex-direction:column;gap:6px;margin-top:10px"></div>
<div class="feedback" id="p11s-fb" style="display:none;margin-top:10px"></div>`);
/* INT 2 — Тренажёр задач */
html += widget('Тренажёр текстовых задач','INTERACT 2','Решите задачу и введите ответ. После 5 задач — итог.',`
<div class="score-display"><span>Задача <b id="p11t-i">1</b> / 5</span><span>Очки: <b id="p11t-score">0</b></span></div>
<div id="p11t-task" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:.98rem;line-height:1.6;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
<input type="text" id="p11t-inp" placeholder="ответ (число или два через ;)" style="width:240px;padding:8px;border:1.5px solid var(--border);border-radius:8px">
<button class="btn primary" id="p11t-go">Ответ</button>
<button class="btn" id="p11t-hint">Подсказка</button>
</div>
<div class="feedback" id="p11t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p11t-start" style="margin-top:10px">Начать</button>`);
/* INT 3 — Конструктор задачи на движение */
html += widget('Движение по реке','INTERACT 3','Лодка идёт по реке и обратно. Введите $v_л$ и $v_р$ — узнайте, как изменится время.',`
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;justify-content:center;margin-bottom:10px">
<label>Скорость лодки $v_л$ (км/ч) = <input type="number" id="p11m-vl" value="10" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>Скорость реки $v_р$ (км/ч) = <input type="number" id="p11m-vr" value="2" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>Расстояние $S$ (км) = <input type="number" id="p11m-s" value="24" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
</div>
<div id="p11m-out" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;line-height:1.7"></div>`);
/* INT 4 — Задача про двузначное число */
html += widget('Задача про двузначное число','INTERACT 4','Сумма квадратов цифр двузначного числа равна 41, а само число равно сумме его цифр, умноженной на 6. Найдите число.',`
<p style="margin-bottom:10px">Пусть $a$ — десятки, $b$ — единицы. Тогда число $= 10a + b$, $a^2 + b^2 = 41$ и $10a + b = 6(a+b)$.</p>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<label>Ваш ответ: <input type="number" id="p11d-inp" placeholder="двузначное число" style="width:130px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<button class="btn primary" id="p11d-check">Проверить</button>
<button class="btn" id="p11d-show">Показать решение</button>
</div>
<div class="feedback" id="p11d-fb" style="display:none;margin-top:10px"></div>
<div id="p11d-sol" style="display:none;margin-top:10px;padding:12px;background:var(--card);border-radius:8px;border:1px solid var(--border)"></div>`);
/* INT 5 — Drag: типы задач */
html += widget('Классифицируем тип задачи','INTERACT 5','Прочитайте задачу и перетащите её в нужный ящик.',`
${DND_HINT_HTML}
<div id="p11c-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px">
<div class="drop-box"><h5>Движение</h5><div class="drop-items" data-cat="dv"></div></div>
<div class="drop-box"><h5>Работа</h5><div class="drop-items" data-cat="wk"></div></div>
<div class="drop-box"><h5>Числа</h5><div class="drop-items" data-cat="nm"></div></div>
<div class="drop-box"><h5>Геометрия</h5><div class="drop-items" data-cat="gm"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p11c-check">Проверить</button><button class="btn" id="p11c-reset">Сначала</button></div>
<div class="feedback" id="p11c-fb" style="display:none"></div>`);
html += makeCard('class','Класс — решите',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Произведение двух последовательных нечётных чисел равно 195. Найдите их.</li>
<li>Длина сада на 5 м больше ширины, а площадь — 84 м². Найдите стороны.</li>
<li>Двое рабочих вместе закончили работу за 6 ч. Первый один сделал бы её на 5 ч быстрее, чем второй. За сколько часов сделал бы её каждый?</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Сумма квадратов двух последовательных натуральных чисел равна 113. Найдите их.</li>
<li>Лодка прошла 30 км по течению и 30 км против за 4 ч. Скорость течения 1 км/ч. Найдите скорость лодки.</li>
<li>Найдите два числа, сумма которых 14, а произведение 48.</li>
</ol>`);
html += secNav('p10','p12');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Шаблон 4 шага */
(function(){
const steps = [
{ q:'Шаг 1. Что обозначим за $x$?', opts:['меньшую сторону','произведение','периметр','диагональ'], ok:0 },
{ q:'Шаг 2. Тогда большая сторона равна:', opts:['$x - 7$','$x + 7$','$60/x$','$7x$'], ok:1 },
{ q:'Шаг 3. Уравнение из условия $S = a \\cdot b = 60$:', opts:['$x(x+7) = 60$','$x^2 - 7 = 60$','$2x + 7 = 60$','$x/7 = 60$'], ok:0 },
{ q:'Шаг 4. После раскрытия скобок и переноса:', opts:['$x^2 + 7x - 60 = 0$','$x^2 - 7x + 60 = 0$','$x^2 + 7 = 60$','$x^2 = 67$'], ok:0 },
{ q:'Шаг 5. $D = 49 + 240 = 289 \\Rightarrow \\sqrt{D} = 17$. Корни:', opts:['$x = 5$ и $x = -12$','$x = 6$ и $x = -10$','$x = 4$ и $x = -15$','$x = 8$ и $x = -7$'], ok:0 },
{ q:'Шаг 6. Ответ:', opts:['5 см и 12 см','12 см и 5 см','оба корня подходят','7 см и 15 см'], ok:0 },
];
let i = 0;
function show(){
const s = steps[i];
document.getElementById('p11s-step').innerHTML = '<b>' + (i+1) + ' / ' + steps.length + '.</b> ' + s.q;
renderMath(document.getElementById('p11s-step'));
const opts = document.getElementById('p11s-opts'); opts.innerHTML = '';
s.opts.forEach((o,k)=>{
const b = document.createElement('button');
b.className = 'btn'; b.innerHTML = o; b.style.cssText = 'text-align:left';
b.addEventListener('click', ()=>{
const fb = document.getElementById('p11s-fb'); fb.style.display = 'block';
if(k === s.ok){
b.classList.add('ok');
feedback(fb, true, '&#10003;');
if(i >= steps.length - 1){ feedback(fb, true, '&#10003; Задача решена!'); achievement('p11_steps'); bumpProgress('p11', 18); confetti(); }
else setTimeout(()=>{ i++; show(); }, 700);
} else { b.classList.add('fail'); feedback(fb, false, 'Не то, подумайте ещё.'); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p11s-fb').style.display = 'none';
}
show();
})();
/* INIT 2 — Тренажёр */
(function(){
const tasks = [
{ q:'Произведение двух последовательных натуральных чисел равно 56. Найдите большее.', hint:'$x(x+1) = 56$', ans:[8] },
{ q:'Площадь прямоугольника 48 см², одна сторона на 2 см больше другой. Найдите меньшую.', hint:'$x(x+2)=48$', ans:[6] },
{ q:'Сумма числа и его квадрата равна 90. Найдите положительное число.', hint:'$x + x^2 = 90$', ans:[9] },
{ q:'Произведение двух чисел равно 35, а их сумма равна 12. Найдите большее.', hint:'Виета: $x_1+x_2=12,\\ x_1 x_2=35$', ans:[7] },
{ q:'Лодка прошла 12 км по течению (скорость = $v+1$) и обратно ($v-1$) за 5 ч. Найдите скорость в стоячей воде.', hint:'$\\dfrac{12}{v+1} + \\dfrac{12}{v-1} = 5$', ans:[5] },
];
let cur = null, i = 1, score = 0;
function show(){
cur = tasks[i-1];
document.getElementById('p11t-i').textContent = i;
document.getElementById('p11t-task').innerHTML = '<b>Задача ' + i + '.</b> ' + cur.q;
renderMath(document.getElementById('p11t-task'));
document.getElementById('p11t-inp').value = '';
document.getElementById('p11t-fb').style.display = 'none';
}
document.getElementById('p11t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p11t-score').textContent = 0; show(); });
document.getElementById('p11t-go').addEventListener('click', ()=>{
const fb = document.getElementById('p11t-fb'); fb.style.display = 'block';
const u = +document.getElementById('p11t-inp').value;
const ok = cur.ans.some(a => Math.abs(u - a) < 1e-6);
if(ok){ score++; feedback(fb, true, '&#10003;'); }
else feedback(fb, false, 'Не то. Ответ: ' + cur.ans.join(' или '));
document.getElementById('p11t-score').textContent = score;
if(i >= tasks.length){ setTimeout(()=>{ feedback(fb, score >= 3, 'Итог: ' + score + '/' + tasks.length); if(score >= 3){ achievement('p11_train'); bumpProgress('p11', 16); confetti(); } }, 600); }
else { i++; setTimeout(show, 900); }
});
document.getElementById('p11t-hint').addEventListener('click', ()=>{
const fb = document.getElementById('p11t-fb'); fb.style.display = 'block';
feedback(fb, true, 'Подсказка: ' + cur.hint); renderMath(fb);
});
})();
/* INIT 3 — Движение */
(function(){
const vlE = document.getElementById('p11m-vl'), vrE = document.getElementById('p11m-vr'), sE = document.getElementById('p11m-s');
const out = document.getElementById('p11m-out');
let done = false;
function refresh(){
const vl = +vlE.value, vr = +vrE.value, s = +sE.value;
if(vl <= vr || vr < 0 || s <= 0){ out.innerHTML = '<span style="color:var(--bad)">Условие: $v_л > v_р \\geq 0,\\ S > 0$.</span>'; renderMath(out); return; }
const t1 = s/(vl+vr), t2 = s/(vl-vr), tT = t1 + t2;
let html = '<div><b>По течению:</b> $v = ' + (vl+vr) + '$ км/ч &rarr; $t = ' + s + '/' + (vl+vr) + ' = ' + fmt(t1) + '$ ч</div>';
html += '<div><b>Против течения:</b> $v = ' + (vl-vr) + '$ км/ч &rarr; $t = ' + fmt(t2) + '$ ч</div>';
html += '<div><b>Итого время:</b> $' + fmt(tT) + '$ ч</div>';
const vAvg = 2*s/tT;
html += '<div><b>Средняя скорость:</b> $\\dfrac{2S}{t_1+t_2} = ' + fmt(vAvg) + '$ км/ч</div>';
out.innerHTML = html; renderMath(out);
if(!done){ done = true; setTimeout(()=>{ achievement('p11_move'); bumpProgress('p11', 14); }, 300); }
}
[vlE,vrE,sE].forEach(e => e.addEventListener('input', refresh));
refresh();
})();
/* INIT 4 — Задача про число */
(function(){
document.getElementById('p11d-check').addEventListener('click', ()=>{
const v = +document.getElementById('p11d-inp').value;
const fb = document.getElementById('p11d-fb'); fb.style.display = 'block';
if(v === 54){ feedback(fb, true, '&#10003; Верно! Число = 54.'); achievement('p11_digit'); bumpProgress('p11', 14); confetti(); }
else feedback(fb, false, 'Не то. Подсказка: $a^2 + b^2 = 41,\\ 10a+b = 6(a+b) \\Rightarrow 4a = 5b$.');
renderMath(fb);
});
document.getElementById('p11d-show').addEventListener('click', ()=>{
const s = document.getElementById('p11d-sol');
s.style.display = 'block';
s.innerHTML = '<p><b>Шаг 1.</b> Из $10a+b = 6(a+b)$ получаем $4a = 5b$, то есть $b = \\dfrac{4a}{5}$. Чтобы $b$ было цифрой (0..9), $a$ должно делиться на 5.</p><p><b>Шаг 2.</b> $a = 5$ &rarr; $b = 4$. Проверяем: $a^2+b^2 = 25+16 = 41$. &#10003;</p><p><b>Ответ:</b> число $= 54$.</p>';
renderMath(s);
});
})();
/* INIT 5 — Drag типы */
(function(){
const items = [
{ id:1, html:'Лодка прошла 24 км по течению и обратно.', cat:'dv' },
{ id:2, html:'Двое рабочих вместе закончили работу за 6 ч.', cat:'wk' },
{ id:3, html:'Произведение последовательных чисел равно 90.', cat:'nm' },
{ id:4, html:'Сторона квадрата увеличена на 3 см, площадь увеличилась на 33 см².', cat:'gm' },
{ id:5, html:'Автомобиль и автобус выехали навстречу из A и B.', cat:'dv' },
{ id:6, html:'Сумма квадратов цифр двузначного числа = 25.', cat:'nm' },
{ id:7, html:'Бассейн наполняется одной трубой на 2 ч быстрее, чем другой.', cat:'wk' },
{ id:8, html:'Площадь прямоугольника 48 см², а периметр 28 см.', cat:'gm' },
];
const sorter = setupSorter({ poolId:'p11c-pool', cats:['dv','wk','nm','gm'], items, scopeSelector:'#p11-body', columnLayout:true });
document.getElementById('p11c-check').addEventListener('click', ()=>{
const fb = document.getElementById('p11c-fb'); fb.style.display = 'block';
if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '&#9888; Разложите все ' + items.length + ' задач.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === items.length){ feedback(fb, true, '&#10003; Все верно!'); achievement('p11_class'); bumpProgress('p11', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p11c-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p11c-fb').style.display='none'; });
})();
}
function buildP12stub(){ buildP12(); }
function buildP12(){
const box = document.getElementById('p12-body');
let html = '';
html += makeCard('repeat','Повторение',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Квадратное уравнение, дискриминант, Виета — §§ 79.</li>
<li>Разложение трёхчлена $ax^2+bx+c$ — § 10.</li>
<li><b>ОДЗ</b> дробного выражения: знаменатель $\\neq 0$.</li>
</ul>`);
html += makeCard('theory','Биквадратное уравнение','12.1',`
<p><b>Биквадратным</b> называют уравнение вида $ax^4 + bx^2 + c = 0$, $a \\neq 0$.</p>
<p style="margin-top:6px"><b>Метод.</b> Замена $t = x^2$, $t \\geq 0$. Получаем квадратное $at^2 + bt + c = 0$. Решаем его, потом для каждого $t \\geq 0$ находим $x = \\pm\\sqrt{t}$.</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:12px;margin:10px 0;text-align:center;font-size:1.05rem">$$t = x^2 \\geq 0,\\quad x = \\pm\\sqrt{t}$$</div>`);
html += makeCard('rule','Дробные → квадратные','12.2',`
<ol style="margin-left:18px;line-height:1.8">
<li>Найти ОДЗ (знаменатели $\\neq 0$).</li>
<li>Привести к общему знаменателю и умножить уравнение на него.</li>
<li>Решить полученное многочленное уравнение.</li>
<li>Отбросить посторонние корни (вне ОДЗ).</li>
</ol>`);
html += makeCard('example','Примеры',null,`
<p><b>1)</b> $x^4 - 5x^2 + 4 = 0$. Замена $t = x^2$: $t^2 - 5t + 4 = 0 \\Rightarrow t = 1$ или $t = 4$. Тогда $x = \\pm 1$ или $x = \\pm 2$. Ответ: $\\{-2;-1;1;2\\}$.</p>
<p style="margin-top:6px"><b>2)</b> $x^4 + 3x^2 - 4 = 0$. $t^2 + 3t - 4 = 0 \\Rightarrow t = 1$ или $t = -4$. Только $t = 1$ годится: $x = \\pm 1$.</p>
<p style="margin-top:6px"><b>3)</b> $\\dfrac{x}{x-2} - \\dfrac{2}{x+2} = 1$. ОДЗ: $x \\neq \\pm 2$. Умножим на $(x-2)(x+2)$: $x(x+2) - 2(x-2) = (x-2)(x+2) \\Rightarrow x^2 + 2x - 2x + 4 = x^2 - 4 \\Rightarrow 4 = -4$ — противоречие, корней нет.</p>`);
/* INT 1 — Биквадратное пошагово */
html += widget('Решатель биквадратного','INTERACT 1','Введите $a$, $b$, $c$ и нажимайте «Дальше» — шаги замены и решения.',`
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:10px">
<label>$a$ = <input type="number" id="p12b-a" value="1" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>$b$ = <input type="number" id="p12b-b" value="-5" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>$c$ = <input type="number" id="p12b-c" value="4" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<button class="btn primary" id="p12b-go">Старт</button>
<button class="btn" id="p12b-next" style="display:none">Дальше</button>
<button class="btn" id="p12b-reset" style="display:none">Сначала</button>
</div>
<div id="p12b-out" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;line-height:1.7;min-height:60px"></div>`);
/* INT 2 — Тренажёр биквадратных */
html += widget('Тренажёр биквадратных','INTERACT 2','Решите устно. Вводите все корни через точку с запятой (или «нет»).',`
<div class="score-display"><span>Задача <b id="p12t-i">1</b> / 6</span><span>Очки: <b id="p12t-score">0</b></span></div>
<div id="p12t-task" style="font-size:1.3rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
<input type="text" id="p12t-inp" placeholder="например: -2; -1; 1; 2" style="width:240px;padding:8px;border:1.5px solid var(--border);border-radius:8px">
<button class="btn primary" id="p12t-go">Ответ</button>
</div>
<div class="feedback" id="p12t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p12t-start" style="margin-top:10px">Начать</button>`);
/* INT 3 — Дробное уравнение */
html += widget('Дробное → квадратное','INTERACT 3','По шагам решаем уравнение с дробями. Внимание к ОДЗ!',`
<p style="margin-bottom:10px">Решим: $\\dfrac{x+3}{x-1} - \\dfrac{2}{x+1} = 2$</p>
<div id="p12f-step" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.02rem;line-height:1.7"></div>
<div id="p12f-opts" style="display:flex;flex-direction:column;gap:6px;margin-top:10px"></div>
<div class="feedback" id="p12f-fb" style="display:none;margin-top:10px"></div>`);
/* INT 4 — Какая замена */
html += widget('Выберите подходящую замену','INTERACT 4','Для каждого уравнения подберите замену переменной.',`
<div id="p12s-pool"></div>
<div class="feedback" id="p12s-fb" style="display:none;margin-top:10px"></div>`);
/* INT 5 — ОДЗ проверка */
html += widget('Найди посторонний корень','INTERACT 5','Дано дробное уравнение с готовыми кандидатами на корни. Найдите корень, который НЕ удовлетворяет ОДЗ.',`
<div id="p12o-task" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.02rem;line-height:1.7;margin-bottom:10px"></div>
<div id="p12o-opts" style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap"></div>
<div class="feedback" id="p12o-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p12o-start" style="margin-top:10px">Начать</button>`);
html += makeCard('class','Класс — решите',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$x^4 - 13x^2 + 36 = 0$</li>
<li>$x^4 - 5x^2 - 36 = 0$</li>
<li>$\\dfrac{2}{x-1} + \\dfrac{3}{x+1} = 1$</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$x^4 - 10x^2 + 9 = 0$</li>
<li>$x^4 + 7x^2 - 8 = 0$</li>
<li>$\\dfrac{x}{x+1} - \\dfrac{1}{x-1} = \\dfrac{2}{x^2-1}$</li>
<li>$(x^2 - 2)^2 - 3(x^2 - 2) - 4 = 0$ (замена!)</li>
</ol>`);
html += secNav('p11','final2');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Биквадратное пошагово */
(function(){
const out = document.getElementById('p12b-out');
const goBtn = document.getElementById('p12b-go');
const nextBtn = document.getElementById('p12b-next');
const resetBtn = document.getElementById('p12b-reset');
let steps = [], idx = 0, awarded = false;
function build(a, b, c){
const arr = [];
arr.push('<b>Дано:</b> $' + a + 'x^4 ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + 'x^2 ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c)) + ' = 0$');
arr.push('<b>Замена.</b> Пусть $t = x^2,\\ t \\geq 0$. Получаем $' + a + 't^2 ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + 't ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c)) + ' = 0$');
const D = b*b - 4*a*c;
arr.push('<b>Дискриминант.</b> $D = ' + (b*b) + ' - ' + (4*a*c) + ' = ' + D + '$');
if(D < 0){
arr.push('<b>Анализ.</b> $D < 0 \\Rightarrow$ нет $t$, значит нет $x$.');
arr.push('<b>Ответ:</b> корней нет.');
} else {
const t1 = (-b - Math.sqrt(D))/(2*a), t2 = (-b + Math.sqrt(D))/(2*a);
arr.push('<b>Корни вспомогательного.</b> $t_1 = ' + fmt(t1) + ',\\ t_2 = ' + fmt(t2) + '$');
const roots = [];
[t1, t2].forEach((t, k)=>{
if(t > 0){ const r = Math.sqrt(t); roots.push(r, -r); arr.push('<b>Возврат.</b> $t_' + (k+1) + ' = ' + fmt(t) + ' > 0 \\Rightarrow x = \\pm\\sqrt{' + fmt(t) + '} = \\pm' + fmt(r) + '$'); }
else if(t === 0){ roots.push(0); arr.push('<b>Возврат.</b> $t_' + (k+1) + ' = 0 \\Rightarrow x = 0$'); }
else arr.push('<b>Возврат.</b> $t_' + (k+1) + ' = ' + fmt(t) + ' < 0$ — не подходит, ведь $t \\geq 0$.');
});
const u = [...new Set(roots)].sort((a,b)=>a-b);
arr.push('<b>Ответ:</b> ' + (u.length ? '$\\{' + u.map(fmt).join(';\\ ') + '\\}$' : 'корней нет'));
}
return arr;
}
function render(){
out.innerHTML = steps.slice(0, idx + 1)
.map(s => `<div style="margin:6px 0;padding:9px 12px;background:var(--card);border-left:3px solid var(--sec-acc);border-radius:7px;animation:fadeIn .3s ease">${s}</div>`)
.join('');
renderMath(out);
if(idx >= steps.length - 1){
nextBtn.disabled = true; nextBtn.textContent = 'Готово';
if(!awarded){ awarded = true; achievement('p12_bi'); bumpProgress('p12', 14); confetti(); }
} else {
nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')';
}
}
goBtn.addEventListener('click', ()=>{
const a = +document.getElementById('p12b-a').value, b = +document.getElementById('p12b-b').value, c = +document.getElementById('p12b-c').value;
if(!a){ out.innerHTML = '$a \\neq 0$'; renderMath(out); return; }
steps = build(a, b, c); idx = 0; awarded = false;
goBtn.style.display = 'none'; nextBtn.style.display = ''; resetBtn.style.display = '';
render();
});
nextBtn.addEventListener('click', ()=>{ if(idx < steps.length - 1){ idx++; render(); } });
resetBtn.addEventListener('click', ()=>{
idx = 0; steps = []; awarded = false; out.innerHTML = '';
goBtn.style.display = ''; nextBtn.style.display = 'none'; resetBtn.style.display = 'none';
});
})();
/* INIT 2 — Тренажёр биквадр */
(function(){
const tasks = [
{ eq:'x^4 - 5x^2 + 4 = 0', ans:[-2,-1,1,2] },
{ eq:'x^4 - 10x^2 + 9 = 0', ans:[-3,-1,1,3] },
{ eq:'x^4 - 13x^2 + 36 = 0', ans:[-3,-2,2,3] },
{ eq:'x^4 + 3x^2 - 4 = 0', ans:[-1,1] },
{ eq:'x^4 + 2x^2 + 5 = 0', ans:null },
{ eq:'x^4 - 16 = 0', ans:[-2,2] },
];
let cur = null, i = 1, score = 0, shuffled = [];
function show(){
cur = shuffled[i-1];
document.getElementById('p12t-i').textContent = i;
document.getElementById('p12t-task').innerHTML = '$' + cur.eq + '$';
renderMath(document.getElementById('p12t-task'));
document.getElementById('p12t-inp').value = '';
document.getElementById('p12t-fb').style.display = 'none';
}
function parse(s){
s = s.trim().toLowerCase();
if(/нет|none/.test(s)) return null;
return s.replace(/,/g,';').split(/[;\s]+/).filter(Boolean).map(Number).sort((a,b)=>a-b);
}
function check(){
const fb = document.getElementById('p12t-fb'); fb.style.display = 'block';
const u = parse(document.getElementById('p12t-inp').value);
let ok = false;
if(cur.ans === null) ok = u === null;
else if(u !== null){ const a = [...cur.ans].sort((x,y)=>x-y); ok = a.length === u.length && a.every((v,k)=>v === u[k]); }
if(ok){ score++; feedback(fb, true, '&#10003;'); }
else feedback(fb, false, 'Правильно: ' + (cur.ans === null ? 'нет' : cur.ans.join('; ')));
document.getElementById('p12t-score').textContent = score;
if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 4, 'Итог: ' + score + '/' + shuffled.length); if(score >= 4){ achievement('p12_train'); bumpProgress('p12', 16); confetti(); } }, 600); }
else { i++; setTimeout(show, 900); }
}
document.getElementById('p12t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p12t-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
document.getElementById('p12t-go').addEventListener('click', check);
document.getElementById('p12t-inp').addEventListener('keyup', e=>{ if(e.key === 'Enter') check(); });
})();
/* INIT 3 — Дробное */
(function(){
const steps = [
{ q:'Шаг 1. ОДЗ:', opts:['$x \\neq 1$ и $x \\neq -1$','$x \\neq 0$','$x > 0$','любое $x$'], ok:0 },
{ q:'Шаг 2. Общий знаменатель:', opts:['$(x-1)(x+1)$','$x-1$','$x+1$','$x^2$'], ok:0 },
{ q:'Шаг 3. После умножения на ОЗ:', opts:['$(x+3)(x+1) - 2(x-1) = 2(x-1)(x+1)$','$x+3 - 2 = 2$','$(x+3) - 2 = 2(x-1)$','$x+3-2(x+1) = 2$'], ok:0 },
{ q:'Шаг 4. Раскрытие и приведение:', opts:['$x^2 + 4x + 5 = 2x^2 - 2 \\Rightarrow x^2 - 4x - 7 = 0$','$x^2 = 7$','$x^2 + 4x = 0$','$x = 5$'], ok:0 },
{ q:'Шаг 5. Корни:', opts:['$x = 2 \\pm \\sqrt{11}$','$x = 1, 7$','$x = -1, 7$','корней нет'], ok:0 },
{ q:'Шаг 6. Проверка ОДЗ ($x \\neq \\pm 1$):', opts:['оба корня подходят','$x = 2 - \\sqrt{11}$ лишний','$x = 2 + \\sqrt{11}$ лишний','оба лишние'], ok:0 },
];
let i = 0;
function show(){
const s = steps[i];
document.getElementById('p12f-step').innerHTML = '<b>' + (i+1) + ' / ' + steps.length + '.</b> ' + s.q;
renderMath(document.getElementById('p12f-step'));
const opts = document.getElementById('p12f-opts'); opts.innerHTML = '';
s.opts.forEach((o,k)=>{
const b = document.createElement('button');
b.className = 'btn'; b.innerHTML = o; b.style.cssText = 'text-align:left';
b.addEventListener('click', ()=>{
const fb = document.getElementById('p12f-fb'); fb.style.display = 'block';
if(k === s.ok){
b.classList.add('ok');
feedback(fb, true, '&#10003;');
if(i >= steps.length - 1){ feedback(fb, true, '&#10003; Уравнение решено!'); achievement('p12_frac'); bumpProgress('p12', 16); confetti(); }
else setTimeout(()=>{ i++; show(); }, 700);
} else { b.classList.add('fail'); feedback(fb, false, 'Не то, подумайте.'); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p12f-fb').style.display = 'none';
}
show();
})();
/* INIT 4 — Замены */
(function(){
const items = [
{ eq:'x^4 + 3x^2 - 4 = 0', ans:'t=x^2' },
{ eq:'(x^2-3)^2 - 5(x^2-3) + 6 = 0', ans:'t=x^2-3' },
{ eq:'x^2 + \\dfrac{1}{x^2} - 4 = 0', ans:'t=x^2' },
{ eq:'(x+\\dfrac{1}{x})^2 - 3(x+\\dfrac{1}{x}) + 2 = 0', ans:'t=x+1/x' },
];
const opts = ['t=x^2', 't=x^2-3', 't=x+1/x', 't=x-1'];
const pool = document.getElementById('p12s-pool');
let answered = 0, score = 0;
items.forEach((it, idx)=>{
const row = document.createElement('div');
row.style.cssText = 'display:flex;gap:8px;align-items:center;padding:8px;border-bottom:1px solid var(--border);flex-wrap:wrap';
row.innerHTML = '<div style="flex:1;min-width:160px">$' + it.eq + '$</div>';
const sel = document.createElement('select');
sel.style.cssText = 'padding:6px;border:1.5px solid var(--border);border-radius:6px';
sel.innerHTML = '<option value="">— выбрать —</option>' + opts.map(o => '<option value="' + o + '">$' + o + '$</option>').join('').replace(/\$/g,'');
const mark = document.createElement('span'); mark.style.cssText = 'font-weight:700;font-size:1.2rem;width:24px;text-align:center';
const check = document.createElement('button'); check.className = 'btn small'; check.textContent = 'Проверить';
check.addEventListener('click', ()=>{
if(!sel.value){ mark.textContent = '?'; mark.style.color = 'var(--muted)'; return; }
if(sel.value === it.ans){ mark.textContent = '✓'; mark.style.color = 'var(--ok)'; if(!row.dataset.scored){ row.dataset.scored='1'; score++; answered++; } }
else { mark.textContent = '✗'; mark.style.color = 'var(--bad)'; if(!row.dataset.answered){ row.dataset.answered='1'; answered++; } }
if(answered >= items.length){
const fb = document.getElementById('p12s-fb'); fb.style.display = 'block';
feedback(fb, score >= 3, 'Итог: ' + score + '/' + items.length);
if(score >= 3){ achievement('p12_subst'); bumpProgress('p12', 14); confetti(); }
}
});
row.appendChild(sel); row.appendChild(check); row.appendChild(mark);
pool.appendChild(row);
});
if(window.renderMathInElement) renderMath(pool);
})();
/* INIT 5 — Посторонний корень */
(function(){
const tasks = [
{ eq:'\\dfrac{1}{x-2} = \\dfrac{x-2}{4}', roots:[0, 4, -2, 2], wrong:[2], odz:'x \\neq 2' },
{ eq:'\\dfrac{x}{x-3} = \\dfrac{9}{x-3}', roots:[3, 9, -9, 0], wrong:[3], odz:'x \\neq 3' },
{ eq:'\\dfrac{1}{x+1} + \\dfrac{1}{x-1} = 0', roots:[1, -1, 0, 2], wrong:[1,-1], odz:'x \\neq \\pm 1' },
];
let cur = null, i = 1;
function show(){
cur = tasks[i-1];
document.getElementById('p12o-task').innerHTML = '<b>Уравнение ' + i + ' / ' + tasks.length + ':</b> $' + cur.eq + '$<br><span style="color:var(--muted);font-size:.9rem">ОДЗ: $' + cur.odz + '$</span><br>Кандидаты на корни:';
renderMath(document.getElementById('p12o-task'));
const opts = document.getElementById('p12o-opts'); opts.innerHTML = '';
cur.roots.forEach(r=>{
const b = document.createElement('button'); b.className = 'btn'; b.textContent = 'x = ' + r;
b.addEventListener('click', ()=>{
const fb = document.getElementById('p12o-fb'); fb.style.display = 'block';
if(cur.wrong.includes(r)){ b.classList.add('ok'); feedback(fb, true, '&#10003; Это посторонний корень (не в ОДЗ).'); }
else { b.classList.add('fail'); feedback(fb, false, 'Не то — этот корень допустим в ОДЗ. Ищите тот, что нарушает.'); return; }
if(i >= tasks.length){ setTimeout(()=>{ feedback(fb, true, 'Все 3 пройдено!'); achievement('p12_odz'); bumpProgress('p12', 14); confetti(); }, 500); }
else { i++; setTimeout(show, 800); }
});
opts.appendChild(b);
});
document.getElementById('p12o-fb').style.display = 'none';
}
document.getElementById('p12o-start').addEventListener('click', ()=>{ i=1; show(); });
})();
}
function buildFinal2stub(){ buildFinal2(); }
function buildFinal2(){
const box = document.getElementById('final2-body');
let html = '';
html += `<div class="card"><div class="card-header"><div class="card-icon theory">${ICONS.theory}</div><div class="card-title">Поздравляем!</div></div><div class="card-body">
<p>Вы прошли все 6 параграфов главы «Квадратные уравнения». В финале вас ждут <b>7 боссов</b> — по одному на каждый параграф и один общий. Победите всех — получите титул «Магистр квадратных уравнений».</p>
</div></div>`;
/* BOSS ARENA */
html += widget('Боссы главы 2','BOSS ARENA','Каждый босс — 5 заданий. Победите всех 7, чтобы открыть финальный титул.',`
<div id="boss-grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px"></div>
<div id="boss-arena" style="margin-top:14px;display:none">
<div id="boss-head" style="display:flex;align-items:center;gap:14px;padding:14px;background:linear-gradient(135deg,#1f2937,#0f172a);border-radius:10px;color:#fff;margin-bottom:10px">
<div id="boss-emoji" style="font-size:2.4rem;line-height:1">★</div>
<div style="flex:1">
<div id="boss-name" style="font-family:'Unbounded',sans-serif;font-weight:800;font-size:1.05rem"></div>
<div id="boss-subname" style="font-size:.85rem;opacity:.75"></div>
<div class="hp-bar" style="margin-top:6px;background:rgba(255,255,255,.15);height:8px;border-radius:4px;overflow:hidden"><div id="boss-hp" style="height:100%;background:linear-gradient(90deg,#10b981,#fbbf24,#ef4444);width:100%;transition:width .5s"></div></div>
</div>
<button class="btn" id="boss-quit" style="background:rgba(255,255,255,.15);color:#fff;border-color:transparent">Выйти</button>
</div>
<div id="boss-task" style="padding:16px;background:var(--card);border:1.5px solid var(--sec-acc);border-radius:10px;min-height:120px"></div>
<div id="boss-opts" style="display:flex;flex-direction:column;gap:6px;margin-top:10px"></div>
<div id="boss-fb" class="feedback" style="display:none;margin-top:10px"></div>
</div>`);
/* FUN MATH */
html += widget('Увлекательная математика','BONUS','3 факта и парадокса про квадратные уравнения.',`
<details class="spoiler" style="margin-bottom:8px"><summary>Кто открыл формулу корней?</summary>
<div class="spoiler-body">Первые методы — в Древнем Вавилоне (2000 до н. э.). Полную формулу для произвольных коэффициентов записал Мухаммед аль-Хорезми в IX веке в своём трактате «Краткая книга об исчислении ал-джабра и ал-мукабалы». От слова «ал-джабр» произошло слово «алгебра».</div>
</details>
<details class="spoiler" style="margin-bottom:8px"><summary>Почему $a \\neq 0$?</summary>
<div class="spoiler-body">Если $a = 0$, то уравнение $bx + c = 0$ — линейное (степени 1), а не квадратное. Степень уравнения — это степень его старшего члена, поэтому требование $a \\neq 0$ нужно, чтобы старший член был именно $x^2$.</div>
</details>
<details class="spoiler"><summary>Парадокс «потерянного корня»</summary>
<div class="spoiler-body">Если уравнение $\\dfrac{x^2-4}{x-2} = 0$ умножить на $x-2$, получим $x^2 - 4 = 0 \\Rightarrow x = \\pm 2$. Но $x = 2$ обнуляет знаменатель! Этот корень — посторонний. ОДЗ всегда выручает.</div>
</details>`);
/* PRACTICE GENERATOR */
html += widget('Финальная практика','PRACTICE','Случайные задачи из всей главы. Решайте, сколько хотите.',`
<div class="score-display"><span>Решено: <b id="prac-i">0</b></span><span>Правильно: <b id="prac-score">0</b></span><span>Серия: <b id="prac-streak">0</b></span></div>
<div id="prac-task" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;line-height:1.6;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:center">
<input type="text" id="prac-inp" placeholder="ответ" style="width:200px;padding:8px;border:1.5px solid var(--border);border-radius:8px">
<button class="btn primary" id="prac-go">Ответ</button>
<button class="btn" id="prac-next">Следующая</button>
</div>
<div class="feedback" id="prac-fb" style="display:none;margin-top:10px"></div>`);
/* CERTIFICATE */
html += `<div class="card"><div class="card-header"><div class="card-icon home">${ICONS.home}</div><div class="card-title">Сертификат прохождения</div></div><div class="card-body">
<p>Как вы оцените своё знание квадратных уравнений?</p>
<div id="cert-state" style="margin-top:10px;font-size:.9rem;color:var(--muted)"></div>
</div></div>`;
html += secNav('p12', null);
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* ===== BOSSES ===== */
const BOSSES = [
{ id:'b1', name:'Хранитель неполных', sub:'§ 7 · ax²+bx=0 и ax²+c=0', icon:'⚔', hp:5, tasks:[
{ t:'select', q:'Сколько корней у уравнения $5x^2 - 45 = 0$?', opts:['Два','Один','Корней нет'], ok:0 },
{ t:'input', q:'Решите $3x^2 - 12x = 0$. Введите больший корень.', ans:[4] },
{ t:'yesno', q:'Уравнение $2x^2 + 8 = 0$ имеет корни.', ok:false },
{ t:'input', q:'Корни $x^2 - 16 = 0$ — это $\\pm a$. Найдите $a$.', ans:[4] },
{ t:'select', q:'$ax^2 + c = 0$ имеет два корня, если…', opts:['$-c/a > 0$','$c > 0$','$ac > 0$','любое'], ok:0 },
]},
{ id:'b2', name:'Дискриминатор', sub:'§ 8 · D = b² 4ac', icon:'D', hp:5, tasks:[
{ t:'input', q:'Найдите $D$ для $x^2 - 7x + 12 = 0$.', ans:[1] },
{ t:'select', q:'$D < 0$ означает:', opts:['два различных корня','один корень','корней нет'], ok:2 },
{ t:'input', q:'Сумма корней $2x^2 - 10x + 8 = 0$ (через $-b/a$):', ans:[5] },
{ t:'yesno', q:'У $x^2 + x + 1 = 0$ есть действительные корни.', ok:false },
{ t:'input', q:'При каком $m$ уравнение $x^2 - 4x + m = 0$ имеет один корень? $m = ?$', ans:[4] },
]},
{ id:'b3', name:'Дух Виета', sub:'§ 9 · сумма и произведение', icon:'∑', hp:5, tasks:[
{ t:'input', q:'Корни $x^2 - 9x + 20 = 0$. Введите больший.', ans:[5] },
{ t:'select', q:'Если $x_1 + x_2 = -p,\\ x_1 x_2 = q$, то корни $\\{2; 7\\}$ дают уравнение:', opts:['$x^2 - 9x + 14 = 0$','$x^2 + 9x + 14 = 0$','$x^2 - 7x + 9 = 0$'], ok:0 },
{ t:'yesno', q:'У уравнения $x^2 + 3x + 10 = 0$ корни одного знака.', ok:true },
{ t:'input', q:'Произведение корней $x^2 + 4x - 21 = 0$.', ans:[-21] },
{ t:'select', q:'Знаки корней $x^2 - 5x + 6 = 0$:', opts:['оба «+»','оба «−»','разные'], ok:0 },
]},
{ id:'b4', name:'Разложитель', sub:'§ 10 · a(xx₁)(xx₂)', icon:'( )', hp:5, tasks:[
{ t:'select', q:'$x^2 - 4 = ?$', opts:['$(x-2)(x+2)$','$(x-4)(x+1)$','$(x-2)^2$','не раскладывается'], ok:0 },
{ t:'yesno', q:'Трёхчлен $x^2 + 9$ раскладывается на множители первой степени с действительными коэффициентами.', ok:false },
{ t:'input', q:'Корни трёхчлена $x^2 - 7x + 12$. Меньший:', ans:[3] },
{ t:'select', q:'$\\dfrac{x^2-1}{x^2+x-2}$ сокращается до:', opts:['$\\dfrac{x+1}{x+2}$','$\\dfrac{x-1}{x+2}$','$\\dfrac{1}{x+2}$'], ok:0 },
{ t:'input', q:'Найдите $x_1 \\cdot x_2$ для $2x^2 - 7x + 3$ (по Виета $c/a$).', ans:[1.5] },
]},
{ id:'b5', name:'Архивариус задач', sub:'§ 11 · текстовые модели', icon:'?', hp:5, tasks:[
{ t:'input', q:'Произведение двух последовательных натуральных чисел = 72. Большее:', ans:[9] },
{ t:'input', q:'Площадь прямоугольника 35 см², сторона на 2 см больше другой. Меньшая сторона:', ans:[5] },
{ t:'select', q:'Двое за 6 ч; первый один на 5 ч быстрее. Уравнение для времени второго $t$:', opts:['$\\dfrac{1}{t-5} + \\dfrac{1}{t} = \\dfrac{1}{6}$','$t + (t-5) = 6$','$6 = t(t-5)$'], ok:0 },
{ t:'yesno', q:'В задаче на площадь оба корня уравнения подходят, если оба положительные.', ok:true },
{ t:'input', q:'Сумма квадратов двух последовательных натуральных = 41. Меньшее:', ans:[4] },
]},
{ id:'b6', name:'Мастер замены', sub:'§ 12 · t = x², ОДЗ', icon:'t', hp:5, tasks:[
{ t:'input', q:'Сколько корней у $x^4 - 13x^2 + 36 = 0$?', ans:[4] },
{ t:'yesno', q:'У уравнения $x^4 + 5x^2 + 4 = 0$ есть действительные корни.', ok:false },
{ t:'select', q:'Подходящая замена для $(x^2-1)^2 - 5(x^2-1) + 4 = 0$:', opts:['$t = x^2 - 1$','$t = x^2$','$t = x - 1$'], ok:0 },
{ t:'input', q:'Биквадратное $x^4 - 5x^2 + 4$. Больший положительный корень:', ans:[2] },
{ t:'select', q:'В уравнении $\\dfrac{1}{x-2} = \\dfrac{x-2}{4}$ посторонний корень — это $x = ?$', opts:['$x = 2$','$x = 4$','$x = 0$'], ok:0 },
]},
{ id:'b7', name:'Магистр алгебры', sub:'Финал · вся глава', icon:'★', hp:7, tasks:[
{ t:'input', q:'$x^2 + 5x + 6 = 0$. Меньший корень:', ans:[-3] },
{ t:'select', q:'$2x^2 - 8 = 0 \\Rightarrow x = ?$', opts:['$\\pm 2$','$\\pm 4$','$\\pm\\sqrt{2}$','$\\pm 8$'], ok:0 },
{ t:'input', q:'$D$ для $3x^2 + 5x - 2 = 0$:', ans:[49] },
{ t:'input', q:'По Виета: $x_1 + x_2$ для $x^2 - 11x + 28$:', ans:[11] },
{ t:'select', q:'$x^2 - 9$ разложится в:', opts:['$(x-3)(x+3)$','$(x-9)(x+1)$','$(x-3)^2$'], ok:0 },
{ t:'input', q:'$x^4 - 10x^2 + 9 = 0$. Сумма всех корней:', ans:[0] },
{ t:'yesno', q:'Уравнение $x^2 = -4$ имеет корни в множестве действительных чисел.', ok:false },
]},
];
const BOSS_STATE = (function(){
try { return JSON.parse(localStorage.getItem('algebra8_ch2_bosses') || '{}'); } catch(e){ return {}; }
})();
function saveBosses(){ try{ localStorage.setItem('algebra8_ch2_bosses', JSON.stringify(BOSS_STATE)); }catch(e){} }
function renderBossGrid(){
const g = document.getElementById('boss-grid');
g.innerHTML = '';
BOSSES.forEach(b=>{
const won = BOSS_STATE[b.id];
const c = document.createElement('div');
c.style.cssText = 'background:' + (won ? 'linear-gradient(135deg,var(--ok-bg),#d1fae5)' : 'var(--card)') + ';border:1.5px solid ' + (won ? 'var(--ok)' : 'var(--border)') + ';border-radius:10px;padding:10px 12px;cursor:pointer;text-align:center;transition:transform .15s,box-shadow .15s';
c.innerHTML = '<div style="font-size:1.6rem;line-height:1;margin-bottom:4px">' + b.icon + '</div><div style="font-family:Unbounded,sans-serif;font-size:.78rem;font-weight:800;color:var(--sec-acc-d)">' + b.name + '</div><div style="font-size:.7rem;color:var(--muted);margin-top:2px">' + b.sub + '</div>' + (won ? '<div style="color:var(--ok);font-weight:700;font-size:.8rem;margin-top:4px">&#10003; Побеждён</div>' : '<div style="margin-top:4px"><button class="btn small" style="font-size:.72rem">В бой</button></div>');
c.addEventListener('click', ()=>startBoss(b.id));
c.addEventListener('mouseover', ()=>{ c.style.transform='translateY(-2px)'; c.style.boxShadow='var(--sh2)'; });
c.addEventListener('mouseout', ()=>{ c.style.transform=''; c.style.boxShadow=''; });
g.appendChild(c);
});
if(window.renderMathInElement) renderMath(g);
}
let currentBoss = null, taskIdx = 0, hpLeft = 0;
function startBoss(id){
const b = BOSSES.find(x => x.id === id);
if(!b) return;
currentBoss = b; taskIdx = 0; hpLeft = b.hp;
document.getElementById('boss-arena').style.display = 'block';
document.getElementById('boss-name').textContent = b.name;
document.getElementById('boss-subname').textContent = b.sub;
document.getElementById('boss-emoji').textContent = b.icon;
showTask();
}
function showTask(){
const t = currentBoss.tasks[taskIdx];
if(!t){ winBoss(); return; }
document.getElementById('boss-hp').style.width = (hpLeft / currentBoss.hp * 100) + '%';
const tEl = document.getElementById('boss-task');
tEl.innerHTML = '<div style="font-size:.75rem;color:var(--muted);margin-bottom:6px">Задание ' + (taskIdx + 1) + ' / ' + currentBoss.tasks.length + '</div><div style="font-size:1.05rem">' + t.q + '</div>';
renderMath(tEl);
const opts = document.getElementById('boss-opts'); opts.innerHTML = '';
document.getElementById('boss-fb').style.display = 'none';
if(t.t === 'select'){
t.opts.forEach((o,k)=>{
const b = document.createElement('button');
b.className = 'btn'; b.innerHTML = o; b.style.cssText = 'text-align:left';
b.addEventListener('click', ()=>checkAnswer(k === t.ok, b));
opts.appendChild(b);
});
renderMath(opts);
} else if(t.t === 'yesno'){
['Да','Нет'].forEach((lab, k)=>{
const b = document.createElement('button');
b.className = 'btn'; b.textContent = lab;
b.addEventListener('click', ()=>checkAnswer((k === 0) === t.ok, b));
opts.appendChild(b);
});
} else if(t.t === 'input'){
const wrap = document.createElement('div');
wrap.style.cssText = 'display:flex;gap:8px;align-items:center';
const inp = document.createElement('input');
inp.type = 'text'; inp.placeholder = 'ваш ответ';
inp.style.cssText = 'flex:1;padding:8px;border:1.5px solid var(--border);border-radius:8px';
const go = document.createElement('button'); go.className = 'btn primary'; go.textContent = 'Ответ';
go.addEventListener('click', ()=>{
const u = parseFloat(inp.value.replace(',','.'));
const ok = t.ans.some(a => Math.abs(u - a) < 1e-6);
checkAnswer(ok, go);
});
inp.addEventListener('keyup', e=>{ if(e.key === 'Enter') go.click(); });
wrap.appendChild(inp); wrap.appendChild(go); opts.appendChild(wrap);
}
}
function checkAnswer(ok, btn){
const fb = document.getElementById('boss-fb'); fb.style.display = 'block';
if(ok){
if(btn) btn.classList.add('ok');
feedback(fb, true, '&#10003; Точное попадание!');
taskIdx++;
setTimeout(showTask, 700);
} else {
if(btn) btn.classList.add('fail');
hpLeft = Math.max(0, hpLeft - 0);
feedback(fb, false, '&#10007; Промах! Попробуйте ещё.');
}
}
function winBoss(){
BOSS_STATE[currentBoss.id] = 1; saveBosses();
achievement('boss_' + currentBoss.id, 'Победил: ' + currentBoss.name);
bumpProgress('final2', 14); confetti();
document.getElementById('boss-task').innerHTML = '<div style="text-align:center;padding:20px"><div style="font-size:3rem">' + currentBoss.icon + '</div><h3 style="color:var(--ok);margin:10px 0">Победа!</h3><p>Босс <b>«' + currentBoss.name + '»</b> повержен.</p></div>';
document.getElementById('boss-opts').innerHTML = '<button class="btn primary" id="boss-back">К списку боссов</button>';
document.getElementById('boss-fb').style.display = 'none';
document.getElementById('boss-back').addEventListener('click', closeBoss);
renderBossGrid();
if(BOSSES.every(b => BOSS_STATE[b.id])){
setTimeout(()=>{ achievement('all_bosses', 'Магистр квадратных уравнений!'); confetti(); refreshCert(); }, 800);
}
}
function closeBoss(){ document.getElementById('boss-arena').style.display = 'none'; currentBoss = null; }
document.getElementById('boss-quit').addEventListener('click', closeBoss);
renderBossGrid();
/* ===== PRACTICE ===== */
(function(){
let cur = null, total = 0, score = 0, streak = 0;
function gen(){
const t = Math.floor(Math.random()*5);
if(t === 0){
const r1 = -5 + Math.floor(Math.random()*11), r2 = -5 + Math.floor(Math.random()*11);
if(r1 === r2) return gen();
const b = -(r1+r2), c = r1*r2;
return { q:'Решите $x^2 ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + 'x ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c)) + ' = 0$. Введите больший корень.', ans:[Math.max(r1, r2)] };
}
if(t === 1){
const r = 1 + Math.floor(Math.random()*7);
return { q:'$x^2 - ' + (r*r) + ' = 0 \\Rightarrow x = \\pm ?$', ans:[r] };
}
if(t === 2){
const r1 = 1 + Math.floor(Math.random()*7), r2 = -(1 + Math.floor(Math.random()*7));
const b = -(r1+r2), c = r1*r2;
return { q:'Сумма корней $x^2 ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + 'x ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c)) + ' = 0$?', ans:[r1+r2] };
}
if(t === 3){
const a = 1 + Math.floor(Math.random()*3), b = -(2 + Math.floor(Math.random()*8)), c = 1 + Math.floor(Math.random()*6);
return { q:'Найдите $D$ для $' + a + 'x^2 ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + 'x ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c)) + ' = 0$', ans:[b*b - 4*a*c] };
}
const r = 1 + Math.floor(Math.random()*4);
return { q:'Биквадратное $x^4 - ' + (r*r + 1) + 'x^2 + ' + (r*r) + ' = 0$. Сколько корней?', ans:[r === 1 ? 2 : 4] };
}
function show(){
cur = gen();
document.getElementById('prac-task').innerHTML = cur.q;
renderMath(document.getElementById('prac-task'));
document.getElementById('prac-inp').value = '';
document.getElementById('prac-fb').style.display = 'none';
}
document.getElementById('prac-go').addEventListener('click', ()=>{
const fb = document.getElementById('prac-fb'); fb.style.display = 'block';
const u = parseFloat(document.getElementById('prac-inp').value.replace(',','.'));
const ok = cur.ans.some(a => Math.abs(u - a) < 1e-6);
total++; if(ok){ score++; streak++; feedback(fb, true, '&#10003;'); if(streak === 5){ achievement('prac_streak', 'Серия из 5!'); confetti(); } }
else { streak = 0; feedback(fb, false, 'Правильно: ' + cur.ans.join(', ')); }
document.getElementById('prac-i').textContent = total;
document.getElementById('prac-score').textContent = score;
document.getElementById('prac-streak').textContent = streak;
if(total >= 5) { bumpProgress('final2', 4); }
});
document.getElementById('prac-next').addEventListener('click', show);
document.getElementById('prac-inp').addEventListener('keyup', e=>{ if(e.key === 'Enter') document.getElementById('prac-go').click(); });
show();
})();
/* CERTIFICATE */
function refreshCert(){
const won = BOSSES.filter(b => BOSS_STATE[b.id]).length;
const cs = document.getElementById('cert-state');
if(!cs) return;
if(won === BOSSES.length){
cs.innerHTML = '<div style="padding:18px;background:linear-gradient(135deg,#fef3c7,#fde68a);border:2px solid #d97706;border-radius:12px;text-align:center"><div style="font-family:Unbounded,sans-serif;font-size:1.2rem;font-weight:900;color:#92400e">МАГИСТР КВАДРАТНЫХ УРАВНЕНИЙ</div><div style="font-size:.9rem;color:#78350f;margin-top:4px">Все 7 боссов главы 2 повержены &mdash; вы освоили §§ 712.</div></div>';
} else {
cs.innerHTML = 'Побеждено боссов: <b>' + won + ' / ' + BOSSES.length + '</b>. Победите всех, чтобы получить титул.';
}
}
refreshCert();
}
</script>
<script>
/* ============================================================
§ 7 — КВАДРАТНЫЕ УРАВНЕНИЯ. НЕПОЛНЫЕ
============================================================ */
function buildP7(){
const box = document.getElementById('p7-body');
let html = '';
/* Card: повторение */
html += makeCard('repeat','Повторение',null, `
<ul style="margin-left:18px;line-height:1.7">
<li><b>Линейное уравнение</b> $ax+b=0$: при $a \\neq 0$ один корень $x=-b/a$.</li>
<li><b>Раскрытие скобок:</b> $(a+b)^2 = a^2+2ab+b^2$, $(a-b)^2=a^2-2ab+b^2$.</li>
<li><b>Свойство нуля:</b> произведение равно нулю $\\Leftrightarrow$ хотя бы один множитель равен нулю.</li>
<li><b>Квадратный корень:</b> $\\sqrt{a}$ определён только для $a \\geq 0$.</li>
</ul>`);
/* Card: теория */
html += makeCard('theory','Что такое квадратное уравнение','7.1',`
<p style="margin-bottom:8px"><b>Определение.</b> Уравнение вида $ax^2 + bx + c = 0$, где $a \\neq 0$, называется <b>квадратным</b>. Числа $a$, $b$, $c$ — <b>коэффициенты</b>:</p>
<ul style="margin-left:18px;line-height:1.7">
<li>$a$ — старший (первый) коэффициент;</li>
<li>$b$ — второй коэффициент;</li>
<li>$c$ — свободный член.</li>
</ul>
<p style="margin-top:8px">Если $a=1$, уравнение называют <b>приведённым</b>. Если хотя бы один из коэффициентов $b$ или $c$ равен нулю — <b>неполным</b>.</p>`);
/* Card: правило */
html += makeCard('rule','Три типа неполных уравнений','7.2',`
<table class="tbl" style="width:100%;border-collapse:collapse;font-size:.95rem">
<thead><tr style="background:var(--sec-acc-soft,var(--pri-soft))"><th style="padding:8px;text-align:left">Вид</th><th style="padding:8px;text-align:left">Метод</th><th style="padding:8px;text-align:left">Корни</th></tr></thead>
<tbody>
<tr><td style="padding:8px;border-bottom:1px solid var(--border)">$ax^2 + bx = 0$</td><td style="padding:8px;border-bottom:1px solid var(--border)">$x(ax+b)=0$</td><td style="padding:8px;border-bottom:1px solid var(--border)">$x_1=0,\\ x_2=-b/a$</td></tr>
<tr><td style="padding:8px;border-bottom:1px solid var(--border)">$ax^2 + c = 0$</td><td style="padding:8px;border-bottom:1px solid var(--border)">$x^2 = -c/a$</td><td style="padding:8px;border-bottom:1px solid var(--border)">$x = \\pm\\sqrt{-c/a}$ или нет</td></tr>
<tr><td style="padding:8px">$ax^2 = 0$</td><td style="padding:8px">очевидно</td><td style="padding:8px">$x = 0$</td></tr>
</tbody>
</table>`);
/* Card: алгоритм */
html += makeCard('algo','Универсальная стратегия',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Привести уравнение к виду $ax^2+bx+c=0$.</li>
<li>Определить, какие коэффициенты равны нулю.</li>
<li>Выбрать метод по таблице выше.</li>
<li>Проверить ответ подстановкой.</li>
</ol>`);
/* Card: пример */
html += makeCard('example','Решим вместе','7.3',`
<p><b>Пример 1.</b> $3x^2 - 12x = 0$. Выносим $x$ за скобки: $x(3x - 12) = 0$. Откуда $x_1 = 0$ или $3x-12 = 0 \\Rightarrow x_2 = 4$. Ответ: $\\{0;\\ 4\\}$.</p>
<p style="margin-top:6px"><b>Пример 2.</b> $2x^2 - 18 = 0$. Получаем $x^2 = 9 \\Rightarrow x = \\pm 3$.</p>
<p style="margin-top:6px"><b>Пример 3.</b> $5x^2 + 20 = 0 \\Rightarrow x^2 = -4$. Корней нет.</p>`);
/* ===== INTERACTIVE 1: Конструктор уравнения ===== */
html += widget('Конструктор квадратного уравнения','INTERACT 1','Перетягивайте слайдеры $a$, $b$, $c$. Смотрите, как уравнение оживает и меняет тип.', `
<div class="sliders">
<label>$a$ = <b id="p7c-a-val">1</b><input type="range" min="-5" max="5" step="1" value="1" id="p7c-a"></label>
<label>$b$ = <b id="p7c-b-val">0</b><input type="range" min="-9" max="9" step="1" value="0" id="p7c-b"></label>
<label>$c$ = <b id="p7c-c-val">-9</b><input type="range" min="-12" max="12" step="1" value="-9" id="p7c-c"></label>
</div>
<div class="eq-show" id="p7c-eq" style="text-align:center;font-size:1.4rem;margin:10px 0;padding:14px;background:var(--sec-acc-soft);border-radius:10px"></div>
<div id="p7c-type" style="text-align:center;font-weight:600;color:var(--sec-acc-d);margin-bottom:6px"></div>
<div id="p7c-roots" style="text-align:center;margin-bottom:6px"></div>`);
/* ===== INTERACTIVE 2: Drag-сортировка ===== */
html += widget('Сортировка уравнений по типу','INTERACT 2','Перетащите каждое уравнение в нужный ящик. На тач-экранах: тап по карточке, затем тап по ящику.',`
${DND_HINT_HTML}
<div id="p7s-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px">
<div class="drop-box"><h5>Полное</h5><div class="drop-items" data-cat="full"></div></div>
<div class="drop-box"><h5>Неполное</h5><div class="drop-items" data-cat="incomplete"></div></div>
<div class="drop-box"><h5>Не квадратное</h5><div class="drop-items" data-cat="notquad"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p7s-check">Проверить</button><button class="btn" id="p7s-reset">Сначала</button></div>
<div class="feedback" id="p7s-fb" style="display:none"></div>`);
/* ===== INTERACTIVE 3: Пошаговый решатель ===== */
html += widget('Пошаговый решатель неполных','INTERACT 3','Выберите тип, нажимайте «Дальше» и наблюдайте за решением пошагово.',`
<div class="pipe-tabs" style="display:flex;gap:6px;margin-bottom:10px">
<button class="btn small p7p-tab active" data-tp="bc0">$ax^2+bx=0$</button>
<button class="btn small p7p-tab" data-tp="ac0">$ax^2+c=0$</button>
</div>
<div id="p7p-stage" style="padding:14px;background:var(--card-soft);border-radius:10px;min-height:160px;font-size:1.05rem"></div>
<div class="actions" style="margin-top:10px"><button class="btn primary" id="p7p-next">Дальше</button><button class="btn" id="p7p-reset">Сначала</button></div>`);
/* ===== INTERACTIVE 4: Задача про страницу книги ===== */
html += widget('Задача про страницу книги','INTERACT 4','Длина страницы на 4 см больше ширины, а площадь — 165 см². Найдите ширину.',`
<p style="margin-bottom:10px">Пусть ширина $= x$, тогда длина $= x+4$. Уравнение: $x(x+4) = 165 \\Rightarrow x^2 + 4x - 165 = 0$.</p>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<label>Ваш ответ (ширина, см): <input type="number" id="p7b-inp" style="width:80px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<button class="btn primary" id="p7b-check">Проверить</button>
</div>
<div class="feedback" id="p7b-fb" style="display:none;margin-top:10px"></div>
<svg id="p7b-svg" viewBox="0 0 320 200" style="width:100%;max-width:320px;margin-top:12px;display:block">
<rect x="40" y="40" width="240" height="120" fill="var(--sec-acc-soft)" stroke="var(--sec-acc)" stroke-width="2"/>
<text x="160" y="32" text-anchor="middle" font-size="14" fill="var(--sec-acc-d)">длина = x + 4</text>
<text x="22" y="105" font-size="14" fill="var(--sec-acc-d)" transform="rotate(-90 22 105)">x</text>
<text x="160" y="108" text-anchor="middle" font-size="20" fill="var(--ink)" id="p7b-area">S = 165</text>
</svg>`);
/* ===== INTERACTIVE 5: Тренажёр 10 неполных ===== */
html += widget('Тренажёр: 10 неполных','INTERACT 5','Решайте устно. Ответ — большее значение корня (через запятую: если корней два — оба, по возрастанию).',`
<div class="score-display"><span>Задача <b id="p7t-i">1</b> / 10</span><span>Очки: <b id="p7t-score">0</b></span><span>Время: <b id="p7t-time">0</b> c</span></div>
<div id="p7t-task" style="font-size:1.4rem;text-align:center;padding:18px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap">
<input type="text" id="p7t-inp" placeholder="например 0; 4 или -3; 3 или нет" style="width:240px;padding:8px;border:1.5px solid var(--border);border-radius:8px">
<button class="btn primary" id="p7t-go">Ответ</button>
<button class="btn" id="p7t-skip">Пропустить</button>
</div>
<div class="feedback" id="p7t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p7t-start" style="margin-top:10px">Начать</button>`);
/* ===== INTERACTIVE 6: Есть ли корни у ax²+c=0 ===== */
html += widget('«Имеет ли корни?» — $ax^2 + c = 0$','INTERACT 6','Быстрый тренажёр на знаки. Нужно сообразить, имеет ли уравнение корни.',`
<div class="score-display"><span>Раунд <b id="p7r-i">1</b> / 8</span><span>Правильно: <b id="p7r-score">0</b></span></div>
<div id="p7r-task" style="font-size:1.5rem;text-align:center;padding:18px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;justify-content:center">
<button class="btn primary" id="p7r-yes">Имеет корни</button>
<button class="btn" id="p7r-no">Корней нет</button>
</div>
<div class="feedback" id="p7r-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p7r-start" style="margin-top:10px">Начать</button>`);
/* ===== Устно / Класс / Дом ===== */
html += makeCard('oral','Устные вопросы',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Какое уравнение называется квадратным? Какое — неполным?</li>
<li>Может ли коэффициент $a$ быть равен нулю? Почему?</li>
<li>Сколько корней может быть у уравнения $ax^2+c=0$?</li>
<li>Назовите коэффициенты уравнения $5x^2 - 2x + 7 = 0$.</li>
</ol>`);
html += makeCard('class','Класс — решите в тетради',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$4x^2 - 9 = 0$</li>
<li>$3x^2 + 6x = 0$</li>
<li>$x^2 + 25 = 0$</li>
<li>$2x^2 = 50$</li>
<li>$x(x-7) = 0$</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$9x^2 - 16 = 0$</li>
<li>$5x^2 + 10x = 0$</li>
<li>$x^2 + 4 = 0$</li>
<li>Длина прямоугольника на 3 см больше ширины, а площадь равна 54 см². Найдите стороны.</li>
</ol>`);
html += secNav(null,'p8');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* ===== INIT INTERACTIVE 1 ===== */
(function initConstructor(){
const aE = document.getElementById('p7c-a'), bE = document.getElementById('p7c-b'), cE = document.getElementById('p7c-c');
const eq = document.getElementById('p7c-eq'), tp = document.getElementById('p7c-type'), rt = document.getElementById('p7c-roots');
let done = false;
function termA(a){
if(a === 0) return '';
if(a === 1) return 'x^2';
if(a === -1) return '-x^2';
return a + 'x^2';
}
function termB(b){
if(b === 0) return '';
const s = b > 0 ? ' + ' : ' - ';
const v = Math.abs(b);
return s + (v === 1 ? '' : v) + 'x';
}
function termC(c){
if(c === 0) return '';
const s = c > 0 ? ' + ' : ' - ';
return s + Math.abs(c);
}
function refresh(){
const a = +aE.value, b = +bE.value, c = +cE.value;
document.getElementById('p7c-a-val').textContent = a;
document.getElementById('p7c-b-val').textContent = b;
document.getElementById('p7c-c-val').textContent = c;
if(a === 0){ eq.innerHTML = '$' + (termB(b) || '0') + termC(c) + ' = 0$ <span style="color:var(--bad);font-size:.85rem">не квадратное!</span>'; tp.textContent = 'Не квадратное (a=0)'; rt.textContent = ''; renderMath(eq); return; }
let s = termA(a) + termB(b) + termC(c) + ' = 0';
eq.innerHTML = '$' + s + '$';
let label = '';
if(b === 0 && c === 0) label = 'Неполное: $ax^2=0$ · корень $x=0$';
else if(b === 0) label = 'Неполное: $ax^2+c=0$';
else if(c === 0) label = 'Неполное: $ax^2+bx=0$ · корни $x_1=0,\\ x_2=' + fmt(-b/a) + '$';
else label = 'Полное квадратное · решаем через дискриминант';
tp.innerHTML = label;
let rs = '';
if(b === 0 && c === 0) rs = '$x=0$';
else if(c === 0) rs = '$x_1=0,\\ x_2=' + fmt(-b/a) + '$';
else if(b === 0){
const v = -c/a;
if(v > 0){ const r = Math.sqrt(v); rs = '$x = \\pm' + fmt(r) + '$'; }
else if(v === 0) rs = '$x=0$';
else rs = 'корней нет';
} else {
const D = b*b - 4*a*c;
if(D > 0){ const r1 = (-b - Math.sqrt(D))/(2*a), r2 = (-b + Math.sqrt(D))/(2*a); rs = '$x_1=' + fmt(r1) + ',\\ x_2=' + fmt(r2) + '$'; }
else if(D === 0) rs = '$x=' + fmt(-b/(2*a)) + '$';
else rs = 'корней нет';
}
rt.innerHTML = rs;
renderMath(eq); renderMath(tp); renderMath(rt);
if(!done){ done = true; setTimeout(()=>{ achievement('p7_constr'); bumpProgress('p7', 12); }, 300); }
}
aE.addEventListener('input', refresh); bE.addEventListener('input', refresh); cE.addEventListener('input', refresh);
refresh();
})();
/* ===== INIT INTERACTIVE 2 ===== */
(function initSort(){
const items = [
{ id:1, html:'$2x^2 + 3x - 5 = 0$', cat:'full' },
{ id:2, html:'$x^2 - 16 = 0$', cat:'incomplete' },
{ id:3, html:'$3x^2 + 6x = 0$', cat:'incomplete' },
{ id:4, html:'$x + 5 = 0$', cat:'notquad' },
{ id:5, html:'$5x^2 - 2x + 1 = 0$', cat:'full' },
{ id:6, html:'$7x^2 = 0$', cat:'incomplete' },
{ id:7, html:'$x^3 - x = 0$', cat:'notquad' },
{ id:8, html:'$x^2 + x + 1 = 0$', cat:'full' },
];
const sorter = setupSorter({ poolId:'p7s-pool', cats:['full','incomplete','notquad'], items, scopeSelector:'#p7-body' });
document.getElementById('p7s-check').addEventListener('click', ()=>{
const total = items.length;
const placedCount = Object.keys(sorter.placed).length;
const fb = document.getElementById('p7s-fb');
fb.style.display = 'block';
if(placedCount < total){ feedback(fb, false, '&#9888; Разложите ВСЕ уравнения по ящикам.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === total){ feedback(fb, true, '&#10003; Идеально! Все ' + total + ' верно.'); achievement('p7_sort'); bumpProgress('p7', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + total + '. Попробуйте перепроверить.');
});
document.getElementById('p7s-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p7s-fb').style.display='none'; });
})();
/* ===== INIT INTERACTIVE 3 ===== */
(function initPipeline(){
const stages = {
bc0: [
'<b>Дано:</b> $2x^2 - 6x = 0$',
'<b>Шаг 1:</b> Вынесем $x$ за скобки: $x(2x - 6) = 0$',
'<b>Шаг 2:</b> Произведение равно нулю $\\Leftrightarrow$ один из множителей равен нулю.',
'<b>Шаг 3:</b> $x = 0$ или $2x - 6 = 0$',
'<b>Шаг 4:</b> $x_1 = 0,\\ x_2 = 3$',
'<b>Ответ:</b> $\\{0;\\ 3\\}$',
],
ac0: [
'<b>Дано:</b> $3x^2 - 27 = 0$',
'<b>Шаг 1:</b> Перенесём $-27$ вправо: $3x^2 = 27$',
'<b>Шаг 2:</b> Разделим на $3$: $x^2 = 9$',
'<b>Шаг 3:</b> Возьмём квадратный корень: $x = \\pm\\sqrt{9}$',
'<b>Шаг 4:</b> $x = \\pm 3$',
'<b>Ответ:</b> $\\{-3;\\ 3\\}$',
],
};
let mode = 'bc0', idx = 0;
const stage = document.getElementById('p7p-stage');
let solved = false;
function show(){
const arr = stages[mode];
stage.innerHTML = arr.slice(0, idx + 1).map(s => '<p style="margin:6px 0">' + s + '</p>').join('');
renderMath(stage);
if(idx >= arr.length - 1 && !solved){ solved = true; achievement('p7_pipeline'); bumpProgress('p7', 12); }
}
document.querySelectorAll('.p7p-tab').forEach(t=>{
t.addEventListener('click', ()=>{ document.querySelectorAll('.p7p-tab').forEach(x=>x.classList.remove('active')); t.classList.add('active'); mode = t.dataset.tp; idx = 0; show(); });
});
document.getElementById('p7p-next').addEventListener('click', ()=>{ if(idx < stages[mode].length - 1) idx++; show(); });
document.getElementById('p7p-reset').addEventListener('click', ()=>{ idx = 0; show(); });
show();
})();
/* ===== INIT INTERACTIVE 4 ===== */
(function initBook(){
document.getElementById('p7b-check').addEventListener('click', ()=>{
const v = +document.getElementById('p7b-inp').value;
const fb = document.getElementById('p7b-fb');
fb.style.display = 'block';
if(Math.abs(v - 11) < 1e-9){ feedback(fb, true, '&#10003; Верно! Ширина = 11 см, длина = 15 см. $11 \\cdot 15 = 165$.'); renderMath(fb); achievement('p7_book'); bumpProgress('p7', 14); confetti(); }
else feedback(fb, false, 'Не то. $x^2+4x-165=0$, корни $-15$ и $11$. Ширина не может быть отрицательной.');
});
})();
/* ===== INIT INTERACTIVE 5 ===== */
(function initTrain(){
function gen(){
const t = Math.floor(Math.random()*3);
if(t === 0){
const a = 1 + Math.floor(Math.random()*4);
const r2 = 1 + Math.floor(Math.random()*8);
return { eq: a + 'x^2 - ' + (a*r2) + 'x = 0', ans: [0, r2] };
}
if(t === 1){
const a = 1 + Math.floor(Math.random()*3);
const r = 1 + Math.floor(Math.random()*7);
return { eq: a + 'x^2 - ' + (a*r*r) + ' = 0', ans: [-r, r] };
}
const a = 1 + Math.floor(Math.random()*3);
const c = 1 + Math.floor(Math.random()*15);
return { eq: a + 'x^2 + ' + c + ' = 0', ans: null };
}
let cur = null, i = 1, score = 0, t0 = 0, timer = null;
function newOne(){
cur = gen();
document.getElementById('p7t-task').innerHTML = '$' + cur.eq + ' = 0$'; renderMath(document.getElementById('p7t-task'));
document.getElementById('p7t-i').textContent = i;
document.getElementById('p7t-inp').value = '';
document.getElementById('p7t-fb').style.display = 'none';
}
function parse(s){
s = s.trim().toLowerCase();
if(/нет|none/.test(s)) return null;
return s.replace(/,/g,';').split(/[;\s]+/).filter(Boolean).map(Number).sort((a,b)=>a-b);
}
function check(){
const u = parse(document.getElementById('p7t-inp').value);
const fb = document.getElementById('p7t-fb');
fb.style.display = 'block';
let ok = false;
if(cur.ans === null){ ok = u === null; }
else if(u !== null){ const a = [...cur.ans].sort((a,b)=>a-b); ok = a.length === u.length && a.every((x,k)=>x === u[k]); }
if(ok){ score++; feedback(fb, true, '&#10003;'); } else feedback(fb, false, 'Правильно: ' + (cur.ans === null ? 'нет' : cur.ans.join('; ')));
document.getElementById('p7t-score').textContent = score;
if(i >= 10){
setTimeout(()=>{ feedback(fb, score >= 7, 'Итог: ' + score + '/10 за ' + (((Date.now() - t0)/1000)|0) + ' c'); if(score >= 7){ achievement('p7_train'); bumpProgress('p7', 16); confetti(); } clearInterval(timer); }, 500);
} else { i++; setTimeout(newOne, 700); }
}
document.getElementById('p7t-start').addEventListener('click', ()=>{ i = 1; score = 0; document.getElementById('p7t-score').textContent = 0; t0 = Date.now(); if(timer) clearInterval(timer); timer = setInterval(()=>{ document.getElementById('p7t-time').textContent = ((Date.now() - t0)/1000)|0; }, 200); newOne(); });
document.getElementById('p7t-go').addEventListener('click', check);
document.getElementById('p7t-inp').addEventListener('keyup', e=>{ if(e.key === 'Enter') check(); });
document.getElementById('p7t-skip').addEventListener('click', ()=>{ if(i < 10){ i++; newOne(); } });
})();
/* ===== INIT INTERACTIVE 6 ===== */
(function initRoots(){
let i = 1, score = 0, cur = null;
function gen(){
const a = (Math.random() < 0.5 ? 1 : -1) * (1 + Math.floor(Math.random()*5));
const c = (Math.random() < 0.5 ? 1 : -1) * (1 + Math.floor(Math.random()*16));
const v = -c/a;
return { a, c, ok: v > 0, eq: (a === 1 ? '' : (a === -1 ? '-' : a)) + 'x^2 ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c)) + ' = 0' };
}
function newOne(){
cur = gen();
document.getElementById('p7r-i').textContent = i;
document.getElementById('p7r-task').innerHTML = '$' + cur.eq + '$'; renderMath(document.getElementById('p7r-task'));
document.getElementById('p7r-fb').style.display = 'none';
}
function answer(yes){
const fb = document.getElementById('p7r-fb');
fb.style.display = 'block';
const ok = (cur.ok === yes);
if(ok){ score++; feedback(fb, true, '&#10003; Точно. $x^2 = ' + fmt(-cur.c/cur.a) + '$'); }
else feedback(fb, false, 'Нет. $x^2 = ' + fmt(-cur.c/cur.a) + '$ — ' + (cur.ok ? 'корни есть' : 'корней нет'));
renderMath(fb);
document.getElementById('p7r-score').textContent = score;
if(i >= 8){ setTimeout(()=>{ feedback(fb, score >= 6, 'Итог: ' + score + '/8'); if(score >= 6){ achievement('p7_roots'); bumpProgress('p7', 14); confetti(); } }, 600); }
else { i++; setTimeout(newOne, 800); }
}
document.getElementById('p7r-start').addEventListener('click', ()=>{ i = 1; score = 0; document.getElementById('p7r-score').textContent = 0; newOne(); });
document.getElementById('p7r-yes').addEventListener('click', ()=>answer(true));
document.getElementById('p7r-no').addEventListener('click', ()=>answer(false));
})();
}
</script>
<script>
/* ============================================================
§ 8 — ФОРМУЛЫ КОРНЕЙ. ДИСКРИМИНАНТ
============================================================ */
function buildP8(){
const box = document.getElementById('p8-body');
let html = '';
html += makeCard('repeat','Повторение из § 7',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Квадратное уравнение: $ax^2+bx+c=0,\\ a \\neq 0$.</li>
<li>Неполные уравнения решали без дискриминанта — через вынесение или прямой корень.</li>
<li>Свойство квадратного корня: $\\sqrt{D}$ существует только при $D \\geq 0$.</li>
</ul>`);
html += makeCard('theory','Универсальная формула','8.1',`
<p>Для уравнения $ax^2 + bx + c = 0$, $a \\neq 0$, выделим полный квадрат:</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:12px;margin:8px 0;text-align:center">$$a\\left(x + \\dfrac{b}{2a}\\right)^2 = \\dfrac{b^2 - 4ac}{4a}$$</div>
<p>Выражение $\\boxed{D = b^2 - 4ac}$ называется <b>дискриминантом</b>. От его знака зависит, есть ли корни.</p>`);
html += makeCard('rule','Главные формулы','8.2',`
<div style="background:var(--card-soft);border:1.5px solid var(--sec-acc);border-radius:10px;padding:12px;margin:6px 0;text-align:center;font-size:1.1rem">$$D = b^2 - 4ac \\qquad x_{1,2} = \\dfrac{-b \\pm \\sqrt{D}}{2a}$$</div>
<table class="tbl" style="width:100%;border-collapse:collapse;margin-top:8px">
<thead><tr style="background:var(--sec-acc-soft)"><th style="padding:8px;text-align:left">Знак $D$</th><th style="padding:8px;text-align:left">Сколько корней</th><th style="padding:8px;text-align:left">Формула</th></tr></thead>
<tbody>
<tr><td style="padding:8px;border-bottom:1px solid var(--border)">$D > 0$</td><td style="padding:8px;border-bottom:1px solid var(--border)">Два различных</td><td style="padding:8px;border-bottom:1px solid var(--border)">$x_{1,2} = \\dfrac{-b \\pm \\sqrt{D}}{2a}$</td></tr>
<tr><td style="padding:8px;border-bottom:1px solid var(--border)">$D = 0$</td><td style="padding:8px;border-bottom:1px solid var(--border)">Один (кратный)</td><td style="padding:8px;border-bottom:1px solid var(--border)">$x = \\dfrac{-b}{2a}$</td></tr>
<tr><td style="padding:8px">$D < 0$</td><td style="padding:8px">Корней нет</td><td style="padding:8px">—</td></tr>
</tbody>
</table>`);
html += makeCard('algo','Алгоритм решения','8.3',`
<ol style="margin-left:18px;line-height:1.8">
<li>Запишите уравнение в виде $ax^2+bx+c=0$, $a \\neq 0$.</li>
<li>Выпишите коэффициенты $a$, $b$, $c$ со знаками.</li>
<li>Вычислите $D = b^2 - 4ac$.</li>
<li>Сравните $D$ с нулём — определите число корней.</li>
<li>Если $D \\geq 0$, по формуле найдите $x_1$, $x_2$.</li>
</ol>`);
html += makeCard('example','Пример решения','8.4',`
<p><b>Решим:</b> $2x^2 - 5x + 2 = 0$. Здесь $a=2$, $b=-5$, $c=2$.</p>
<p>$D = (-5)^2 - 4 \\cdot 2 \\cdot 2 = 25 - 16 = 9$.</p>
<p>$\\sqrt{D} = 3$. $x_{1,2} = \\dfrac{5 \\pm 3}{4}$, поэтому $x_1 = \\dfrac{1}{2},\\ x_2 = 2$.</p>
<p><b>Проверка:</b> $2 \\cdot 4 - 10 + 2 = 0$. &#10003;</p>`);
/* INTERACTIVE 1: калькулятор дискриминанта */
html += widget('Калькулятор дискриминанта','INTERACT 1','Введите $a$, $b$, $c$ — мгновенно увидите $D$ и корни. Идёт пошаговое вычисление.',`
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:10px">
<label>$a$ = <input type="number" id="p8d-a" value="1" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>$b$ = <input type="number" id="p8d-b" value="-5" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>$c$ = <input type="number" id="p8d-c" value="6" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
</div>
<div id="p8d-out" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;line-height:1.8"></div>`);
/* INTERACTIVE 2: парабола */
html += widget('Парабола: $y = ax^2+bx+c$','INTERACT 2','Двигайте слайдеры — смотрите, как меняется график и сколько раз он пересекает ось OX.',`
<div class="sliders">
<label>$a$ = <b id="p8g-a-val">1</b><input type="range" min="-3" max="3" step="0.1" value="1" id="p8g-a"></label>
<label>$b$ = <b id="p8g-b-val">0</b><input type="range" min="-6" max="6" step="0.2" value="0" id="p8g-b"></label>
<label>$c$ = <b id="p8g-c-val">-4</b><input type="range" min="-8" max="8" step="0.2" value="-4" id="p8g-c"></label>
</div>
<svg id="p8g-svg" viewBox="-10 -10 220 160" style="width:100%;max-width:480px;display:block;margin:10px auto;background:#fafafa;border:1px solid var(--border);border-radius:8px"></svg>
<div id="p8g-info" style="text-align:center;font-weight:600;color:var(--sec-acc-d)"></div>`);
/* INTERACTIVE 3: 3 случая */
html += widget('Три случая дискриминанта','INTERACT 3','Кликните по карточке — увидите пример и график для каждого случая.',`
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin-bottom:12px">
<div class="case-card" data-case="pos"><h5>$D > 0$</h5><div>Два корня</div></div>
<div class="case-card" data-case="zero"><h5>$D = 0$</h5><div>Один корень</div></div>
<div class="case-card" data-case="neg"><h5>$D < 0$</h5><div>Корней нет</div></div>
</div>
<div id="p8c-detail" style="padding:14px;background:var(--card-soft);border-radius:10px;min-height:140px;display:flex;gap:14px;flex-wrap:wrap;align-items:center"></div>`);
/* INTERACTIVE 4: тренажёр D */
html += widget('Тренажёр: «Сколько корней?»','INTERACT 4','По данному уравнению вычислите $D$ устно и ответьте: сколько корней?',`
<div class="score-display"><span>Задача <b id="p8t-i">1</b> / 10</span><span>Очки: <b id="p8t-score">0</b></span></div>
<div id="p8t-task" style="font-size:1.3rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div id="p8t-d-hint" style="text-align:center;color:var(--muted);margin-bottom:8px;font-size:.85rem"></div>
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
<button class="btn" data-n="2" id="p8t-2">Два</button>
<button class="btn" data-n="1" id="p8t-1">Один</button>
<button class="btn" data-n="0" id="p8t-0">Корней нет</button>
</div>
<div class="feedback" id="p8t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p8t-start" style="margin-top:10px">Начать</button>`);
/* INTERACTIVE 5: пошаговый */
html += widget('Пошаговый решатель','INTERACT 5','Введите $a$, $b$, $c$ и нажимайте «Дальше» — каждый шаг появляется отдельно.',`
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:10px">
<label>$a$ = <input type="number" id="p8s-a" value="1" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>$b$ = <input type="number" id="p8s-b" value="-7" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<label>$c$ = <input type="number" id="p8s-c" value="12" style="width:70px;padding:6px;border:1.5px solid var(--border);border-radius:6px"></label>
<button class="btn primary" id="p8s-go">Старт</button>
<button class="btn" id="p8s-next" style="display:none">Дальше</button>
<button class="btn" id="p8s-reset" style="display:none">Сначала</button>
</div>
<div id="p8s-stage" style="padding:14px;background:var(--card-soft);border-radius:10px;min-height:100px"></div>`);
/* INTERACTIVE 6: угадай знак D по графику */
html += widget('«Угадай знак D» — только по графику','INTERACT 6','По параболе определите знак дискриминанта.',`
<div class="score-display"><span>Раунд <b id="p8r-i">1</b> / 8</span><span>Правильно: <b id="p8r-score">0</b></span></div>
<svg id="p8r-svg" viewBox="-10 -10 220 160" style="width:100%;max-width:380px;display:block;margin:10px auto;background:#fafafa;border:1px solid var(--border);border-radius:8px"></svg>
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
<button class="btn" data-d="pos" id="p8r-pos">$D > 0$</button>
<button class="btn" data-d="zero" id="p8r-zero">$D = 0$</button>
<button class="btn" data-d="neg" id="p8r-neg">$D < 0$</button>
</div>
<div class="feedback" id="p8r-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p8r-start" style="margin-top:10px">Начать</button>`);
/* Устно/класс/дом */
html += makeCard('oral','Устные вопросы',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Что такое дискриминант? Какова его формула?</li>
<li>При каком знаке $D$ уравнение имеет два корня? Один? Ни одного?</li>
<li>Как изменится $D$, если уравнение умножить на $-1$?</li>
<li>Найдите $D$ для $x^2 + 2x + 1 = 0$.</li>
</ol>`);
html += makeCard('class','Класс — решите в тетради',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$x^2 - 7x + 12 = 0$</li>
<li>$2x^2 + 3x - 2 = 0$</li>
<li>$x^2 - 6x + 9 = 0$</li>
<li>$3x^2 + x + 1 = 0$</li>
<li>$5x^2 - 4x - 1 = 0$</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$x^2 + 5x + 6 = 0$</li>
<li>$2x^2 - 7x + 3 = 0$</li>
<li>$4x^2 - 4x + 1 = 0$</li>
<li>$x^2 + x + 2 = 0$</li>
<li>При каких $m$ уравнение $x^2 - 4x + m = 0$ имеет один корень?</li>
</ol>`);
html += secNav('p7','p9');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT INTERACTIVE 1 */
(function initDisc(){
const aE = document.getElementById('p8d-a'), bE = document.getElementById('p8d-b'), cE = document.getElementById('p8d-c');
const out = document.getElementById('p8d-out');
let done = false;
function refresh(){
const a = +aE.value, b = +bE.value, c = +cE.value;
if(!isFinite(a) || a === 0){ out.innerHTML = 'Введите $a \\neq 0$ — иначе уравнение не квадратное.'; renderMath(out); return; }
const D = b*b - 4*a*c;
let s = '<div><b>Уравнение:</b> $' + (a === 1 ? '' : (a === -1 ? '-' : a)) + 'x^2 ' + (b >= 0 ? '+' + b : b) + 'x ' + (c >= 0 ? '+' + c : c) + ' = 0$</div>';
s += '<div><b>Шаг 1.</b> $D = b^2 - 4ac = (' + b + ')^2 - 4 \\cdot (' + a + ') \\cdot (' + c + ') = ' + (b*b) + ' - ' + (4*a*c) + ' = ' + D + '$</div>';
if(D > 0){ const r1 = (-b - Math.sqrt(D))/(2*a), r2 = (-b + Math.sqrt(D))/(2*a); s += '<div><b>Шаг 2.</b> $D > 0 \\Rightarrow$ два корня:</div><div>$x_{1,2} = \\dfrac{-(' + b + ') \\pm \\sqrt{' + D + '}}{2 \\cdot ' + a + '}$</div><div><b>Ответ:</b> $x_1 = ' + fmt(r1) + ',\\ x_2 = ' + fmt(r2) + '$</div>'; }
else if(D === 0){ s += '<div><b>Шаг 2.</b> $D = 0 \\Rightarrow$ один корень: $x = ' + fmt(-b/(2*a)) + '$</div>'; }
else s += '<div><b>Шаг 2.</b> $D < 0 \\Rightarrow$ корней нет.</div>';
out.innerHTML = s; renderMath(out);
if(!done){ done = true; setTimeout(()=>{ achievement('p8_disc'); bumpProgress('p8', 12); }, 300); }
}
[aE,bE,cE].forEach(e => e.addEventListener('input', refresh));
refresh();
})();
/* INIT INTERACTIVE 2 */
(function initParabola(){
const aE = document.getElementById('p8g-a'), bE = document.getElementById('p8g-b'), cE = document.getElementById('p8g-c');
const svg = document.getElementById('p8g-svg'), info = document.getElementById('p8g-info');
let done = false;
const W = 200, H = 140, x0 = W/2, y0 = H/2, sx = 14, sy = 14;
function refresh(){
const a = +aE.value, b = +bE.value, c = +cE.value;
document.getElementById('p8g-a-val').textContent = a.toFixed(1);
document.getElementById('p8g-b-val').textContent = b.toFixed(1);
document.getElementById('p8g-c-val').textContent = c.toFixed(1);
let path = '';
for(let i = 0; i <= 200; i++){
const x = -7 + i * 14 / 200;
const y = a*x*x + b*x + c;
const cx = x0 + x*sx, cy = y0 - y*sy;
path += (i === 0 ? 'M' : 'L') + cx.toFixed(2) + ',' + cy.toFixed(2) + ' ';
}
let svgC = '<line x1="0" y1="' + y0 + '" x2="' + W + '" y2="' + y0 + '" stroke="#999" stroke-width="0.5"/>';
svgC += '<line x1="' + x0 + '" y1="0" x2="' + x0 + '" y2="' + H + '" stroke="#999" stroke-width="0.5"/>';
svgC += '<path d="' + path + '" fill="none" stroke="var(--sec-acc)" stroke-width="1.8"/>';
if(a !== 0){
const D = b*b - 4*a*c;
if(D >= 0){
const r1 = (-b - Math.sqrt(D))/(2*a), r2 = (-b + Math.sqrt(D))/(2*a);
[r1, r2].forEach(r=>{ svgC += '<circle cx="' + (x0 + r*sx).toFixed(2) + '" cy="' + y0 + '" r="3" fill="#e91e63"/>'; });
info.innerHTML = 'D = ' + D.toFixed(2) + ' · ' + (D > 0 ? 'два корня: ' + fmt(r1) + ', ' + fmt(r2) : 'один корень: ' + fmt(r1));
} else info.innerHTML = 'D = ' + D.toFixed(2) + ' · корней нет (парабола не пересекает OX)';
}
svg.innerHTML = svgC;
if(!done){ done = true; setTimeout(()=>{ achievement('p8_parab'); bumpProgress('p8', 12); }, 400); }
}
[aE,bE,cE].forEach(e => e.addEventListener('input', refresh));
refresh();
})();
/* INIT INTERACTIVE 3 */
(function initCases(){
const data = {
pos: { eq: 'x^2 - 5x + 6 = 0', D: 1, roots: 'x_1 = 2,\\ x_2 = 3', svg: parab(1, -5, 6) },
zero:{ eq: 'x^2 - 4x + 4 = 0', D: 0, roots: 'x = 2', svg: parab(1, -4, 4) },
neg: { eq: 'x^2 + x + 1 = 0', D:-3, roots: 'корней нет', svg: parab(1, 1, 1) },
};
function parab(a, b, c){
const W = 180, H = 130, x0 = W/2, y0 = H/2, sx = 11, sy = 11;
let path = '';
for(let i = 0; i <= 200; i++){
const x = -8 + i * 16 / 200;
const y = a*x*x + b*x + c;
const cx = x0 + x*sx, cy = y0 - y*sy;
if(cy >= -20 && cy <= H + 20) path += (path ? 'L' : 'M') + cx.toFixed(2) + ',' + cy.toFixed(2) + ' ';
}
let s = '<svg viewBox="0 0 ' + W + ' ' + H + '" style="width:180px;height:130px;background:#fff;border:1px solid #ddd;border-radius:6px">';
s += '<line x1="0" y1="' + y0 + '" x2="' + W + '" y2="' + y0 + '" stroke="#999" stroke-width="0.5"/>';
s += '<line x1="' + x0 + '" y1="0" x2="' + x0 + '" y2="' + H + '" stroke="#999" stroke-width="0.5"/>';
s += '<path d="' + path + '" fill="none" stroke="#e91e63" stroke-width="1.5"/></svg>';
return s;
}
const detail = document.getElementById('p8c-detail');
let seen = new Set();
document.querySelectorAll('.case-card').forEach(c=>{
c.addEventListener('click', ()=>{
document.querySelectorAll('.case-card').forEach(x=>x.classList.remove('active'));
c.classList.add('active');
const d = data[c.dataset.case];
detail.innerHTML = '<div style="flex:1;min-width:180px"><div><b>Пример:</b> $' + d.eq + '$</div><div><b>$D$ =</b> ' + d.D + '</div><div><b>Корни:</b> $' + d.roots + '$</div></div><div>' + d.svg + '</div>';
renderMath(detail);
seen.add(c.dataset.case);
if(seen.size === 3){ achievement('p8_cases'); bumpProgress('p8', 12); }
});
});
document.querySelector('.case-card').click();
})();
/* INIT INTERACTIVE 4 */
(function initTrain(){
let cur = null, i = 1, score = 0;
function gen(){
const t = Math.floor(Math.random()*3);
if(t === 0){
const r1 = -3 + Math.floor(Math.random()*7), r2 = -3 + Math.floor(Math.random()*7);
if(r1 === r2) return gen();
return { a:1, b:-(r1+r2), c:r1*r2, n:2 };
}
if(t === 1){
const r = -4 + Math.floor(Math.random()*9);
return { a:1, b:-2*r, c:r*r, n:1 };
}
const b = -3 + Math.floor(Math.random()*7), c = 3 + Math.floor(Math.random()*8);
return { a:1, b, c, n:0 };
}
function newOne(){
cur = gen();
const a = cur.a, b = cur.b, c = cur.c;
const txt = 'x^2 ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + 'x ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c)) + ' = 0';
document.getElementById('p8t-task').innerHTML = '$' + txt + '$';
document.getElementById('p8t-i').textContent = i;
document.getElementById('p8t-d-hint').innerHTML = 'Подскажу: $D = b^2 - 4ac$';
renderMath(document.getElementById('p8t-task'));
renderMath(document.getElementById('p8t-d-hint'));
document.getElementById('p8t-fb').style.display = 'none';
}
function answer(n){
const D = cur.b*cur.b - 4*cur.a*cur.c;
const fb = document.getElementById('p8t-fb');
fb.style.display = 'block';
if(n === cur.n){ score++; feedback(fb, true, '&#10003; Верно! D = ' + D); }
else feedback(fb, false, 'Нет. D = ' + D + ' &rarr; ' + (cur.n === 2 ? '2 корня' : cur.n === 1 ? '1 корень' : 'корней нет'));
document.getElementById('p8t-score').textContent = score;
if(i >= 10){ setTimeout(()=>{ feedback(fb, score >= 7, 'Итог: ' + score + '/10'); if(score >= 7){ achievement('p8_disc_train'); bumpProgress('p8', 16); confetti(); } }, 600); }
else { i++; setTimeout(newOne, 800); }
}
document.getElementById('p8t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p8t-score').textContent = 0; newOne(); });
document.getElementById('p8t-2').addEventListener('click', ()=>answer(2));
document.getElementById('p8t-1').addEventListener('click', ()=>answer(1));
document.getElementById('p8t-0').addEventListener('click', ()=>answer(0));
})();
/* INIT INTERACTIVE 5 — пошаговый по одному шагу */
(function initSteps(){
const stage = document.getElementById('p8s-stage');
const goBtn = document.getElementById('p8s-go');
const nextBtn = document.getElementById('p8s-next');
const resetBtn = document.getElementById('p8s-reset');
let steps = [], idx = 0, awarded = false;
function buildSteps(a, b, c){
const D = b*b - 4*a*c;
const arr = [];
arr.push('<b>Шаг 1.</b> Выпишем коэффициенты: $a = ' + a + ',\\ b = ' + b + ',\\ c = ' + c + '$');
arr.push('<b>Шаг 2.</b> $D = b^2 - 4ac = (' + b + ')^2 - 4 \\cdot (' + a + ') \\cdot (' + c + ') = ' + (b*b) + ' - ' + (4*a*c) + ' = ' + D + '$');
arr.push('<b>Шаг 3.</b> ' + (D > 0 ? '$D > 0 \\Rightarrow$ два корня' : D === 0 ? '$D = 0 \\Rightarrow$ один корень' : '$D < 0 \\Rightarrow$ корней нет'));
if(D >= 0){
arr.push('<b>Шаг 4.</b> $\\sqrt{D} = \\sqrt{' + D + '} = ' + fmt(Math.sqrt(D)) + '$');
if(D > 0){
const r1 = (-b - Math.sqrt(D))/(2*a), r2 = (-b + Math.sqrt(D))/(2*a);
arr.push('<b>Шаг 5.</b> $x_{1,2} = \\dfrac{-b \\pm \\sqrt{D}}{2a} = \\dfrac{' + (-b) + ' \\pm ' + fmt(Math.sqrt(D)) + '}{' + (2*a) + '}$');
arr.push('<b>Шаг 6.</b> $x_1 = ' + fmt(r1) + ',\\ x_2 = ' + fmt(r2) + '$');
} else {
arr.push('<b>Шаг 5.</b> $x = \\dfrac{-b}{2a} = \\dfrac{' + (-b) + '}{' + (2*a) + '} = ' + fmt(-b/(2*a)) + '$');
}
}
arr.push('<b>Ответ:</b> ' + (D > 0 ? 'два корня' : D === 0 ? 'один корень' : 'корней нет'));
return arr;
}
function render(){
const html = steps.slice(0, idx + 1)
.map((s, k)=>`<div class="step-line" style="margin:8px 0;padding:10px 12px;background:var(--card);border-left:3px solid var(--sec-acc);border-radius:7px;animation:fadeIn .3s ease">${s}</div>`)
.join('');
stage.innerHTML = html;
renderMath(stage);
if(idx >= steps.length - 1){
nextBtn.disabled = true;
nextBtn.textContent = 'Готово';
if(!awarded){ awarded = true; achievement('p8_steps'); bumpProgress('p8', 14); confetti(); }
} else {
nextBtn.disabled = false;
nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')';
}
}
goBtn.addEventListener('click', ()=>{
const a = +document.getElementById('p8s-a').value;
const b = +document.getElementById('p8s-b').value;
const c = +document.getElementById('p8s-c').value;
if(!a){ stage.innerHTML = '<p>$a$ не может быть нулём.</p>'; renderMath(stage); return; }
steps = buildSteps(a, b, c);
idx = 0; awarded = false;
goBtn.style.display = 'none';
nextBtn.style.display = ''; resetBtn.style.display = '';
nextBtn.disabled = false;
render();
});
nextBtn.addEventListener('click', ()=>{
if(idx < steps.length - 1){ idx++; render(); }
});
resetBtn.addEventListener('click', ()=>{
idx = 0; steps = []; awarded = false;
stage.innerHTML = '';
goBtn.style.display = ''; nextBtn.style.display = 'none'; resetBtn.style.display = 'none';
});
})();
/* INIT INTERACTIVE 6 */
(function initGraph(){
let cur = null, i = 1, score = 0;
function gen(){
const t = Math.floor(Math.random()*3);
const a = Math.random() < 0.5 ? 1 : -1;
if(t === 0){
const r1 = -3 + Math.floor(Math.random()*7), r2 = -3 + Math.floor(Math.random()*7);
if(r1 === r2) return gen();
return { a, b:-a*(r1+r2), c:a*r1*r2, kind:'pos' };
}
if(t === 1){
const r = -3 + Math.floor(Math.random()*7);
return { a, b:-2*a*r, c:a*r*r, kind:'zero' };
}
const v = 1 + Math.floor(Math.random()*4);
return { a:1, b:0, c:v, kind:'neg' };
}
function draw(){
const W = 200, H = 140, x0 = W/2, y0 = H/2, sx = 14, sy = 14;
let path = '';
for(let k = 0; k <= 200; k++){
const x = -7 + k * 14 / 200;
const y = cur.a*x*x + cur.b*x + cur.c;
const cx = x0 + x*sx, cy = y0 - y*sy;
if(cy >= -40 && cy <= H + 40) path += (path ? 'L' : 'M') + cx.toFixed(2) + ',' + cy.toFixed(2) + ' ';
}
let s = '<line x1="0" y1="' + y0 + '" x2="' + W + '" y2="' + y0 + '" stroke="#999" stroke-width="0.5"/>';
s += '<line x1="' + x0 + '" y1="0" x2="' + x0 + '" y2="' + H + '" stroke="#999" stroke-width="0.5"/>';
s += '<path d="' + path + '" fill="none" stroke="#e91e63" stroke-width="1.6"/>';
document.getElementById('p8r-svg').innerHTML = s;
}
function newOne(){
cur = gen();
document.getElementById('p8r-i').textContent = i;
document.getElementById('p8r-fb').style.display = 'none';
draw();
}
function answer(k){
const fb = document.getElementById('p8r-fb');
fb.style.display = 'block';
if(k === cur.kind){ score++; feedback(fb, true, '&#10003;'); }
else feedback(fb, false, 'Нет. Правильно: ' + (cur.kind === 'pos' ? 'D > 0' : cur.kind === 'zero' ? 'D = 0' : 'D < 0'));
document.getElementById('p8r-score').textContent = score;
if(i >= 8){ setTimeout(()=>{ feedback(fb, score >= 6, 'Итог: ' + score + '/8'); if(score >= 6){ achievement('p8_graph'); bumpProgress('p8', 14); confetti(); } }, 600); }
else { i++; setTimeout(newOne, 800); }
}
document.getElementById('p8r-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p8r-score').textContent = 0; newOne(); });
document.getElementById('p8r-pos').addEventListener('click', ()=>answer('pos'));
document.getElementById('p8r-zero').addEventListener('click', ()=>answer('zero'));
document.getElementById('p8r-neg').addEventListener('click', ()=>answer('neg'));
})();
}
</script>
</body>
</html>