Files
Learn_System/frontend/textbooks/geometry_7_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

1598 lines
119 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 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>Геометрия 7 · Глава 2 · Признаки равенства треугольников</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<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>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/geom7_svg.js?v=6" defer></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#fafafa; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --muted:#64748b;
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
--pri:#059669; --pri2:#047857; --pri-soft:#d1fae5;
--acc:#10b981; --acc2:#059669; --acc-soft:#a7f3d0;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#0a0e0c; --card:#0a1310; --card-soft:#0e1a14; --text:#e0fef0; --muted:#7aa090; --border:#1a2a22}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
button,input,select,textarea{font-family:inherit;font-size:inherit}
button{cursor:pointer;border:0;background:transparent;color:inherit}
a{color:inherit;text-decoration:none}
.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
.hdr{position:relative;background:linear-gradient(110deg,#064e3b 0%,#059669 55%,#34d399 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(167,243,208,.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(220,255,235,.12);line-height:1;pointer-events: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;flex-wrap:wrap}
.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{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{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:'\25B3';position:absolute;right:-10px;top:-20px;font-size:clamp(2rem,8vw,5.5rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none;font-family:'Unbounded',sans-serif}
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px}
.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)}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(5,150,105,.32)}
.hero-progress{flex:1;min-width:200px;max-width:280px}
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
.hp-bar{height:8px;background:rgba(5,150,105,.18);border-radius:5px;overflow:hidden}
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
.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;font-family:'Unbounded',sans-serif}
.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:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(5,150,105,.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,#fef3c7)}
.sec[id="sec-p8"] { --sec-acc:#059669; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
.sec[id="sec-p9"] { --sec-acc:#0d9488; --sec-acc-d:#0f766e; --sec-acc-soft:#ccfbf1; }
.sec[id="sec-p10"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p11"]{ --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.sec[id="sec-p12"]{ --sec-acc:#c026d3; --sec-acc-d:#a21caf; --sec-acc-soft:#fae8ff; }
.sec[id="sec-p13"]{ --sec-acc:#db2777; --sec-acc-d:#9d174d; --sec-acc-soft:#fce7f3; }
.sec[id="sec-p14"]{ --sec-acc:#dc2626; --sec-acc-d:#b91c1c; --sec-acc-soft:#fee2e2; }
.sec[id="sec-final2"]{ --sec-acc:#059669; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
.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;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.55rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));line-height:1.25}
.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(5,150,105,.06);position:relative;z-index:1}
.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}
.card-icon.theory{background:#8b5cf6}.card-icon.rule{background:#ec4899}.card-icon.algo{background:#f59e0b}.card-icon.example{background:#10b981}
.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:.94rem;line-height:1.65}
.card-body p{margin-bottom:8px}
.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}
.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}
.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}
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.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))}
.tinp{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-family:'JetBrains Mono',monospace}
.tinp:focus{outline:0;border-color:var(--sec-acc,var(--pri));box-shadow:0 0 0 3px var(--sec-acc-soft,var(--pri-soft))}
.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)}
.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}}
.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(16,185,129,.18);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}
.spoiler{border:1px solid var(--border);border-radius:10px;background:var(--card);margin:10px 0;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:'\2212'}
.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}
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,#059669,#10b981);color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(5,150,105,.45);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
.ach-popup.show{display:flex}
.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}
.dnd-pool.col{flex-direction:column;align-items:stretch}
.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:pointer;user-select:none;font-size:.92rem}
.dnd-chip:hover{border-color:var(--sec-acc,var(--pri))}
.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(5,150,105,.22)}
.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;cursor:pointer}
.drop-box{background:var(--card);border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:90px}
.drop-box.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid}
.drop-box h5{font-family:'Unbounded',sans-serif;font-size:.78rem;color:var(--sec-acc-d,var(--pri2));margin-bottom:8px;text-transform:uppercase}
.drop-items{display:flex;flex-wrap:wrap;gap:6px;min-height:32px}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.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}
.hp-boss{height:14px;background:rgba(220,38,38,.12);border-radius:9px;overflow:hidden;border:1px solid #fecaca;margin:8px 0}
.hp-boss-fill{height:100%;background:linear-gradient(90deg,#dc2626,#f59e0b);border-radius:9px;transition:width .5s cubic-bezier(.4,0,.2,1)}
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
.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}
}
.boss-card{padding:16px;background:var(--card);border-radius:12px;border:2px solid var(--bad,#dc2626);margin-bottom:14px}
.boss-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.boss-title{font-family:'Unbounded',sans-serif;font-weight:800;color:#7f1d1d;font-size:1.04rem;flex:1}
.boss-stage{font-size:.85rem;color:var(--muted)}
.boss-q{font-size:1rem;line-height:1.55;padding:11px 13px;background:var(--card-soft);border-radius:8px;margin-bottom:9px;border-left:3px solid var(--bad,#dc2626)}
.svg-host{display:flex;justify-content:center;margin:12px 0}
.svg-host-row{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin:12px 0}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Геометрия 7 · Глава 2</h1>
<div class="hdr-sub">Признаки равенства треугольников · равнобедренный · серединный перпендикуляр</div>
</div>
<div class="hdr-side">
<a href="/textbook/geometry-7" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К геометрии 7</a>
<button id="sidebar-btn" class="hdr-btn"><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"><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>3 признака равенства треугольников</b> (по двум сторонам и углу между ними; по стороне и двум углам; по трём сторонам), знакомимся с <b>высотой, медианой, биссектрисой</b>, разбираем свойства <b>равнобедренного треугольника</b> и заканчиваем <b>серединным перпендикуляром</b> к отрезку.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p8')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 8</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" 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-p8" class="sec" data-watermark="△"><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="h"><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="✵"><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-p13" class="sec" data-watermark="ССС"><div class="sec-header"><span class="sec-num">§ 13</span><h2 class="sec-h">Третий признак равенства треугольников</h2></div><div id="p13-body"></div></section>
<section id="sec-p14" class="sec" data-watermark="⟂"><div class="sec-header"><span class="sec-num">§ 14</span><h2 class="sec-h">Серединный перпендикуляр к отрезку</h2></div><div id="p14-body"></div></section>
<section id="sec-final2" class="sec" data-watermark="★"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#059669,#10b981)">Финал главы</span><h2 class="sec-h">Итоги. 6 боссов главы 2</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">Интерактивный учебник «Геометрия 7» · Глава 2 · Признаки равенства треугольников · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<script>
'use strict';
const STATE = { current:'p8', progress:{p8:0,p9:0,p10:0,p11:0,p12:0,p13:0,p14:0,final2:0}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 8;
const _TB_SLUG = 'geometry-7-ch2';
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!',
p9_done:'Признаки СУС и УСУ — освоены!',
p11_done:'Равнобедренные — твои!',
p13_done:'Третий признак тоже знаешь!',
p14_done:'Серединный перпендикуляр — фундамент!',
ch2_done:'Глава 2 пройдена!',
};
function loadProgress(){
try{
const s=localStorage.getItem('geometry7_ch2_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('geometry7_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)); } }
STATE.xp=+(localStorage.getItem('geometry7_xp')||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('geometry7_ch2_progress', JSON.stringify(STATE.progress));
localStorage.setItem('geometry7_ch2_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('geometry7_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);
if(STATE.progress[key]>=100){
if(key==='p9') achievement('p9_done');
else if(key==='p11') achievement('p11_done');
else if(key==='p13') achievement('p13_done');
else if(key==='p14') achievement('p14_done');
else if(key==='final2') achievement('ch2_done');
}
}
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 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(); refreshProgressUI();
if(window.LS&&window.LS.xp) window.LS.xp.add(n,'geometry7-ch2-'+(src||'misc'));
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); }
}
}
function refreshProgressUI(){
const total=Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0)/TOTAL_PARAS);
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 22 20 2 20"/></svg> Ур. '+STATE.level+' \xb7 '+(STATE.xp||0)+' XP'; }
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-'+id);
}
const PARAS = [
{ id:'p8', num:'§ 8', name:'Треугольники', sub:'Виды и периметр' },
{ id:'p9', num:'§ 9', name:'1-й и 2-й признаки', sub:'СУС и УСУ' },
{ id:'p10', num:'§ 10', name:'Высота, медиана, биссектриса', sub:'Замечательные точки' },
{ id:'p11', num:'§ 11', name:'Равнобедренный треугольник', sub:'Углы при основании равны' },
{ id:'p12', num:'§ 12', name:'Признаки равнобедренного', sub:'Если 2 угла равны' },
{ id:'p13', num:'§ 13', name:'3-й признак', sub:'ССС — три стороны' },
{ id:'p14', num:'§ 14', name:'Серединный перпендикуляр', sub:'ГМТ + центр описанной' },
{ id:'final2', num:'★', name:'Финал главы', sub:'Итоги \xB7 6 боссов', 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);
});
if(window.renderMathInElement) try{ renderMath(g); }catch(e){}
}
const BUILT=new Set();
const BUILDERS = { p8:()=>buildP8(), p9:()=>buildP9(), p10:()=>buildP10(), p11:()=>buildP11(), p12:()=>buildP12(), p13:()=>buildP13(), p14:()=>buildP14(), final2:()=>buildFinal2() };
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);
markLastPara(id);
}
const SIDEBARS = {
p8:{title:'Шпаргалка \xA78',rows:[
['$\\triangle ABC$','3 вершины, 3 стороны, 3 угла'],
['$P = AB + BC + CA$','периметр'],
['По сторонам','равно-сторонний / равно-бедр. / разно-сторонний'],
['По углам','остр-/прямо-/тупо-угольный'],
['Равные','против равных сторон — равные углы'],
]},
p9:{title:'Шпаргалка \xA79',rows:[
['1-й признак (СУС / SAS)','2 стороны + угол МЕЖДУ ними'],
['2-й признак (УСУ / ASA)','сторона + 2 прилежащих угла'],
['Задаёт однозначно','выводит равенство треугольников'],
['Применение','одинаковые элементы → $\\triangle = \\triangle$'],
]},
p10:{title:'Шпаргалка \xA710',rows:[
['Высота','перпендикуляр из вершины на пр. сторону'],
['Медиана','от вершины к середине пр. стороны'],
['Биссектриса','делит угол пополам, идёт до стороны'],
['Пересечения','3 высоты — в одной точке'],
['Замеч. точки','центроид (медианы), инцентр (бис.), ортоцентр (выс.)'],
]},
p11:{title:'Шпаргалка \xA711',rows:[
['Равнобедр.','две стороны равны (боковые)'],
['Свойство','углы при основании равны'],
['Биссектриса к осн.','= высота и медиана'],
['Равносторонний','частный случай'],
]},
p12:{title:'Шпаргалка \xA712',rows:[
['Признак 1','если 2 угла равны → равнобедр.'],
['Признак 2','если высота=медиана → равнобедр.'],
['Признак 3','если высота=биссектриса → равнобедр.'],
['Обратная теорема','к свойству углов при основании'],
]},
p13:{title:'Шпаргалка \xA713',rows:[
['3-й признак (ССС / SSS)','3 стороны $\\to$ равны'],
['Жёсткость','3 стороны задают $\\triangle$ однозначно'],
['3 угла','НЕ задают треугольник'],
['Не всегда','2 стороны + угол НЕ между ними'],
]},
p14:{title:'Шпаргалка \xA714',rows:[
['Серединный $\\perp$','$\\perp AB$ и через середину $AB$'],
['ГМТ','равноудалённых от концов отрезка'],
['$KA = KB$','для любой точки $K$ на серед. $\\perp$'],
['3 серед. $\\perp$ $\\triangle$','пересекаются в одной точке'],
['Эта точка','центр описанной окружности'],
]},
final2:{title:'Финал главы',rows:[
['\xA78\xA714','теория главы 2'],
['Боссов','6'],
['Награда','+120 XP за полное прохождение'],
]},
};
const TIPS=[
{sec:'p8',html:'В <b>равных</b> треугольниках против равных сторон лежат равные углы — это фундамент для всех 3 признаков.'},
{sec:'p9',html:'<b>1-й признак (СУС)</b>: угол должен быть <b>между</b> сторонами. <b>2-й признак (УСУ)</b>: оба угла должны быть <b>прилежащими</b> к стороне.'},
{sec:'p10',html:'В <b>равнобедренном</b> треугольнике высота, медиана и биссектриса <b>к основанию совпадают</b>. В обычном — нет.'},
{sec:'p11',html:'Доказательство свойства углов при основании: проводим биссектрису к основанию — получаем два равных $\\triangle$ по 1-му признаку.'},
{sec:'p12',html:'Признак — это <b>обратная</b> теорема к свойству. Если 2 угла равны $\\Rightarrow$ треугольник равнобедренный.'},
{sec:'p13',html:'<b>3 угла</b> НЕ задают треугольник (могут быть разными по размерам). А <b>3 стороны</b> задают <b>однозначно</b>!'},
{sec:'p14',html:'Точка на серединном перпендикуляре <b>равноудалена</b> от концов отрезка. И наоборот: если $KA = KB$, то $K$ на серединном $\\perp$.'},
{sec:'final2',html:'6 боссов — самая большая проверка пока. Готовь блокнот для записей.'},
];
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS.p8;
let html='';
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, 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>'+xpNext+' XP</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" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e">Подсказка</h4><div class="sidecard-row" style="font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
if(STATE.achievements.size>0){
html+='<div class="sidecard"><h4>Достижения</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){}
}
function initTheme(){
const t=localStorage.getItem('geometry7_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('geometry7_ch2_theme', dark?'dark':'light');
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
});
}
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){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'&#10003; Верно!':'&#10007; Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
const ICONS = {
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>',
};
function makeCard(kind, title, num, body){
const labels={theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример'};
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]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
}
function secNav(prev, next){
const NAMES={p8:'\xA78',p9:'\xA79',p10:'\xA710',p11:'\xA711',p12:'\xA712',p13:'\xA713',p14:'\xA714',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;
}
function setupSorter(cfg){
const placed={}; const pool=document.getElementById(cfg.poolId); const scope=document.querySelector(cfg.scopeSelector);
if(!pool||!scope) return {placed,render:()=>{},reset:()=>{}};
pool.classList.add('dnd-pool'); if(cfg.columnLayout) pool.classList.add('col');
let armed=null;
function buildChip(it,isPlaced){ const e=document.createElement('div'); e.className='dnd-chip'+(isPlaced?' placed':''); e.dataset.id=it.id; e.innerHTML='<span class="dnd-txt">'+it.html+'</span><span class="dnd-x" title="Убрать">\xd7</span>'; attach(e,it.id); return e; }
function attach(elm,itId){
elm.addEventListener('click', ev=>{
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; }
if(placed[itId]){delete placed[itId];armed=null;render();} else {armed=(armed===itId)?null:itId;render();}
});
}
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();} }); }); }
function render(){ pool.innerHTML=''; cfg.items.forEach(it=>{if(placed[it.id])return;const c=buildChip(it,false);if(armed===it.id)c.classList.add('armed');pool.appendChild(c);}); 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(); }};
}
function makeTrainer(opts){
let i=0, score=0;
const Q=opts.questions;
const parser = opts.parser || (v => parseFloat(String(v).replace(',','.')));
function show(){
if(i >= Q.length){
document.getElementById(opts.idPrefix+'-q').innerHTML = '<b>Готово!</b> Результат: '+score+' / '+Q.length;
if(opts.onComplete) opts.onComplete(score, Q.length);
return;
}
document.getElementById(opts.idPrefix+'-i').textContent = (i+1);
document.getElementById(opts.idPrefix+'-s').textContent = score;
document.getElementById(opts.idPrefix+'-q').innerHTML = Q[i].q;
document.getElementById(opts.idPrefix+'-ans').value = '';
renderMath(document.getElementById(opts.idPrefix+'-q'));
document.getElementById(opts.idPrefix+'-fb').style.display = 'none';
}
function go(){
if(i >= Q.length) return;
const fb = document.getElementById(opts.idPrefix+'-fb');
const raw = document.getElementById(opts.idPrefix+'-ans').value.trim();
if(raw === ''){ feedback(fb, false, '&#10007; Введи ответ.'); return; }
const expected = Q[i].a;
let ok = false;
if(typeof expected === 'function') ok = expected(raw);
else { const got = parser(raw); ok = !isNaN(got) && Math.abs(got - expected) < 1e-6; }
if(ok){ score++; feedback(fb, true, '&#10003; Верно! Дальше ▶'); }
else feedback(fb, false, '&#10007; Неверно. Правильно: <b>'+(Q[i].show||expected)+'</b>. Дальше ▶');
document.getElementById(opts.idPrefix+'-s').textContent = score;
i++; setTimeout(show, 1100);
}
document.getElementById(opts.idPrefix+'-go').addEventListener('click', go);
document.getElementById(opts.idPrefix+'-ans').addEventListener('keydown', e=>{ if(e.key==='Enter') go(); });
const restart = document.getElementById(opts.idPrefix+'-start');
if(restart) restart.addEventListener('click', ()=>{ i=0; score=0; show(); });
show();
}
function trainerHTML(idPrefix, total, placeholder){
return '<div class="score-display"><span>Задача <b id="'+idPrefix+'-i">1</b> / '+total+'</span><span>Очки: <b id="'+idPrefix+'-s">0</b> / '+total+'</span></div>'
+'<div id="'+idPrefix+'-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center"></div>'
+'<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center">'
+'<input type="text" id="'+idPrefix+'-ans" class="tinp" placeholder="'+(placeholder||'Ответ')+'" style="width:140px;text-align:center">'
+'<button class="btn primary" id="'+idPrefix+'-go">Проверить</button>'
+'<button class="btn" id="'+idPrefix+'-start">Заново</button>'
+'</div><div class="feedback" id="'+idPrefix+'-fb"></div>';
}
function readButton(paraId){
return '<div style="margin-top:18px;display:flex;justify-content:center">'
+'<button class="btn primary" id="'+paraId+'-read-btn">'
+'<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>'
+' Я прочитал \xA7'+paraId.replace('p','')+' (+10 XP)'
+'</button></div>';
}
function wireReadBtn(paraId){
document.getElementById(paraId+'-read-btn').addEventListener('click', ()=>{
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
const b=document.getElementById(paraId+'-read-btn'); b.textContent='Прочитано! +10 XP'; b.disabled=true; b.style.opacity=.6;
});
}
function initSidebarToggle(){
const side=document.getElementById('col-side'),back=document.getElementById('col-side-backdrop'),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();
buildParaSelector(); refreshProgressUI(); goTo('p8');
setTimeout(()=>achievement('start','Начало главы 2!'), 600);
}
document.addEventListener('DOMContentLoaded', init);
/* ============================================================
\xA7 8 — Треугольники
============================================================ */
function buildP8(){
const box = document.getElementById('p8-body');
const G = window.GEOM7;
let html = '';
/* SVG: разные виды треугольников */
let svgTri='';
if(G){
const b=G.svgBox(280,160,{id:'p8-tri',cell:20});
const A={x:60,y:130}, B={x:140,y:30}, C={x:230,y:130};
svgTri = b.open
+ G.polygon([A,B,C],{color:'#059669',fill:'rgba(5,150,105,.08)'})
+ G.point(A.x,A.y,'A',{color:'#dc2626',dx:-14,dy:14})
+ G.point(B.x,B.y,'B',{color:'#dc2626',dx:-6,dy:-8})
+ G.point(C.x,C.y,'C',{color:'#dc2626',dx:8,dy:14})
+ b.close;
}
function triByAngles(label, angles){
if(!G) return '';
const b=G.svgBox(140,130,{id:'p8-by-'+label,cell:20});
let pts;
if(label==='остроугольный') pts=[{x:25,y:110},{x:115,y:110},{x:80,y:25}];
else if(label==='прямоугольный') pts=[{x:30,y:110},{x:110,y:110},{x:30,y:30}];
else pts=[{x:20,y:90},{x:115,y:110},{x:65,y:40}];
let s=b.open + G.polygon(pts,{color:'#0891b2',fill:'rgba(8,145,178,.1)'});
if(label==='прямоугольный') s += G.rightAngleMark(pts[0],pts[1],pts[2],{color:'#dc2626',size:12});
s += '<text x="70" y="125" text-anchor="middle" font-size="10" fill="#475569" font-weight="700" font-family="Unbounded">'+label+'</text>';
s += b.close; return s;
}
html += makeCard('theory', 'Что такое треугольник', '8.1', `
<p>Если на плоскости отметить три точки $A, B, C$, не лежащие на одной прямой, и соединить их отрезками, получится <b>треугольник $\\triangle ABC$</b>.</p>
<p>$AB, BC, CA$ — <b>стороны</b>; $A, B, C$ — <b>вершины</b>; $\\angle A, \\angle B, \\angle C$ — <b>углы</b>.</p>
<p>Периметр: $P_{ABC} = AB + BC + CA$.</p>
<p><b>Свойство жёсткости</b>: если соединить концы трёх планок, получится треугольник, который <b>невозможно деформировать</b> — он сохраняет форму. Это широко используется в строительстве.</p>
<div class="svg-host">`+svgTri+`</div>`);
html += makeCard('rule', 'Равные треугольники', '8.2', `
<p>Два треугольника называются <b>равными</b>, если их можно <b>совместить наложением</b> так, что совпадут все три стороны и все три угла.</p>
<p>Если $\\triangle ABC = \\triangle A_1 B_1 C_1$, то одновременно равны: $AB = A_1 B_1$, $BC = B_1 C_1$, $AC = A_1 C_1$ <b>И</b> $\\angle A = \\angle A_1$, $\\angle B = \\angle B_1$, $\\angle C = \\angle C_1$.</p>
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Свойство.</b> В равных треугольниках против равных сторон лежат равные углы, а против равных углов — равные стороны.</p>`);
html += makeCard('rule', 'Виды треугольников', '8.3', `
<p><b>По сторонам</b>:</p>
<ul style="padding-left:22px;line-height:1.85">
<li><b>Разносторонний</b> — все стороны разные.</li>
<li><b>Равнобедренный</b> — две стороны равны (боковые); третья — основание.</li>
<li><b>Равносторонний</b> — все три стороны равны (частный случай равнобедренного).</li>
</ul>
<p><b>По углам</b>:</p>
<div class="svg-host-row">`+triByAngles('остроугольный',[60,60,60])+triByAngles('прямоугольный',[90,45,45])+triByAngles('тупоугольный',[120,30,30])+`</div>
<ul style="padding-left:22px;line-height:1.85">
<li><b>Остроугольный</b> — все углы острые.</li>
<li><b>Прямоугольный</b> — есть прямой угол.</li>
<li><b>Тупоугольный</b> — есть тупой угол.</li>
</ul>`);
html += '<div class="wg" id="p8-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Классификация по сторонам</div></div>'
+'<div class="wg-help">По длинам трёх сторон определи тип треугольника.</div>'
+'<div class="score-display"><span>Задача <b id="p8-iv1-i">1</b> / 6</span><span>Очки: <b id="p8-iv1-s">0</b> / 6</span></div>'
+'<div id="p8-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.1rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap"><button class="btn primary" id="p8-iv1-eq" style="background:#dc2626;border-color:#dc2626">Равносторон.</button><button class="btn primary" id="p8-iv1-is" style="background:#7c3aed;border-color:#7c3aed">Равнобедр.</button><button class="btn primary" id="p8-iv1-sc" style="background:#0891b2;border-color:#0891b2">Разносторон.</button></div>'
+'<div class="feedback" id="p8-iv1-fb"></div></div>';
html += '<div class="wg" id="p8-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Периметр</div></div>'
+'<div class="wg-help">Применяй: $P = a + b + c$. Для равнобедренного: $P = 2 \\cdot \\text{бок} + \\text{осн}$.</div>'
+trainerHTML('p8-iv2', 5, 'периметр')
+'</div>';
html += secNav(null, 'p9') + readButton('p8');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'$AB = BC = AC = 6$ см', ans:'eq' },
{ e:'$AB = 5$, $BC = 5$, $AC = 8$', ans:'is' },
{ e:'$AB = 3$, $BC = 4$, $AC = 5$', ans:'sc' },
{ e:'$AB = 7$, $BC = 6$, $AC = 7$', ans:'is' },
{ e:'$AB = 12$, $BC = 12$, $AC = 12$', ans:'eq' },
{ e:'$AB = 8$, $BC = 9$, $AC = 10$', ans:'sc' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p8-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(12,'p8-iv1');bumpProgress('p8',25);} else if(score>=4){addXp(6,'p8-iv1');bumpProgress('p8',12);} return; }
document.getElementById('p8-iv1-i').textContent=(i+1);
document.getElementById('p8-iv1-s').textContent=score;
document.getElementById('p8-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p8-iv1-q'));
document.getElementById('p8-iv1-fb').style.display='none';
}
function ans(a){
if(i>=Q.length) return;
const fb=document.getElementById('p8-iv1-fb');
if(a===Q[i].ans){ score++; feedback(fb,true,'&#10003; Верно!'); }
else{ const lab={eq:'равносторонний',is:'равнобедренный',sc:'разносторонний'}; feedback(fb,false,'&#10007; Правильно: <b>'+lab[Q[i].ans]+'</b>'); }
document.getElementById('p8-iv1-s').textContent=score;
i++; setTimeout(show,1100);
}
document.getElementById('p8-iv1-eq').addEventListener('click',()=>ans('eq'));
document.getElementById('p8-iv1-is').addEventListener('click',()=>ans('is'));
document.getElementById('p8-iv1-sc').addEventListener('click',()=>ans('sc'));
show();
})();
makeTrainer({
idPrefix:'p8-iv2',
questions:[
{ q:'$AB = 5$, $BC = 6$, $AC = 7$. Найди $P$.', a:18 },
{ q:'Равнобедренный: боковая = 14 дм, основание = 8 дм. $P = ?$', a:36 },
{ q:'$P = 40$ см. Основание в 2 раза меньше боковой стороны. Найди основание.', a:8 },
{ q:'Равносторонний с $P = 30$ см. Найди сторону.', a:10 },
{ q:'Стороны в отношении $2 : 3 : 4$, $P = 36$. Найди наибольшую сторону.', a:16 },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p8-iv2');bumpProgress('p8',28);} else if(s>=3){addXp(8,'p8-iv2');bumpProgress('p8',14);} }
});
wireReadBtn('p8');
}
/* ============================================================
\xA7 9 — Первый и второй признаки равенства
============================================================ */
function buildP9(){
const box = document.getElementById('p9-body');
const G = window.GEOM7;
let html = '';
function triSAS(){
if(!G) return '';
const b=G.svgBox(260,160,{id:'p9-sas',cell:20});
const A={x:50,y:130}, B={x:160,y:30}, C={x:220,y:130};
let s=b.open;
s += G.polygon([A,B,C],{color:'#0891b2',fill:'rgba(8,145,178,.08)'});
s += G.segment(A,B,{color:'#dc2626',width:2.5,ticks:1});
s += G.segment(A,C,{color:'#7c3aed',width:2.5,ticks:2});
s += G.angle(A,B,C,{color:'#f59e0b',r:30,label:'∠A',fontSize:11,labelOffset:14});
s += G.point(A.x,A.y,'A',{color:'#1e293b',dx:-14,dy:14});
s += G.point(B.x,B.y,'B',{color:'#1e293b',dx:-6,dy:-8});
s += G.point(C.x,C.y,'C',{color:'#1e293b',dx:8,dy:14});
s += b.close; return s;
}
function triASA(){
if(!G) return '';
const b=G.svgBox(260,160,{id:'p9-asa',cell:20});
const A={x:50,y:130}, B={x:170,y:30}, C={x:220,y:130};
let s=b.open;
s += G.polygon([A,B,C],{color:'#0891b2',fill:'rgba(8,145,178,.08)'});
s += G.segment(A,C,{color:'#dc2626',width:2.5,ticks:1});
s += G.angle(A,B,C,{color:'#f59e0b',r:28,label:'∠A',fontSize:11,labelOffset:14});
s += G.angle(C,A,B,{color:'#10b981',r:28,label:'∠C',fontSize:11,labelOffset:14});
s += G.point(A.x,A.y,'A',{color:'#1e293b',dx:-14,dy:14});
s += G.point(B.x,B.y,'B',{color:'#1e293b',dx:-6,dy:-8});
s += G.point(C.x,C.y,'C',{color:'#1e293b',dx:8,dy:14});
s += b.close; return s;
}
html += makeCard('rule', 'Первый признак равенства — СУС (SAS)', '9.1', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Теорема (1-й признак).</b> Если две стороны и угол <b>между ними</b> одного треугольника соответственно равны двум сторонам и углу между ними другого, то такие треугольники равны.</p>
<p>На рисунке: $AB = A_1 B_1$, $AC = A_1 C_1$, $\\angle A = \\angle A_1$ $\\Rightarrow$ $\\triangle ABC = \\triangle A_1 B_1 C_1$.</p>
<div class="svg-host">`+triSAS()+`</div>
<p style="background:var(--warn-bg);padding:10px 14px;border-radius:8px;border-left:4px solid var(--warn)"><b>Запоминалка.</b> Сокращение <b>СУС</b> (сторона&nbsp;— угол&nbsp;— сторона) — три элемента в порядке расположения. Латинская версия — <b>SAS</b> (Side&nbsp;— Angle&nbsp;— Side).</p>
<p>Говорят, что <b>две стороны и угол между ними задают треугольник однозначно</b>.</p>`);
html += makeCard('rule', 'Второй признак равенства — УСУ (ASA)', '9.2', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Теорема (2-й признак).</b> Если сторона и два <b>прилежащих</b> к ней угла одного треугольника соответственно равны стороне и двум прилежащим к ней углам другого, то такие треугольники равны.</p>
<p>На рисунке: $AC = A_1 C_1$, $\\angle A = \\angle A_1$, $\\angle C = \\angle C_1$ $\\Rightarrow$ $\\triangle ABC = \\triangle A_1 B_1 C_1$.</p>
<div class="svg-host">`+triASA()+`</div>
<p style="background:var(--warn-bg);padding:10px 14px;border-radius:8px;border-left:4px solid var(--warn)"><b>Запоминалка.</b> Сокращение <b>УСУ</b> (угол&nbsp;— сторона&nbsp;— угол). Латинская версия — <b>ASA</b> (Angle&nbsp;— Side&nbsp;— Angle).</p>
<p>Говорят, что <b>сторона и два прилежащих к ней угла задают треугольник однозначно</b>.</p>`);
html += makeCard('example', 'Пример применения', '9.3', `
<p><b>Задача.</b> Отрезки $AB$ и $CD$ пересекаются в их серединах. Докажи, что $AC = BD$.</p>
<p><b>Решение.</b> Пусть $O$ — точка пересечения. Рассмотрим $\\triangle AOC$ и $\\triangle BOD$:</p>
<ul style="padding-left:22px;line-height:1.9">
<li>$AO = OB$, $CO = OD$ — по условию (середины);</li>
<li>$\\angle AOC = \\angle BOD$ — как вертикальные.</li>
</ul>
<p>По <b>1-му признаку</b>: $\\triangle AOC = \\triangle BOD$. Отсюда $AC = BD$ — как соответствующие стороны равных треугольников.</p>`);
html += '<div class="wg" id="p9-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Какой признак применить?</div></div>'
+'<div class="wg-help">По данным элементам определи, какой признак гарантирует равенство треугольников: <b>1-й (СУС)</b> или <b>2-й (УСУ)</b>.</div>'
+'<div class="score-display"><span>Задача <b id="p9-iv1-i">1</b> / 6</span><span>Очки: <b id="p9-iv1-s">0</b> / 6</span></div>'
+'<div id="p9-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:10px;justify-content:center"><button class="btn primary" id="p9-iv1-sas" style="background:#dc2626;border-color:#dc2626">1-й (СУС)</button><button class="btn primary" id="p9-iv1-asa" style="background:#7c3aed;border-color:#7c3aed">2-й (УСУ)</button><button class="btn primary" id="p9-iv1-no" style="background:#64748b;border-color:#64748b">Недостаточно</button></div>'
+'<div class="feedback" id="p9-iv1-fb"></div></div>';
html += '<div class="wg" id="p9-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Найди равные элементы</div></div>'
+'<div class="wg-help">Из равенства $\\triangle ABC = \\triangle KMN$ ($A\\to K$, $B\\to M$, $C\\to N$) — равные стороны и углы.</div>'
+trainerHTML('p9-iv2', 5, 'число / буквы')
+'</div>';
html += secNav('p8', 'p10') + readButton('p9');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'$AB = A_1B_1$, $\\angle A = \\angle A_1$, $AC = A_1 C_1$', ans:'sas' },
{ e:'$AC = A_1 C_1$, $\\angle A = \\angle A_1$, $\\angle C = \\angle C_1$', ans:'asa' },
{ e:'$BC = B_1 C_1$, $\\angle B = \\angle B_1$, $\\angle C = \\angle C_1$', ans:'asa' },
{ e:'$AB = A_1 B_1$, $AC = A_1 C_1$ (без углов)', ans:'no' },
{ e:'$AB = A_1 B_1$, $BC = B_1 C_1$, $\\angle B = \\angle B_1$', ans:'sas' },
{ e:'$\\angle A = \\angle A_1$, $\\angle B = \\angle B_1$, $\\angle C = \\angle C_1$', ans:'no' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p9-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p9-iv1');bumpProgress('p9',30);} else if(score>=4){addXp(8,'p9-iv1');bumpProgress('p9',15);} return; }
document.getElementById('p9-iv1-i').textContent=(i+1);
document.getElementById('p9-iv1-s').textContent=score;
document.getElementById('p9-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p9-iv1-q'));
document.getElementById('p9-iv1-fb').style.display='none';
}
function ans(a){
if(i>=Q.length) return;
const fb=document.getElementById('p9-iv1-fb');
if(a===Q[i].ans){ score++; feedback(fb,true,'&#10003; Верно!'); }
else{ const lab={sas:'1-й (СУС)',asa:'2-й (УСУ)',no:'недостаточно'}; feedback(fb,false,'&#10007; Правильно: <b>'+lab[Q[i].ans]+'</b>'); }
document.getElementById('p9-iv1-s').textContent=score;
i++; setTimeout(show,1100);
}
document.getElementById('p9-iv1-sas').addEventListener('click',()=>ans('sas'));
document.getElementById('p9-iv1-asa').addEventListener('click',()=>ans('asa'));
document.getElementById('p9-iv1-no').addEventListener('click',()=>ans('no'));
show();
})();
makeTrainer({
idPrefix:'p9-iv2',
parser:(v)=>v,
questions:[
{ q:'$\\triangle ABC = \\triangle KMN$, $AB = 5$ см. Чему равна $KM$?', a:(v)=>+v===5, show:'5' },
{ q:'$\\triangle ABC = \\triangle KMN$, $BC = 6$. Чему равна $MN$?', a:(v)=>+v===6, show:'6' },
{ q:'$\\triangle ABC = \\triangle KMN$, $AC = 7$. Чему равна $KN$?', a:(v)=>+v===7, show:'7' },
{ q:'$\\triangle ABC = \\triangle DEF$, $\\angle A = 60°$. Чему равен $\\angle D$?', a:(v)=>+v===60, show:'60°' },
{ q:'$\\triangle ABC = \\triangle DEF$, $\\angle A = 60°, \\angle B = 90°$. Чему равен $\\angle F$?', a:(v)=>+v===30, show:'30° (так как $\\angle C = 180 - 60 - 90 = 30$)' },
],
onComplete:(s,n)=>{ if(s===n){addXp(12,'p9-iv2');bumpProgress('p9',22);} else if(s>=3){addXp(6,'p9-iv2');bumpProgress('p9',12);} }
});
wireReadBtn('p9');
}
/* ============================================================
\xA7 10 — Высота, медиана, биссектриса
============================================================ */
function buildP10(){
const box = document.getElementById('p10-body');
const G = window.GEOM7;
let html = '';
function triWith(kind){
if(!G) return '';
const b=G.svgBox(180,150,{id:'p10-'+kind,cell:20});
const A={x:30,y:120}, B={x:90,y:25}, C={x:160,y:120};
let s=b.open;
s += G.polygon([A,B,C],{color:'#0891b2',fill:'rgba(8,145,178,.08)'});
if(kind==='height'){
const H={x:90,y:120};
s += G.segment(B,H,{color:'#dc2626',width:2.5});
s += G.rightAngleMark(H,B,A,{color:'#dc2626',size:10});
s += G.point(H.x,H.y,'H',{color:'#dc2626',dx:-12,dy:14,r:3});
} else if(kind==='median'){
const M={x:(A.x+C.x)/2,y:(A.y+C.y)/2};
s += G.segment(B,M,{color:'#dc2626',width:2.5});
s += G.segment(A,M,{color:'#dc2626',width:2,dash:'2 2'});
s += G.segment(M,C,{color:'#dc2626',width:2,dash:'2 2'});
s += G.point(M.x,M.y,'M',{color:'#dc2626',dx:-12,dy:14,r:3});
} else if(kind==='bisector'){
const K={x:(A.x+C.x)/2+5,y:120};
s += G.segment(B,K,{color:'#dc2626',width:2.5});
s += G.angle(B,A,K,{color:'#10b981',r:18,label:'',fontSize:9});
s += G.angle(B,K,C,{color:'#10b981',r:24,label:'',fontSize:9});
s += G.point(K.x,K.y,'K',{color:'#dc2626',dx:-10,dy:14,r:3});
}
s += G.point(A.x,A.y,'A',{color:'#1e293b',dx:-14,dy:8});
s += G.point(B.x,B.y,'B',{color:'#1e293b',dx:-6,dy:-8});
s += G.point(C.x,C.y,'C',{color:'#1e293b',dx:8,dy:8});
s += '<text x="90" y="143" text-anchor="middle" font-size="10" fill="#475569" font-weight="700" font-family="Unbounded">'+
(kind==='height'?'Высота BH':kind==='median'?'Медиана BM':'Биссектриса BK')+'</text>';
s += b.close; return s;
}
html += makeCard('rule', 'Высота', '10.1', `
<p><b>Высотой</b> треугольника называется <b>перпендикуляр</b>, опущенный из вершины треугольника на противоположную сторону (или её продолжение).</p>
<p>Обозначается малой буквой $h$ или отрезком (например, $BH$ — высота из $B$).</p>`);
html += makeCard('rule', 'Медиана', '10.2', `
<p><b>Медианой</b> треугольника называется отрезок, соединяющий вершину треугольника с <b>серединой</b> противоположной стороны.</p>
<p>$BM$ — медиана, если $M$ — середина $AC$.</p>`);
html += makeCard('rule', 'Биссектриса', '10.3', `
<p><b>Биссектрисой</b> треугольника называется отрезок биссектрисы угла треугольника, соединяющий вершину с точкой пересечения биссектрисы с противоположной стороной.</p>
<p>$BK$ — биссектриса, если $\\angle ABK = \\angle KBC$.</p>
<div class="svg-host-row">`+triWith('height')+triWith('median')+triWith('bisector')+`</div>`);
html += makeCard('rule', 'Замечательные точки', '10.4', `
<p>У треугольника три вершины, значит, три высоты, три медианы, три биссектрисы.</p>
<p><b>Они пересекаются в одной точке</b>:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>Точка пересечения медиан — <b>центроид</b> (всегда внутри).</li>
<li>Точка пересечения биссектрис — <b>инцентр</b> (всегда внутри; центр вписанной окружности).</li>
<li>Точка пересечения высот — <b>ортоцентр</b>. У остроугольного — внутри, у тупоугольного — вне, у прямоугольного — в вершине прямого угла.</li>
</ul>
<p>В равных треугольниках равны соответствующие высоты, медианы и биссектрисы.</p>`);
html += '<div class="wg" id="p10-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Высота / медиана / биссектриса?</div></div>'
+'<div class="wg-help">Какой отрезок описывает условие?</div>'
+'<div class="score-display"><span>Задача <b id="p10-iv1-i">1</b> / 6</span><span>Очки: <b id="p10-iv1-s">0</b> / 6</span></div>'
+'<div id="p10-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap"><button class="btn primary" id="p10-iv1-h" style="background:#dc2626;border-color:#dc2626">Высота</button><button class="btn primary" id="p10-iv1-m" style="background:#7c3aed;border-color:#7c3aed">Медиана</button><button class="btn primary" id="p10-iv1-b" style="background:#10b981;border-color:#10b981">Биссектриса</button></div>'
+'<div class="feedback" id="p10-iv1-fb"></div></div>';
html += '<div class="wg" id="p10-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Задачи с медианами</div></div>'
+'<div class="wg-help">Медиана делит сторону пополам.</div>'
+trainerHTML('p10-iv2', 4, 'длина')
+'</div>';
html += secNav('p9', 'p11') + readButton('p10');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'Отрезок $\\perp$ от вершины на противоположную сторону', ans:'h' },
{ e:'Отрезок от вершины до середины противоположной стороны', ans:'m' },
{ e:'Отрезок, делящий угол треугольника пополам', ans:'b' },
{ e:'Отрезок $BK$, где $\\angle ABK = \\angle KBC$', ans:'b' },
{ e:'Отрезок $BM$, где $AM = MC$', ans:'m' },
{ e:'Отрезок $BH$, где $\\angle BHA = 90°$', ans:'h' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p10-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(12,'p10-iv1');bumpProgress('p10',25);} else if(score>=4){addXp(6,'p10-iv1');bumpProgress('p10',12);} return; }
document.getElementById('p10-iv1-i').textContent=(i+1);
document.getElementById('p10-iv1-s').textContent=score;
document.getElementById('p10-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p10-iv1-q'));
document.getElementById('p10-iv1-fb').style.display='none';
}
function ans(a){
if(i>=Q.length) return;
const fb=document.getElementById('p10-iv1-fb');
if(a===Q[i].ans){ score++; feedback(fb,true,'&#10003; Верно!'); }
else{ const lab={h:'высота',m:'медиана',b:'биссектриса'}; feedback(fb,false,'&#10007; Это <b>'+lab[Q[i].ans]+'</b>'); }
document.getElementById('p10-iv1-s').textContent=score;
i++; setTimeout(show,1100);
}
document.getElementById('p10-iv1-h').addEventListener('click',()=>ans('h'));
document.getElementById('p10-iv1-m').addEventListener('click',()=>ans('m'));
document.getElementById('p10-iv1-b').addEventListener('click',()=>ans('b'));
show();
})();
makeTrainer({
idPrefix:'p10-iv2',
questions:[
{ q:'$BM$ — медиана, $AC = 14$ см. Найди $AM$.', a:7 },
{ q:'В равнобедр. $\\triangle ABC$ с $P = 30$ см медиана $BM = 6$ см к основанию. Найди $P_{ABM}$.', a:18 },
{ q:'$AK$ — медиана. $AK = 4$, $CM = 5$, $AC = 12$. Найди $P_{ABC}$ если $AB = 12$.', a:32 },
{ q:'Медиана к гипотенузе равна 6 см. Чему равна гипотенуза?', a:12 },
],
onComplete:(s,n)=>{ if(s===n){addXp(12,'p10-iv2');bumpProgress('p10',22);} else if(s>=2){addXp(6,'p10-iv2');bumpProgress('p10',10);} }
});
wireReadBtn('p10');
}
/* ============================================================
\xA7 11 — Равнобедренный треугольник
============================================================ */
function buildP11(){
const box = document.getElementById('p11-body');
const G = window.GEOM7;
let html = '';
let svgIso='';
if(G){
const b=G.svgBox(220,180,{id:'p11-iso',cell:20});
const A={x:40,y:150}, B={x:110,y:30}, C={x:180,y:150};
const K={x:(A.x+C.x)/2,y:150};
svgIso = b.open
+ G.polygon([A,B,C],{color:'#7c3aed',fill:'rgba(124,58,237,.08)'})
+ G.segment(A,B,{color:'#7c3aed',width:2.5,ticks:1})
+ G.segment(C,B,{color:'#7c3aed',width:2.5,ticks:1})
+ G.segment(B,K,{color:'#dc2626',width:2,dash:'4 3'})
+ G.angle(A,B,C,{color:'#10b981',r:22,label:'∠A',fontSize:10,labelOffset:14})
+ G.angle(C,A,B,{color:'#10b981',r:22,label:'∠C',fontSize:10,labelOffset:14})
+ G.point(A.x,A.y,'A',{color:'#1e293b',dx:-14,dy:8})
+ G.point(B.x,B.y,'B',{color:'#1e293b',dx:-6,dy:-8})
+ G.point(C.x,C.y,'C',{color:'#1e293b',dx:8,dy:8})
+ G.point(K.x,K.y,'K',{color:'#dc2626',dx:-10,dy:14,r:3})
+ '<text x="110" y="172" text-anchor="middle" font-size="10" fill="#475569" font-weight="700" font-family="Unbounded">AB = BC, ∠A = ∠C</text>'
+ b.close;
}
html += makeCard('theory', 'Что такое равнобедренный', '11.1', `
<p><b>Равнобедренным</b> называется треугольник, у которого <b>две стороны равны</b> (боковые). Третья сторона — <b>основание</b>. Вершина, противоположная основанию, — <b>вершина равнобедренного треугольника</b>.</p>
<p>Углы при основании — углы между боковыми сторонами и основанием.</p>
<div class="svg-host">`+svgIso+`</div>`);
html += makeCard('rule', 'Свойство углов при основании', '11.2', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Теорема.</b> В равнобедренном треугольнике углы при основании равны.</p>
<p><b>Доказательство.</b> $\\triangle ABC$, $AB = BC$. Проведём биссектрису $BK$. Рассмотрим $\\triangle ABK$ и $\\triangle CBK$:</p>
<ul style="padding-left:22px;line-height:1.9">
<li>$BK$ — общая;</li>
<li>$AB = BC$ — по условию;</li>
<li>$\\angle ABK = \\angle CBK$ — по определению биссектрисы.</li>
</ul>
<p>По 1-му признаку $\\triangle ABK = \\triangle CBK$. Отсюда $\\angle A = \\angle C$. Ч. т. д.</p>`);
html += makeCard('rule', 'Биссектриса = медиана = высота', '11.3', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Теорема.</b> В равнобедренном треугольнике биссектриса, проведённая к основанию, является также его <b>медианой и высотой</b>.</p>
<p>Из равенства $\\triangle ABK = \\triangle CBK$ (см. выше) следует:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>$AK = CK$ → $BK$ — медиана;</li>
<li>$\\angle BKA = \\angle BKC$ = 90° (так как они смежные и равные) → $BK$ — высота.</li>
</ul>
<p>Это можно сформулировать ещё короче: <b>биссектриса, медиана и высота равнобедренного треугольника, проведённые к основанию, совпадают</b>.</p>`);
html += '<div class="wg" id="p11-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Углы при основании</div></div>'
+'<div class="wg-help">Используй: углы при основании равны; сумма углов треугольника = 180° (изучается в §19, но здесь принимаем как факт).</div>'
+trainerHTML('p11-iv1', 5, 'градусы')
+'</div>';
html += '<div class="wg" id="p11-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Длины сторон</div></div>'
+'<div class="wg-help">Применяй: $P = 2 \\cdot \\text{бок} + \\text{осн}$; биссектриса к основанию — это медиана (делит основание пополам).</div>'
+trainerHTML('p11-iv2', 5, 'число')
+'</div>';
html += secNav('p10', 'p12') + readButton('p11');
box.innerHTML = html; renderMath(box);
makeTrainer({
idPrefix:'p11-iv1',
questions:[
{ q:'В равнобедр. $\\triangle$ угол при вершине = $40°$. Найди угол при основании.', a:70 },
{ q:'Угол при основании равнобедр. = $70°$. Найди угол при вершине.', a:40 },
{ q:'В равнобедр. $\\angle A = 55°$ (при основании). Чему равен $\\angle C$ (тоже при основании)?', a:55 },
{ q:'Все углы равностороннего треугольника равны?', a:60 },
{ q:'В равнобедр. угол при вершине = $90°$. Найди угол при основании.', a:45 },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p11-iv1');bumpProgress('p11',28);} else if(s>=3){addXp(8,'p11-iv1');bumpProgress('p11',14);} }
});
makeTrainer({
idPrefix:'p11-iv2',
questions:[
{ q:'Равнобедр. $\\triangle$: $P = 40$ см, основание в 2 раза меньше боковой. Найди основание.', a:8 },
{ q:'Боковая в равнобедр. на 4 см больше основания. $P = 56$. Найди боковую.', a:20 },
{ q:'$AB = BC = 12$, $AC = 8$. Медиана $BM$ к $AC$. Найди $AM$.', a:4 },
{ q:'$AB = BC$, $\\angle A = 70°$. Найди $\\angle B$ (при вершине).', a:40 },
{ q:'Равнобедр. с боковой = 14 дм и основанием = 8 дм. Разогнули в равносторонний. Чему равна его сторона?', a:12 },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p11-iv2');bumpProgress('p11',28);} else if(s>=3){addXp(8,'p11-iv2');bumpProgress('p11',14);} }
});
wireReadBtn('p11');
}
/* ============================================================
\xA7 12 — Признаки равнобедренного треугольника
============================================================ */
function buildP12(){
const box = document.getElementById('p12-body');
let html = '';
html += makeCard('rule', 'Признак (обратный к свойству)', '12.1', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Теорема.</b> Если в треугольнике <b>два угла равны</b>, то он равнобедренный.</p>
<p><b>Доказательство.</b> Пусть $\\triangle ABC$, $\\angle A = \\angle C$. Мысленно перевернём треугольник обратной стороной — получим $\\triangle CBA$. Наложим:</p>
<ul style="padding-left:22px;line-height:1.9">
<li>$AC$ совпадает с $CA$ (сторона);</li>
<li>$\\angle A$ совпадает с $\\angle C$ (по условию);</li>
<li>$\\angle C$ совпадает с $\\angle A$ (по условию).</li>
</ul>
<p>По 2-му признаку $\\triangle ABC = \\triangle CBA$. Тогда $AB$ совпадает с $CB$, то есть $AB = BC$. Значит $\\triangle ABC$ — равнобедренный. Ч. т. д.</p>`);
html += makeCard('theory', 'Прямая и обратная теорема', '12.2', `
<p>Свойство и признак — это <b>прямая и обратная</b> теоремы:</p>
<table style="width:100%;margin-top:8px;border-collapse:collapse">
<thead><tr><th style="text-align:left;padding:8px;background:var(--sec-acc-soft);border:1px solid var(--border)">Прямая</th><th style="text-align:left;padding:8px;background:var(--sec-acc-soft);border:1px solid var(--border)">Обратная</th></tr></thead>
<tbody><tr><td style="padding:8px;border:1px solid var(--border)">Если $\\triangle$ равнобедр., то углы при основании равны.</td><td style="padding:8px;border:1px solid var(--border)">Если в $\\triangle$ два угла равны, то он равнобедр.</td></tr></tbody>
</table>
<p style="margin-top:10px">У теоремы есть <b>условие</b> (дано) и <b>заключение</b> (что доказать). В обратной — они меняются местами.</p>
<p>Не всегда обратная теорема верна! Здесь — повезло, верна.</p>`);
html += makeCard('rule', 'Другие признаки', '12.3', `
<p>Можно также показать:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>Если в треугольнике <b>высота равна медиане</b>, проведённые из одной вершины, то треугольник равнобедренный.</li>
<li>Если в треугольнике <b>высота равна биссектрисе</b>, проведённые из одной вершины, то треугольник равнобедренный.</li>
<li>Если в треугольнике <b>медиана равна биссектрисе</b>, проведённые из одной вершины, то треугольник равнобедренный.</li>
</ul>`);
html += '<div class="wg" id="p12-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Равнобедренный или нет?</div></div>'
+'<div class="wg-help">По данным признакам определи: можно ли утверждать, что треугольник равнобедренный?</div>'
+'<div class="score-display"><span>Задача <b id="p12-iv1-i">1</b> / 5</span><span>Очки: <b id="p12-iv1-s">0</b> / 5</span></div>'
+'<div id="p12-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:10px;justify-content:center"><button class="btn primary" id="p12-iv1-yes" style="background:#10b981;border-color:#10b981">Да, равнобедр.</button><button class="btn primary" id="p12-iv1-no" style="background:#dc2626;border-color:#dc2626">Нет</button></div>'
+'<div class="feedback" id="p12-iv1-fb"></div></div>';
html += '<div class="wg" id="p12-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Найди стороны</div></div>'
+'<div class="wg-help">Если 2 угла равны → треугольник равнобедр. → две стороны равны (против равных углов).</div>'
+trainerHTML('p12-iv2', 4, 'число')
+'</div>';
html += secNav('p11', 'p13') + readButton('p12');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'$\\angle A = \\angle B = 50°$', isIso:true },
{ e:'$\\angle A = 60°, \\angle B = 70°, \\angle C = 50°$', isIso:false },
{ e:'Все углы по $60°$', isIso:true },
{ e:'$\\angle A = \\angle B$ и высота из $C$ совпадает с медианой', isIso:true },
{ e:'Стороны $5, 6, 7$', isIso:false },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p12-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(12,'p12-iv1');bumpProgress('p12',28);} else if(score>=3){addXp(6,'p12-iv1');bumpProgress('p12',14);} return; }
document.getElementById('p12-iv1-i').textContent=(i+1);
document.getElementById('p12-iv1-s').textContent=score;
document.getElementById('p12-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p12-iv1-q'));
document.getElementById('p12-iv1-fb').style.display='none';
}
function ans(isYes){
if(i>=Q.length) return;
const fb=document.getElementById('p12-iv1-fb');
if(isYes===Q[i].isIso){ score++; feedback(fb,true,'&#10003; Верно!'); }
else feedback(fb,false,'&#10007; '+(Q[i].isIso?'Это <b>равнобедренный</b>':'Это <b>не</b> равнобедренный'));
document.getElementById('p12-iv1-s').textContent=score;
i++; setTimeout(show,1300);
}
document.getElementById('p12-iv1-yes').addEventListener('click',()=>ans(true));
document.getElementById('p12-iv1-no').addEventListener('click',()=>ans(false));
show();
})();
makeTrainer({
idPrefix:'p12-iv2',
questions:[
{ q:'$\\angle A = \\angle C = 70°$, $AB = 6$ см. Чему равна $BC$?', a:6 },
{ q:'$\\angle A = \\angle B$, $AC + BC = 16$, $AB = 12$ см. Найди $BC$.', a:8 },
{ q:'$\\angle A = \\angle B = 55°$, периметр = 24 см, $AC = 6$ см. Найди $AB$.', a:12 },
{ q:'$AB = AC$, $\\angle A = 100°$. Найди $\\angle B$.', a:40 },
],
onComplete:(s,n)=>{ if(s===n){addXp(12,'p12-iv2');bumpProgress('p12',22);} else if(s>=2){addXp(6,'p12-iv2');bumpProgress('p12',10);} }
});
wireReadBtn('p12');
}
/* ============================================================
\xA7 13 — Третий признак равенства
============================================================ */
function buildP13(){
const box = document.getElementById('p13-body');
const G = window.GEOM7;
let html = '';
let svgSSS='';
if(G){
const b=G.svgBox(260,160,{id:'p13-sss',cell:20});
const A={x:50,y:130}, B={x:140,y:30}, C={x:220,y:130};
svgSSS = b.open
+ G.polygon([A,B,C],{color:'#0891b2',fill:'rgba(8,145,178,.08)'})
+ G.segment(A,B,{color:'#dc2626',width:2.5,ticks:1})
+ G.segment(B,C,{color:'#7c3aed',width:2.5,ticks:2})
+ G.segment(A,C,{color:'#059669',width:2.5,ticks:3})
+ G.point(A.x,A.y,'A',{color:'#1e293b',dx:-14,dy:14})
+ G.point(B.x,B.y,'B',{color:'#1e293b',dx:-6,dy:-8})
+ G.point(C.x,C.y,'C',{color:'#1e293b',dx:8,dy:14})
+ b.close;
}
html += makeCard('rule', 'Третий признак равенства — ССС (SSS)', '13.1', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Теорема (3-й признак).</b> Если <b>три стороны</b> одного треугольника соответственно равны трём сторонам другого треугольника, то такие треугольники равны.</p>
<p>Если $AB = A_1 B_1$, $BC = B_1 C_1$, $AC = A_1 C_1$, то $\\triangle ABC = \\triangle A_1 B_1 C_1$.</p>
<div class="svg-host">`+svgSSS+`</div>
<p style="background:var(--warn-bg);padding:10px 14px;border-radius:8px;border-left:4px solid var(--warn)"><b>Запоминалка.</b> Сокращение <b>ССС</b> (сторона&nbsp;— сторона&nbsp;— сторона) — все три элемента это стороны, углы вообще не упоминаются. Латинская версия — <b>SSS</b> (Side&nbsp;— Side&nbsp;— Side).</p>
<p>Говорят, что <b>три стороны задают треугольник однозначно</b>.</p>
<p>В отличие от признаков 1 и 2, здесь <b>не требуется</b> равенство углов в условии — оно получается автоматически.</p>`);
html += makeCard('theory', 'Что НЕ является признаком', '13.2', `
<p>Не все «тройки элементов» задают треугольник однозначно:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>Три <b>угла</b>: не задают (треугольники могут отличаться по размеру).</li>
<li>Две стороны + угол, <b>не между ними</b>: не всегда задают однозначно (бывают «неоднозначные» случаи).</li>
</ul>
<p>Поэтому 3 «работающих» признака — это: <b>СУС</b> (1-й, SAS), <b>УСУ</b> (2-й, ASA), <b>ССС</b> (3-й, SSS).</p>`);
html += makeCard('example', 'Применение', '13.3', `
<p><b>Задача.</b> В простой замкнутой ломаной $ABCD$: $AB = AD$, $BC = DC$. Доказать, что $\\angle B = \\angle D$ и луч $AC$ — биссектриса $\\angle BAD$.</p>
<p><b>Решение.</b> Проведём $AC$. Рассмотрим $\\triangle ABC$ и $\\triangle ADC$:</p>
<ul style="padding-left:22px;line-height:1.9">
<li>$AB = AD$ — по условию;</li>
<li>$BC = DC$ — по условию;</li>
<li>$AC$ — общая.</li>
</ul>
<p>По <b>3-му признаку</b> $\\triangle ABC = \\triangle ADC$.</p>
<p>Отсюда $\\angle B = \\angle D$ и $\\angle BAC = \\angle DAC$, значит $AC$ — биссектриса $\\angle BAD$.</p>`);
html += '<div class="wg" id="p13-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Можно ли применить признак?</div></div>'
+'<div class="wg-help">Какой признак: 1-й (СУС), 2-й (УСУ), 3-й (ССС) или ни один? <b>СУС</b> = сторона-угол-сторона, <b>УСУ</b> = угол-сторона-угол, <b>ССС</b> = три стороны.</div>'
+'<div class="score-display"><span>Задача <b id="p13-iv1-i">1</b> / 6</span><span>Очки: <b id="p13-iv1-s">0</b> / 6</span></div>'
+'<div id="p13-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:6px;justify-content:center;flex-wrap:wrap">'
+'<button class="btn primary" id="p13-iv1-sas" style="background:#dc2626;border-color:#dc2626">СУС</button>'
+'<button class="btn primary" id="p13-iv1-asa" style="background:#7c3aed;border-color:#7c3aed">УСУ</button>'
+'<button class="btn primary" id="p13-iv1-sss" style="background:#059669;border-color:#059669">ССС</button>'
+'<button class="btn primary" id="p13-iv1-no" style="background:#64748b;border-color:#64748b">Ни один</button>'
+'</div><div class="feedback" id="p13-iv1-fb"></div></div>';
html += '<div class="wg" id="p13-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Задачи на 3-й признак</div></div>'
+'<div class="wg-help">После доказательства равенства треугольников — заключение о равенстве элементов.</div>'
+trainerHTML('p13-iv2', 4, 'число')
+'</div>';
html += secNav('p12', 'p14') + readButton('p13');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'$AB = A_1B_1$, $BC = B_1C_1$, $AC = A_1C_1$', ans:'sss' },
{ e:'$AB = A_1B_1$, $AC = A_1C_1$, $\\angle A = \\angle A_1$', ans:'sas' },
{ e:'$AC = A_1C_1$, $\\angle A = \\angle A_1$, $\\angle C = \\angle C_1$', ans:'asa' },
{ e:'$\\angle A = \\angle A_1$, $\\angle B = \\angle B_1$, $\\angle C = \\angle C_1$', ans:'no' },
{ e:'$AB = A_1B_1$, $AC = A_1C_1$, $\\angle B = \\angle B_1$ (угол не между сторонами)', ans:'no' },
{ e:'Все 3 стороны $\\triangle ABC$ равны 5, 6, 7. То же для $\\triangle KMN$', ans:'sss' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p13-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p13-iv1');bumpProgress('p13',30);} else if(score>=4){addXp(8,'p13-iv1');bumpProgress('p13',15);} return; }
document.getElementById('p13-iv1-i').textContent=(i+1);
document.getElementById('p13-iv1-s').textContent=score;
document.getElementById('p13-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p13-iv1-q'));
document.getElementById('p13-iv1-fb').style.display='none';
}
function ans(a){
if(i>=Q.length) return;
const fb=document.getElementById('p13-iv1-fb');
if(a===Q[i].ans){ score++; feedback(fb,true,'&#10003; Верно!'); }
else{ const lab={sas:'1-й (СУС)',asa:'2-й (УСУ)',sss:'3-й (ССС)',no:'ни один'}; feedback(fb,false,'&#10007; Правильно: <b>'+lab[Q[i].ans]+'</b>'); }
document.getElementById('p13-iv1-s').textContent=score;
i++; setTimeout(show,1200);
}
document.getElementById('p13-iv1-sas').addEventListener('click',()=>ans('sas'));
document.getElementById('p13-iv1-asa').addEventListener('click',()=>ans('asa'));
document.getElementById('p13-iv1-sss').addEventListener('click',()=>ans('sss'));
document.getElementById('p13-iv1-no').addEventListener('click',()=>ans('no'));
show();
})();
makeTrainer({
idPrefix:'p13-iv2',
questions:[
{ q:'$\\triangle ABC = \\triangle A_1B_1C_1$ по 3-му признаку (ССС). $\\angle A = 50°$. Чему равен $\\angle A_1$?', a:50 },
{ q:'$AB = BC$, $AD = CD$ в четырёхугольнике $ABCD$. Чему равен $\\angle BAD$ если $\\angle BCD = 80°$? (используй $\\triangle ABD = \\triangle CBD$)', a:80 },
{ q:'$AB = AD = 6$, $BC = DC = 4$. $AC$ — биссектриса $\\angle BAD$. Найди длину ломаной $ABCD$.', a:20 },
{ q:'У 4-угольника $ABCD$: $BC = AD = 10$ дм, $\\angle ACB = \\angle CAD$, $AB = 8$ дм. Найди $P_{ABCD}$.', a:36 },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p13-iv2');bumpProgress('p13',28);} else if(s>=2){addXp(8,'p13-iv2');bumpProgress('p13',14);} }
});
wireReadBtn('p13');
}
/* ============================================================
\xA7 14 — Серединный перпендикуляр к отрезку
============================================================ */
function buildP14(){
const box = document.getElementById('p14-body');
const G = window.GEOM7;
let html = '';
let svgMP='', svgCirc='';
if(G){
const b=G.svgBox(260,180,{id:'p14-mp',cell:20});
const A={x:60,y:120}, B={x:200,y:120};
const M={x:130,y:120};
const K={x:130,y:40};
svgMP = b.open
+ G.segment(A,B,{color:'#0891b2',width:2.5})
+ G.segment({x:130,y:20},{x:130,y:160},{color:'#dc2626',width:2.5})
+ G.rightAngleMark(M,B,K,{color:'#dc2626',size:10})
+ G.segment(A,M,{color:'#0891b2',width:1.5,ticks:1})
+ G.segment(M,B,{color:'#0891b2',width:1.5,ticks:1})
+ G.point(A.x,A.y,'A',{color:'#1e293b',dx:-12,dy:12})
+ G.point(B.x,B.y,'B',{color:'#1e293b',dx:8,dy:12})
+ G.point(M.x,M.y,'M',{color:'#dc2626',dx:8,dy:14,r:3})
+ G.point(K.x,K.y,'K',{color:'#dc2626',dx:8,dy:-4,r:3})
+ '<text x="130" y="174" text-anchor="middle" font-size="10" fill="#475569" font-weight="700" font-family="Unbounded">CD — серединный перпендикуляр к AB</text>'
+ b.close;
const b2=G.svgBox(280,220,{id:'p14-circ',cell:20});
/* Окружность задана центром O и радиусом R; вершины — на окружности */
const cO={x:140,y:115}, cR=70;
/* Углы (в SVG-конвенции, y вниз): vA — низ-лево, vB — низ-право, vC — верх */
const aA=140*Math.PI/180, aB=40*Math.PI/180, aC=-80*Math.PI/180;
const vA={x:cO.x+cR*Math.cos(aA), y:cO.y+cR*Math.sin(aA)};
const vB={x:cO.x+cR*Math.cos(aB), y:cO.y+cR*Math.sin(aB)};
const vC={x:cO.x+cR*Math.cos(aC), y:cO.y+cR*Math.sin(aC)};
/* Середины сторон */
const Mab={x:(vA.x+vB.x)/2,y:(vA.y+vB.y)/2};
const Mbc={x:(vB.x+vC.x)/2,y:(vB.y+vC.y)/2};
const Mac={x:(vA.x+vC.x)/2,y:(vA.y+vC.y)/2};
svgCirc = b2.open
+ G.circle(cO,cR,{color:'#059669',width:2,dash:'4 3'})
+ G.polygon([vA,vB,vC],{color:'#0891b2',fill:'rgba(8,145,178,.08)'})
/* Серединные перпендикуляры — от середины стороны до O (отрезки) */
+ G.segment(Mab,cO,{color:'#7c3aed',width:1.7,dash:'4 3'})
+ G.segment(Mbc,cO,{color:'#7c3aed',width:1.7,dash:'4 3'})
+ G.segment(Mac,cO,{color:'#7c3aed',width:1.7,dash:'4 3'})
/* Маркеры середин */
+ G.point(Mab.x,Mab.y,'',{r:2.5,color:'#7c3aed'})
+ G.point(Mbc.x,Mbc.y,'',{r:2.5,color:'#7c3aed'})
+ G.point(Mac.x,Mac.y,'',{r:2.5,color:'#7c3aed'})
+ G.point(vA.x,vA.y,'A',{color:'#1e293b',dx:-14,dy:10})
+ G.point(vB.x,vB.y,'B',{color:'#1e293b',dx:8,dy:10})
+ G.point(vC.x,vC.y,'C',{color:'#1e293b',dx:-4,dy:-8})
+ G.point(cO.x,cO.y,'O',{color:'#dc2626',dx:7,dy:5,r:3.5})
+ '<text x="140" y="212" text-anchor="middle" font-size="10" fill="#475569" font-weight="700" font-family="Unbounded">O — центр описанной окружности (OA = OB = OC)</text>'
+ b2.close;
}
html += makeCard('rule', 'Определение', '14.1', `
<p><b>Серединным перпендикуляром</b> к отрезку называется <b>прямая</b>, перпендикулярная этому отрезку и проходящая через его <b>середину</b>.</p>
<p>На рисунке: $CD$ — серединный перпендикуляр к $AB$, если $CD \\perp AB$ и $AM = MB$ (где $M$ — точка пересечения).</p>
<div class="svg-host">`+svgMP+`</div>`);
html += makeCard('rule', 'Теорема о серединном перпендикуляре', '14.2', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Теорема (ГМТ).</b> Любая точка серединного перпендикуляра к отрезку <b>равноудалена</b> от концов этого отрезка. И наоборот: если точка равноудалена от концов отрезка, то она лежит на серединном перпендикуляре.</p>
<p><b>Прямая часть.</b> Пусть $m$ — серединный перпендикуляр к $AB$, $K \\in m$. Тогда в $\\triangle AKB$ отрезок $KM$ — и высота ($\\perp$), и медиана ($AM = MB$). Значит $\\triangle AKB$ — равнобедренный (по признаку «высота = медиана»). Отсюда $KA = KB$.</p>
<p><b>Обратная часть.</b> Пусть $KA = KB$. В равнобедренном $\\triangle AKB$ высота $KM$ из вершины $K$ совпадает с медианой. Значит $KM \\perp AB$ и $AM = MB$ — то есть $K$ лежит на серединном перпендикуляре к $AB$.</p>
<p><b>Геометрическим местом точек (ГМТ)</b> называется множество всех точек, обладающих общим свойством. Серединный перпендикуляр — ГМТ точек, равноудалённых от концов отрезка.</p>`);
html += makeCard('rule', 'Центр описанной окружности', '14.3', `
<p><b>Теорема.</b> Серединные перпендикуляры к сторонам треугольника <b>пересекаются в одной точке</b>. Эта точка — <b>центр описанной</b> около треугольника окружности.</p>
<p><b>Доказательство.</b> Пусть $O$ — точка пересечения серединных перпендикуляров к $AC$ и $AB$. Тогда $OA = OC$ (точка на серединном к $AC$) и $OA = OB$ (точка на серединном к $AB$). Значит $OB = OC$, то есть $O$ равноудалена от $B$ и $C$ — лежит на серединном перпендикуляре к $BC$.</p>
<p>Так как $OA = OB = OC$, можно построить окружность с центром $O$, проходящую через все три вершины — это <b>описанная</b> окружность.</p>
<div class="svg-host">`+svgCirc+`</div>`);
html += makeCard('example', 'Замечательные точки треугольника', '14.4', `
<p>В $\\triangle$ есть несколько важных точек пересечения:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>3 биссектрисы → <b>инцентр</b> (центр вписанной окружности);</li>
<li>3 медианы → <b>центроид</b> (центр тяжести);</li>
<li>3 высоты → <b>ортоцентр</b>;</li>
<li>3 серединных перпендикуляра → <b>центр описанной окружности</b>.</li>
</ul>`);
html += '<div class="wg" id="p14-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Свойство серединного перпендикуляра</div></div>'
+'<div class="wg-help">Точка на серединном перпендикуляре к $AB$ равноудалена от $A$ и $B$.</div>'
+trainerHTML('p14-iv1', 5, 'число')
+'</div>';
html += '<div class="wg" id="p14-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Верно или нет?</div></div>'
+'<div class="wg-help">Применяй теорему: серединный перпендикуляр — ГМТ равноудалённых от концов.</div>'
+'<div class="score-display"><span>Задача <b id="p14-iv2-i">1</b> / 5</span><span>Очки: <b id="p14-iv2-s">0</b> / 5</span></div>'
+'<div id="p14-iv2-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:10px;justify-content:center"><button class="btn primary" id="p14-iv2-yes" style="background:#10b981;border-color:#10b981">Верно</button><button class="btn primary" id="p14-iv2-no" style="background:#dc2626;border-color:#dc2626">Неверно</button></div>'
+'<div class="feedback" id="p14-iv2-fb"></div></div>';
html += secNav('p13', 'final2') + readButton('p14');
box.innerHTML = html; renderMath(box);
makeTrainer({
idPrefix:'p14-iv1',
questions:[
{ q:'Точка $M$ на серединном $\\perp$ к $AB$, $MA = 8$ см. Чему равно $MB$?', a:8 },
{ q:'$M$ на серединном $\\perp$ к $AB$, $MA + MB = 16$ м. Найди $MA$.', a:8 },
{ q:'Серединный $\\perp$ к стороне $AC$ пересекает $BC$ в точке $K$. $AB = 5$, $BC = 7$. Найди $P_{ABK}$ если $AK = KC$.', a:12 },
{ q:'Точки $M$ и $K$ на серединном $\\perp$ к $AB$ по разные стороны. $MA = 16$, $KB = 12$. $P_{AMBK}$?', a:56 },
{ q:'$\\angle AMB = 84°$, $a$ — серединный $\\perp$ к $AB$, $M \\in a$. Чему равен $\\angle BMK$ если $K \\in a$ по другую сторону от $AB$?', a:42 },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p14-iv1');bumpProgress('p14',28);} else if(s>=3){addXp(8,'p14-iv1');bumpProgress('p14',14);} }
});
(function(){
const Q=[
{ e:'Любая точка серединного $\\perp$ к $AB$ равноудалена от $A$ и $B$', ok:true },
{ e:'Если $KA = KB$, то $K$ лежит на серединном $\\perp$ к $AB$', ok:true },
{ e:'Центр описанной около треугольника окружности — точка пересечения биссектрис', ok:false },
{ e:'Серединный перпендикуляр проходит через вершину треугольника', ok:false },
{ e:'Серединные $\\perp$ к сторонам треугольника пересекаются в одной точке', ok:true },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p14-iv2-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(12,'p14-iv2');bumpProgress('p14',25);} else if(score>=3){addXp(6,'p14-iv2');bumpProgress('p14',12);} return; }
document.getElementById('p14-iv2-i').textContent=(i+1);
document.getElementById('p14-iv2-s').textContent=score;
document.getElementById('p14-iv2-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p14-iv2-q'));
document.getElementById('p14-iv2-fb').style.display='none';
}
function ans(isYes){
if(i>=Q.length) return;
const fb=document.getElementById('p14-iv2-fb');
if(isYes===Q[i].ok){ score++; feedback(fb,true,'&#10003; Верно!'); }
else feedback(fb,false,'&#10007; '+(Q[i].ok?'Это верно':'Это неверно'));
document.getElementById('p14-iv2-s').textContent=score;
i++; setTimeout(show,1300);
}
document.getElementById('p14-iv2-yes').addEventListener('click',()=>ans(true));
document.getElementById('p14-iv2-no').addEventListener('click',()=>ans(false));
show();
})();
wireReadBtn('p14');
}
/* ============================================================
FINAL 2 — 6 БОССОВ
============================================================ */
const BOSSES = [
{
n:1, title:'Босс \xA78 — Треугольники', color:'#059669',
steps:[
{ q:'$AB = BC = AC$ — какой треугольник? «равностор», «равнобедр» или «разностор»', verify:(v)=>String(v).trim().toLowerCase().startsWith('равностор'), hint:'Все 3 стороны равны.' },
{ q:'$AB = 6, BC = 7, AC = 8$. Найди $P$.', verify:(v)=>+v===21, hint:'$6+7+8$.' },
{ q:'Равнобедр. с боковой 14 и основанием 8. $P$ = ?', verify:(v)=>+v===36, hint:'$2 \\cdot 14 + 8$.' },
{ q:'$P = 40$, основание в 2 раза меньше боковой. Найди основание.', verify:(v)=>+v===8, hint:'$2 \\cdot 2x + x = 40$.' },
{ q:'В $\\triangle ABC = \\triangle KMN$ $\\angle B = 60°$. Найди $\\angle M$.', verify:(v)=>+v===60, hint:'Соответствующие углы равны.' },
]
},
{
n:2, title:'Босс \xA79 — 1-й и 2-й признаки', color:'#0d9488',
steps:[
{ q:'1-й признак: сколько элементов нужно знать равными? (число)', verify:(v)=>+v===3, hint:'2 стороны + 1 угол.' },
{ q:'Какой угол важен в 1-м признаке? «между» сторонами или «прилежащий» к стороне?', verify:(v)=>String(v).trim().toLowerCase().startsWith('между'), hint:'Между двумя сторонами.' },
{ q:'2-й признак: сколько углов нужно знать? (число)', verify:(v)=>+v===2, hint:'И они прилежащие к стороне.' },
{ q:'$\\triangle ABC = \\triangle DEF$, $AB = 7$ см. Чему равна $DE$?', verify:(v)=>+v===7, hint:'Соотв. стороны.' },
{ q:'Достаточно ли знать $AB = A_1B_1$, $\\angle A = \\angle A_1$ для равенства? «да» или «нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('н'), hint:'Только 2 элемента — мало.' },
]
},
{
n:3, title:'Босс \xA710-11 — Высоты, медианы, равнобедр.', color:'#0891b2',
steps:[
{ q:'Биссектриса делит угол на $?$ равных частей', verify:(v)=>+v===2, hint:'Пополам.' },
{ q:'В равнобедр. угол при вершине $= 40°$. Найди угол при основании.', verify:(v)=>+v===70, hint:'$(180-40)/2$.' },
{ q:'В равнобедр. угол при основании $= 70°$. Найди угол при вершине.', verify:(v)=>+v===40, hint:'$180 - 2 \\cdot 70$.' },
{ q:'В равнобедр. биссектриса к основанию = ? («медиана», «высота» или «и медиана и высота»)', verify:(v)=>String(v).trim().toLowerCase().includes('и медиана и высот'), hint:'И тем и другим.' },
{ q:'Все углы равностороннего треугольника равны $?$ градусов', verify:(v)=>+v===60, hint:'$180 / 3$.' },
]
},
{
n:4, title:'Босс \xA712 — Признаки равнобедр.', color:'#7c3aed',
steps:[
{ q:'Если в треугольнике 2 угла равны → он $?$. Введи слово.', verify:(v)=>String(v).trim().toLowerCase().startsWith('равнобедр'), hint:'Обратная теорема к свойству углов при основании.' },
{ q:'$\\angle A = \\angle C = 50°$, $AB = 6$ см. Чему равна $BC$?', verify:(v)=>+v===6, hint:'Против равных углов — равные стороны.' },
{ q:'Если высота треугольника равна медиане из той же вершины → он $?$', verify:(v)=>String(v).trim().toLowerCase().startsWith('равнобедр'), hint:'Доп. признак.' },
{ q:'$\\angle A = \\angle B = 55°$, $P = 24$ см, $AC = 6$ см. Найди $AB$.', verify:(v)=>+v===12, hint:'$\\angle A = \\angle B \\Rightarrow AC = BC = 6$. $AB = 24 - 6 - 6 = 12$.' },
{ q:'$AB = BC$, $\\angle B = 80°$. Найди $\\angle A$.', verify:(v)=>+v===50, hint:'$\\angle A = \\angle C$, сумма = 180°. $\\angle A = (180-80)/2$.' },
]
},
{
n:5, title:'Босс \xA713 — 3-й признак (ССС)', color:'#db2777',
steps:[
{ q:'3-й признак (ССС) — это $?$ сторон. (число)', verify:(v)=>+v===3, hint:'Три стороны.' },
{ q:'Задают ли треугольник однозначно три угла? «да» или «нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('н'), hint:'Могут быть разные размеры.' },
{ q:'$\\triangle ABC = \\triangle KMN$ по ССС. $\\angle A = 70°$. Найди $\\angle K$.', verify:(v)=>+v===70, hint:'Соотв. углы равны.' },
{ q:'$AB = AD = 6$, $BC = DC = 4$, тогда длина ломаной $ABCD$ = ?', verify:(v)=>+v===20, hint:'$6+4+4+6$.' },
{ q:'Сколько признаков равенства треугольников всего изучено?', verify:(v)=>+v===3, hint:'СУС, УСУ, ССС.' },
]
},
{
n:6, title:'Финальный босс — Серединный перпендикуляр', color:'#dc2626',
steps:[
{ q:'Серединный $\\perp$ к $AB$: точка $K$ на нём, $KA = 5$ см. Чему равно $KB$?', verify:(v)=>+v===5, hint:'Равноудалённость.' },
{ q:'$M$ на серединном $\\perp$, $MA + MB = 20$. Найди $MA$.', verify:(v)=>+v===10, hint:'$MA = MB$, поэтому $2MA = 20$.' },
{ q:'Сколько серединных перпендикуляров у треугольника?', verify:(v)=>+v===3, hint:'К каждой стороне.' },
{ q:'Они пересекаются в $?$ точке (число)', verify:(v)=>+v===1, hint:'В одной — центр описанной.' },
{ q:'Эта точка — центр $?$ окружности треугольника («описанной» или «вписанной»)', verify:(v)=>String(v).trim().toLowerCase().startsWith('опис'), hint:'Описанная.' },
]
},
];
function buildFinal2(){
const box = document.getElementById('final2-body');
let html = '';
html += makeCard('theory', 'Что мы изучили', 'Итог', `
<p>Глава 2 — фундамент всей школьной геометрии:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>познакомились с <b>треугольником</b> и его видами;</li>
<li>доказали и применили <b>3 признака равенства</b> треугольников (СУС, УСУ, ССС);</li>
<li>научились использовать <b>высоту, медиану, биссектрису</b> и узнали о замечательных точках;</li>
<li>освоили <b>равнобедренный треугольник</b>: свойство углов при основании, признаки, биссектриса = медиана = высота к основанию;</li>
<li>доказали теорему о <b>серединном перпендикуляре</b> как ГМТ равноудалённых точек и узнали о центре <b>описанной</b> окружности.</li>
</ul>
<p>6 боссов проверяют все эти темы.</p>`);
html += '<div id="bosses-container"></div>';
html += '<div style="margin-top:22px;padding:18px 20px;background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-radius:14px;border:1.5px solid var(--pri);text-align:center">'
+'<div style="font-family:\'Unbounded\',sans-serif;font-weight:800;color:var(--pri2);font-size:1.1rem;margin-bottom:6px">Прогресс по боссам</div>'
+'<div id="boss-overall" style="font-size:.95rem;color:var(--text);margin-bottom:10px">0 / 6 боссов побеждено</div>'
+'<div style="height:14px;background:rgba(5,150,105,.12);border-radius:9px;overflow:hidden"><div id="boss-overall-fill" style="height:100%;width:0%;background:linear-gradient(90deg,#059669,#10b981);transition:width .5s"></div></div>'
+'</div>';
html += secNav('p14', null);
box.innerHTML = html; renderMath(box);
const cont = document.getElementById('bosses-container');
const BOSS_STATE = (function(){
try{ const s=localStorage.getItem('geometry7_ch2_bosses'); if(s) return JSON.parse(s); }catch(e){}
return BOSSES.map(()=>({stage:0,defeated:false}));
})();
function saveBosses(){ try{ localStorage.setItem('geometry7_ch2_bosses', JSON.stringify(BOSS_STATE)); }catch(e){} }
function refreshOverall(){
const won=BOSS_STATE.filter(b=>b.defeated).length;
const txt=document.getElementById('boss-overall'); if(txt) txt.textContent=won+' / '+BOSSES.length+' боссов побеждено';
const fill=document.getElementById('boss-overall-fill'); if(fill) fill.style.width=(won*100/BOSSES.length)+'%';
if(won>=BOSSES.length){ bumpProgress('final2',60); achievement('ch2_done','Глава 2 пройдена!'); }
}
cont.innerHTML = BOSSES.map((b,idx)=>{
return '<div class="boss-card" id="boss-card-'+idx+'" style="border-color:'+b.color+'">'
+'<div class="boss-head">'
+'<svg viewBox="0 0 24 24" fill="none" stroke="'+b.color+'" stroke-width="2.2" style="width:28px;height:28px"><polygon points="12,2 22,20 2,20"/></svg>'
+'<div class="boss-title" style="color:'+b.color+'">'+b.title+'</div>'
+'<div class="boss-stage" id="boss-'+idx+'-stage">Этап 1 / '+b.steps.length+'</div>'
+'</div>'
+'<div class="hp-boss" style="border-color:'+b.color+'66;background:'+b.color+'1a"><div class="hp-boss-fill" id="boss-'+idx+'-fill" style="width:0%;background:linear-gradient(90deg,'+b.color+',#f59e0b)"></div></div>'
+'<div class="boss-q" id="boss-'+idx+'-q" style="border-color:'+b.color+'"></div>'
+'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">'
+'<input type="text" id="boss-'+idx+'-input" class="tinp" placeholder="Ответ" style="width:160px;text-align:center">'
+'<button class="btn primary" id="boss-'+idx+'-go" style="background:'+b.color+';border-color:'+b.color+'">Атака</button>'
+'<button class="btn" id="boss-'+idx+'-hint">Подсказка</button>'
+'<button class="btn" id="boss-'+idx+'-restart">↻</button>'
+'</div>'
+'<div class="feedback" id="boss-'+idx+'-fb"></div>'
+'</div>';
}).join('');
if(window.renderMathInElement) try{ renderMath(cont); }catch(e){}
BOSSES.forEach((b,idx)=>{
function show(){
const st=BOSS_STATE[idx];
const stageEl=document.getElementById('boss-'+idx+'-stage');
const fill=document.getElementById('boss-'+idx+'-fill');
const q=document.getElementById('boss-'+idx+'-q');
const fb=document.getElementById('boss-'+idx+'-fb');
if(st.defeated){
stageEl.textContent='✓ Побеждён'; fill.style.width='100%';
q.innerHTML='<b style="color:'+b.color+'">Босс повержен!</b>';
document.getElementById('boss-'+idx+'-go').disabled=true;
document.getElementById('boss-'+idx+'-go').style.opacity=.5;
return;
}
stageEl.textContent='Этап '+(st.stage+1)+' / '+b.steps.length;
fill.style.width=(st.stage*100/b.steps.length)+'%';
q.innerHTML=b.steps[st.stage].q;
document.getElementById('boss-'+idx+'-input').value='';
fb.style.display='none';
renderMath(q);
}
document.getElementById('boss-'+idx+'-go').addEventListener('click',()=>{
const st=BOSS_STATE[idx]; if(st.defeated) return;
const step=b.steps[st.stage];
const val=document.getElementById('boss-'+idx+'-input').value;
const fb=document.getElementById('boss-'+idx+'-fb');
if(!val.trim()){ feedback(fb,false,'&#10007; Введи ответ.'); return; }
if(step.verify(val)){
st.stage++;
if(st.stage>=b.steps.length){
st.defeated=true; saveBosses();
feedback(fb,true,'&#10003; Босс '+b.n+' побеждён! +20 XP');
addXp(20,'boss-'+b.n); bumpProgress('final2',16); refreshOverall();
setTimeout(show,1400);
}else{
saveBosses(); feedback(fb,true,'&#10003; Верно! +3 XP'); addXp(3,'boss-step'); setTimeout(show,1100);
}
}else{ feedback(fb,false,'&#10007; Промах.'); }
});
document.getElementById('boss-'+idx+'-hint').addEventListener('click',()=>{
const st=BOSS_STATE[idx]; if(st.defeated) return;
const fb=document.getElementById('boss-'+idx+'-fb');
fb.className='feedback ok';
fb.innerHTML='<span style="color:#92400e">\u{1F4A1} Подсказка:</span> '+b.steps[st.stage].hint;
fb.style.display='block';
fb.style.background='var(--warn-bg)'; fb.style.color='#92400e'; fb.style.borderLeftColor='var(--warn)';
renderMath(fb);
});
document.getElementById('boss-'+idx+'-restart').addEventListener('click',()=>{
BOSS_STATE[idx]={stage:0,defeated:false}; saveBosses();
document.getElementById('boss-'+idx+'-go').disabled=false;
document.getElementById('boss-'+idx+'-go').style.opacity=1;
show(); refreshOverall();
});
show();
});
refreshOverall();
}
</script>
</body>
</html>