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

1263 lines
90 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>Геометрия 10 · Раздел 1 · «Введение в стереометрию»</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/stereo3d.js"></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; --ink:#0f172a; --muted:#64748b;
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
--pri:#2563eb; --pri2:#1d4ed8; --pri-soft:#dbeafe;
--acc:#3b82f6; --acc2:#2563eb; --acc-soft:#bfdbfe;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#0a0e14; --card:#0d1424; --card-soft:#101a30; --text:#dbeafe; --ink:#dbeafe; --muted:#93c5fd; --border:#1e3a5f}
*{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,#1e3a8a 0%,#2563eb 55%,#93c5fd 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(147,197,253,.2);min-height:130px}
.hdr::before{content:'РАЗДЕЛ 1';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(5rem,15vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.12);line-height:1;pointer-events:none;user-select:none;z-index:0}
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
.hdr-sub{font-size:.85rem;opacity:.88;margin-top:6px;font-weight:500;line-height:1.4}
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center;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:0;top:-30px;font-size:clamp(2rem,12vw,8rem);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;letter-spacing:-.01em}
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(0,0,0,.18)}
.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(0,0,0,.12);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;letter-spacing:.02em;box-shadow:0 4px 12px rgba(0,0,0,.18);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(180px,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(0,0,0,.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,var(--warn-bg),var(--pri-soft))}
.psel-card.final .psel-num{color:var(--warn)}
.psel-card .psel-done{position:absolute;top:6px;right:6px;width:18px;height:18px;border-radius:50%;background:#10b981;display:none;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(16,185,129,.45);z-index:2}
.psel-card .psel-done svg{width:11px;height:11px;stroke:#fff;fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
.psel-card.done .psel-done{display:flex}
.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(--pri-soft);line-height:1;pointer-events:none;user-select:none;z-index:0;opacity:.35}
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--pri-soft);position:relative;z-index:1}
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--pri),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.6rem;font-weight:800;color:var(--pri2);letter-spacing:-.01em;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(0,0,0,.04);position:relative;z-index:1;transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
.card:hover{transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(0,0,0,.08)}
.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.repeat{background:#0ea5e9}.card-icon.theory{background:#8b5cf6}.card-icon.algo{background:#f59e0b}.card-icon.rule{background:#ec4899}.card-icon.example{background:#10b981}.card-icon.oral{background:#06b6d4}
.card-icon .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(--pri-soft);padding:3px 7px;border-radius:5px}
.card-body{font-size:.94rem;line-height:1.65}
.card-body p{margin-bottom:8px}
.card-body p:last-child{margin-bottom:0}
.card-body ul{margin:6px 0 6px 22px;line-height:1.7}
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
.btn:hover{background:var(--pri-soft);border-color:var(--pri)}
.btn:active{transform:scale(.96)}
.btn.primary{background:var(--pri);color:#fff;border-color:var(--pri)}
.btn.primary:hover{background:var(--pri2);border-color:var(--pri2)}
.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)}
.wg{background:linear-gradient(135deg,var(--card),var(--pri-soft));border:1.5px solid 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(--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(--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),var(--pri-soft));border-left:4px solid var(--warn);padding:9px 14px;border-radius:9px}
.tinp{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);transition:border-color .15s;font-family:'JetBrains Mono',monospace;width:140px}
.tinp:focus{outline:0;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;align-items:center}
.score-display{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:10px 14px;background:var(--pri-soft);border-radius:10px;margin-bottom:12px;font-size:.92rem}
.score-display b{color:var(--pri2);font-size:1.1rem}
.spoiler{border:1px solid var(--border);border-radius:10px;background:var(--card);margin:10px 0;overflow:hidden}
.spoiler summary{padding:8px 14px;background:var(--pri-soft);font-weight:700;cursor:pointer;font-size:.88rem;color: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(--pri);width:18px}
.spoiler[open] summary::before{content:'\2212'}
.spoiler-body{padding:10px 14px;font-size:.92rem;line-height:1.6}
.opts-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
.opt-btn{padding:8px 14px;background:var(--card);border:1.5px solid var(--border);border-radius:9px;font-weight:700;font-size:.88rem;color:var(--text);cursor:pointer;transition:all .15s}
.opt-btn:hover{background:var(--pri-soft);border-color:var(--pri)}
.opt-btn.correct{background:var(--ok-bg);border-color:var(--ok);color:#065f46}
.opt-btn.wrong{background:var(--fail-bg);border-color:var(--fail);color:#991b1b}
.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}
.sidecard-row:last-child{margin-bottom:0}
@media(max-width:980px){.col-side{position:static;max-height:none}}
.xp-card{background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border:1.5px solid var(--warn);border-radius:12px;padding:14px;margin-bottom:14px}
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--warn);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:#92400e;font-family:'Unbounded',sans-serif}
.xp-bar{height:9px;background:rgba(0,0,0,.10);border-radius:6px;overflow:hidden;margin:7px 0}
.xp-fill{height:100%;background:linear-gradient(90deg,var(--warn),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}
.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,var(--pri),var(--warn));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(0,0,0,.32);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
.ach-popup.show{display:flex}
.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}
}
.viz3d{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:12px;text-align:center;margin:10px 0}
.viz3d-row{display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin:10px 0}
.viz3d-cell{flex:1 1 180px;max-width:220px;text-align:center}
.viz3d-label{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:6px}
.rot-row{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;margin-top:10px;font-size:.84rem;color:var(--muted)}
.rot-row label{display:inline-flex;align-items:center;gap:6px;font-weight:600}
.boss-card{background:var(--card);border:2px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;transition:border-color .35s,box-shadow .35s,transform .2s}
.boss-card.solved{border-color:#10b981;box-shadow:0 0 0 3px rgba(16,185,129,.18)}
.boss-head{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap}
.boss-tag{font-size:.7rem;font-weight:800;padding:3px 9px;border-radius:99px;background:var(--pri-soft);color:var(--pri2);letter-spacing:.04em;text-transform:uppercase}
.boss-title{font-family:'Unbounded',sans-serif;font-weight:800;color:var(--text);font-size:1.02rem;flex:1;min-width:0}
.boss-q{padding:12px 14px;background:var(--pri-soft);border-radius:10px;font-size:.96rem;line-height:1.55;margin-bottom:10px;color:var(--text)}
.boss-input{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-family:'JetBrains Mono',monospace;width:130px;text-align:center;font-size:.95rem}
.boss-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:6px}
.boss-fb{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none;line-height:1.45}
.boss-fb.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
.boss-fb.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
.boss-hint-txt{margin-top:8px;padding:9px 13px;background:var(--warn-bg);border-left:3px solid var(--warn);border-radius:6px;font-size:.86rem;color:var(--text);display:none;line-height:1.5}
.boss-hint-txt.show{display:block}
.stub-note{padding:18px 22px;background:linear-gradient(135deg,var(--pri-soft),var(--warn-bg));border:1.5px dashed var(--pri);border-radius:13px;text-align:center;color:var(--text);margin-bottom:14px}
.stub-note h3{font-family:'Unbounded',sans-serif;color:var(--pri2);margin-bottom:8px;font-size:1.05rem}
.stub-note p{color:var(--muted);font-size:.9rem;line-height:1.55}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Геометрия 10 · Раздел 1</h1>
<div class="hdr-sub">Введение в стереометрию · пространственные фигуры, аксиомы, сечения</div>
</div>
<div class="hdr-side">
<a href="/textbook/geometry-10" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К геометрии 10</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>Стереометрия — это геометрия в пространстве. Изучаем пространственные фигуры, аксиомы, по которым устроено 3D-пространство, и метод сечений многогранников.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p1')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 1</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-p1" class="sec" data-watermark="&#9651;"><div class="sec-header"><span class="sec-num">§ 1</span><h2 class="sec-h">Пространственные фигуры</h2></div><div id="p1-body"></div></section>
<section id="sec-p2" class="sec" data-watermark="&#9671;"><div class="sec-header"><span class="sec-num">§ 2</span><h2 class="sec-h">Прямые и плоскости</h2></div><div id="p2-body"></div></section>
<section id="sec-p3" class="sec" data-watermark="&#9674;"><div class="sec-header"><span class="sec-num">§ 3</span><h2 class="sec-h">Построения сечений</h2></div><div id="p3-body"></div></section>
<section id="sec-final" class="sec" data-watermark="&#9733;"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#d97706,#f59e0b)">&#9733;</span><h2 class="sec-h">Финал раздела</h2></div><div id="final-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">Интерактивный учебник «Геометрия 10» · Раздел 1 · «Введение в стереометрию» · 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:'p1', progress:{}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 3;
const _TB_SLUG = 'geometry-10-r1';
const PARAS = [
{ id:'p1', num:'§ 1', name:'Пространственные фигуры', sub:'Призма · пирамида · цилиндр · конус · шар' },
{ id:'p2', num:'§ 2', name:'Прямые и плоскости', sub:'3 аксиомы стереометрии + следствия' },
{ id:'p3', num:'§ 3', name:'Построения сечений', sub:'Метод следов · max 6-угольник у куба' },
{ id:'final', num:'★', name:'Финал раздела', sub:'4 интегрированных босса', final:true }
];
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
const ACH_LABELS = {
p1_done:'§1 — пространственные фигуры освоены',
p2_done:'§2 — аксиомы освоены',
p3_done:'§3 — сечения освоены',
start:'Начало раздела 1!',
r1_done:'Введение в стереометрию пройдено!'
};
function loadProgress(){
try{
const s=localStorage.getItem('geometry10_r1_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('geometry10_r1_achievements');
if(a){ const p=JSON.parse(a); 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('geometry10_xp')||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('geometry10_r1_progress', JSON.stringify(STATE.progress));
localStorage.setItem('geometry10_r1_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('geometry10_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);
}
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,'geometry10-r1-'+(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)+'%'; if((STATE.progress[k]||0)>=100) el.classList.add('done'); });
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);
}
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><div class="psel-done"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></div>';
card.addEventListener('click', ()=>goTo(p.id));
g.appendChild(card);
});
}
const BUILT=new Set();
const BUILDERS = { p1:()=>buildP1(), p2:()=>buildP2(), p3:()=>buildP3(), final:()=>buildFinal() };
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 = {
p1:{title:'Шпаргалка § 1', rows:[['Тема','Пространственные фигуры'],['Многогранники','Призма, пирамида'],['Тела вращения','Цилиндр, конус, шар'],['Эйлер','$В-Р+Г=2$'],['Призма ($n$-уг.)','$3n$ рёбер, $n+2$ грани'],['Пирамида ($n$-уг.)','$2n$ рёбер, $n+1$ грань']]},
p2:{title:'Шпаргалка § 2', rows:[['Тема','Прямые и плоскости'],['A1','3 точки $\\Rightarrow$ единственная плоскость'],['A2','2 точки прямой в $\\alpha \\Rightarrow a \\subset \\alpha$'],['A3','$\\alpha \\cap \\beta \\Rightarrow $ прямая'],['Способов задать','4 (3т, прямая+т, 2 пересек., 2 парал.)']]},
p3:{title:'Шпаргалка § 3', rows:[['Тема','Сечения многогранников'],['Метод','Метод следов'],['Куб','Max 6 сторон сечения'],['Тетраэдр','Max 4 стороны'],['Призма ($n$)','Max $n+2$ стороны'],['Парал. основанию','Подобно основанию']]},
final:{title:'Финал раздела 1', rows:[['§ 1','Пространственные фигуры'],['§ 2','Аксиомы'],['§ 3','Сечения'],['Боссы','4 интегрированных'],['Награда','+100 XP + ачивка']]}
};
const TIPS=[
{sec:'p1',html:'§ 1 — крути 3D-куб слайдерами, считай элементы по формуле Эйлера. Главное: $В-Р+Г=2$.'},
{sec:'p2',html:'§ 2 — 3 аксиомы стереометрии. Главное: через 3 неколлинеарных точки — единственная плоскость; пересечение двух плоскостей — всегда прямая.'},
{sec:'p3',html:'§ 3 — метод следов. Главное: сечение куба может иметь от 3 до 6 сторон; правильный 6-угольник получается через 6 средин рёбер.'},
{sec:'final',html:'Финал раздела 1 — 4 босса на интегральные задачи. После победы — ачивка «Введение в стереометрию пройдено».'}
];
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS[PARAS[0].id];
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];
if(tip){
html+='<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-color:var(--warn)"><h4 style="color:#92400e;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polygon points="12,2 22,20 2,20"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
}
if(STATE.achievements.size>0){
html+='<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">'+STATE.achievements.size+'</span></h4>';
[...STATE.achievements.values()].slice(-4).forEach(text=>{ html+='<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">&#10003; '+text+'</div>'; });
html+='</div>';
}
box.innerHTML=html;
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
}
function initTheme(){
const t=localStorage.getItem('geometry10_theme')||localStorage.getItem('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('geometry10_theme', dark?'dark':'light');
localStorage.setItem('theme', dark?'dark':'light');
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
});
document.getElementById('sidebar-btn').addEventListener('click', ()=>{
document.getElementById('col-side').classList.toggle('open');
document.getElementById('col-side-backdrop').classList.toggle('show');
});
document.getElementById('col-side-backdrop').addEventListener('click', ()=>{
document.getElementById('col-side').classList.remove('open');
document.getElementById('col-side-backdrop').classList.remove('show');
});
}
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){} }
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n-Math.round(n))<1e-9?String(Math.round(n)):(+n.toFixed(6)).toString(); }
const ICONS = {
repeat:'<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
theory:'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
algo:'<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
rule:'<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
example:'<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
oral:'<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
};
function makeCard(kind, title, num, body){
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно'};
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 secNavFor(curId){
const idx = PARAS.findIndex(p => p.id === curId);
const prev = idx > 0 ? PARAS[idx-1].id : null;
const next = idx < PARAS.length - 1 ? PARAS[idx+1].id : null;
const NAMES = {p1:'\xA71',p2:'\xA72',p3:'\xA73',final:'Финал'};
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 readButton(paraId){
const p = PARAS.find(x => x.id === paraId);
const labelTail = p && p.final ? 'финал' : (p ? p.num : '\xA7?');
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>'
+' Я прочитал — '+labelTail+' (+10 XP)'
+'</button></div>';
}
function wireReadBtn(paraId){
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
btn.addEventListener('click', ()=>{
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
const aId = paraId+'_done';
if(ACH_LABELS[aId]) achievement(aId);
});
}
/* ===== Boss helper (shared by §1-§3 and final) ===== */
function normalizeAns(s){
return String(s||'').toLowerCase()
.replace(/\s+/g,'').replace(/°/g,'')
.replace(/sqrt/g,'√').replace(/корень/g,'√')
.replace(/,/g,'.');
}
function makeBoss(id, def){
const stage = (typeof def.stage === 'number') ? def.stage : 0;
const solved = !!def.solved;
const total = def.stages.length;
const stageObj = def.stages[Math.min(stage, total-1)];
let optsHtml = '';
if(solved){
return '<div class="boss-card solved" id="boss-'+id+'"><div class="boss-head"><span class="boss-tag">'+(def.tag||'Босс')+'</span><span class="boss-title">'+def.title+'</span></div><div class="boss-q">Побеждён! +'+def.xp+' XP получены.</div></div>';
}
if(stageObj.type === 'mc'){
optsHtml = '<div class="opts-row">';
stageObj.opts.forEach((o,i)=>{ optsHtml += '<button class="opt-btn" data-i="'+i+'">'+o+'</button>'; });
optsHtml += '</div>';
} else {
optsHtml = '<div class="boss-row"><input class="boss-input" id="boss-'+id+'-inp" placeholder="ответ"><button class="btn primary" id="boss-'+id+'-go">Атака</button></div>';
}
return '<div class="boss-card" id="boss-'+id+'"><div class="boss-head"><span class="boss-tag">'+(def.tag||'Босс')+'</span><span class="boss-title">'+def.title+' — этап '+(stage+1)+' / '+total+'</span></div><div class="boss-q">'+stageObj.q+'</div>'+optsHtml+'<div class="boss-fb" id="boss-'+id+'-fb"></div></div>';
}
function bindBoss(id, def, state, save, onWin){
const card = document.getElementById('boss-'+id);
if(!card) return;
if(state.solved) return;
const stageObj = def.stages[state.stage];
const fb = document.getElementById('boss-'+id+'-fb');
function advance(){
state.stage++;
if(state.stage >= def.stages.length){
state.solved = true;
save();
addXp(def.xp, 'boss-'+id);
if(onWin) onWin();
} else { save(); }
rebuildBoss(id, def, state, save, onWin);
}
if(stageObj.type === 'mc'){
card.querySelectorAll('.opt-btn').forEach(btn=>{
btn.addEventListener('click', ()=>{
const i = +btn.dataset.i;
if(i === stageObj.correct){
btn.classList.add('correct');
fb.className='feedback ok'; fb.innerHTML='&#10003; Верно. '+(stageObj.explain||''); fb.style.display='block'; renderMath(fb);
setTimeout(advance, 700);
} else {
btn.classList.add('wrong');
fb.className='feedback fail'; fb.innerHTML='&#10007; Не так. '+(stageObj.explain||''); fb.style.display='block'; renderMath(fb);
}
});
});
} else {
const inp = document.getElementById('boss-'+id+'-inp');
const go = document.getElementById('boss-'+id+'-go');
function attack(){
const v = normalizeAns(inp.value);
const ans = Array.isArray(stageObj.a) ? stageObj.a.map(normalizeAns) : [normalizeAns(stageObj.a)];
if(ans.indexOf(v) >= 0){
fb.className='feedback ok'; fb.innerHTML='&#10003; Верно! '+(stageObj.explain||''); fb.style.display='block'; renderMath(fb);
setTimeout(advance, 600);
} else {
fb.className='feedback fail'; fb.innerHTML='&#10007; Не то. '+(stageObj.explain||''); fb.style.display='block'; renderMath(fb);
}
}
go.addEventListener('click', attack);
inp.addEventListener('keydown', e=>{ if(e.key==='Enter'){ e.preventDefault(); attack(); } });
}
}
function rebuildBoss(id, def, state, save, onWin){
const card = document.getElementById('boss-'+id);
if(!card) return;
card.outerHTML = makeBoss(id, Object.assign({}, def, state));
bindBoss(id, def, state, save, onWin);
renderMath(document.getElementById('boss-'+id));
}
function makeAndBindBoss(slotId, id, def, state, save, onWin){
const slot = document.getElementById(slotId);
if(!slot) return;
slot.innerHTML = makeBoss(id, Object.assign({}, def, state));
bindBoss(id, def, state, save, onWin);
renderMath(slot);
}
/* ===== STEREO3D helpers ===== */
function ensureStereo(cb){
if(window.STEREO3D) return cb();
setTimeout(()=>ensureStereo(cb), 60);
}
/* ===== §1 builder ===== */
function buildP1(){
const box = document.getElementById('p1-body'); if(!box) return;
let html = '';
html += makeCard('theory', 'Стереометрия — геометрия пространства', '§ 1.1',
'<p><b>Стереометрия</b> изучает фигуры в трёхмерном пространстве. В отличие от планиметрии, здесь добавляется глубина — третье измерение.</p>'
+ '<p>Основные тела делятся на два класса:</p>'
+ '<ul>'
+ '<li><b>Многогранники</b> — все грани плоские (призма, пирамида, параллелепипед).</li>'
+ '<li><b>Тела вращения</b> — образованы вращением плоской фигуры вокруг оси (цилиндр, конус, шар).</li>'
+ '</ul>');
html += '<div class="viz3d" id="viz1-solids-cell"><div class="viz3d-row" id="viz1-solids"></div><div class="viz3d-label">5 основных тел стереометрии</div></div>';
html += makeCard('theory', 'Элементы многогранника', '§ 1.2',
'<p><b>Многогранник</b> — тело, ограниченное конечным числом плоских многоугольников.</p>'
+ '<ul>'
+ '<li><b>Грань</b> — каждый из этих многоугольников.</li>'
+ '<li><b>Ребро</b> — общая сторона двух соседних граней.</li>'
+ '<li><b>Вершина</b> — общая точка трёх или более рёбер.</li>'
+ '<li><b>Диагональ</b> — отрезок, соединяющий две вершины, не лежащие на одной грани.</li>'
+ '</ul>'
+ '<p><b>Куб</b> $ABCDA_1B_1C_1D_1$: $В=8$, $Р=12$, $Г=6$.</p>'
+ '<div class="viz3d" id="viz1-cube"></div>'
+ '<p style="font-size:.86rem;color:var(--muted)">Подсвечены: <span style="color:#dc2626;font-weight:800">диагональ $AC_1$</span> и <span style="color:#7c3aed;font-weight:800">грань $ABB_1A_1$</span>.</p>');
html += makeCard('rule', 'Формула Эйлера', '§ 1.3',
'<p>Для любого <b>выпуклого многогранника</b>:</p>'
+ '<p style="text-align:center;margin:8px 0">$$В - Р + Г = 2$$</p>'
+ '<p>Куб: $8 - 12 + 6 = 2$ ✓. Тетраэдр: $4 - 6 + 4 = 2$ ✓.</p>'
+ '<details class="spoiler"><summary>Свойства основных тел</summary><div class="spoiler-body">'
+ '<ul>'
+ '<li>$n$-угольная призма: $В=2n$, $Р=3n$, $Г=n+2$.</li>'
+ '<li>$n$-угольная пирамида: $В=n+1$, $Р=2n$, $Г=n+1$.</li>'
+ '<li>Куб: $В=8$, $Р=12$, $Г=6$.</li>'
+ '<li>Тетраэдр: $В=4$, $Р=6$, $Г=4$.</li>'
+ '</ul>'
+ '</div></details>');
html += makeCard('theory', 'Призма и пирамида', '§ 1.4',
'<p><b>Призма</b> — многогранник с двумя равными многоугольными гранями (основаниями) в параллельных плоскостях, остальные грани — параллелограммы.</p>'
+ '<p>Виды: <b>прямая</b> (боковые рёбра $\\perp$ основанию), <b>наклонная</b>, <b>правильная</b> (прямая + правильный $n$-угольник в основании).</p>'
+ '<div class="viz3d-row" style="margin-top:10px"><div class="viz3d-cell"><div id="viz1-prism-direct"></div><div class="viz3d-label">Прямая призма</div></div><div class="viz3d-cell"><div id="viz1-prism-oblique"></div><div class="viz3d-label">Наклонная призма</div></div></div>'
+ '<p><b>Пирамида</b> — многогранник с одним основанием-многоугольником и боковыми гранями-треугольниками с общей вершиной.</p>');
html += makeCard('theory', 'Тела вращения', '§ 1.5',
'<p>Образованы вращением плоской фигуры вокруг оси:</p>'
+ '<ul>'
+ '<li><b>Цилиндр</b> — прямоугольник вокруг стороны.</li>'
+ '<li><b>Конус</b> — прямоугольный треугольник вокруг катета.</li>'
+ '<li><b>Шар</b> — полукруг вокруг диаметра.</li>'
+ '</ul>');
/* Workshop 1: Identify solid */
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 1</span><span class="wg-title">Узнай тело по описанию</span></div>'
+ '<div class="wg-help">Прочти описание и выбери подходящее тело. Решено: <b id="i1-solid-score">0</b> / 6.</div>'
+ '<div id="i1-solid-q" style="margin:8px 0"></div><div class="opts-row" id="i1-solid-opts"></div><div class="feedback" id="i1-solid-fb"></div></div>';
/* Workshop 2: Count elements (Euler) */
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 2</span><span class="wg-title">Сосчитай элементы</span></div>'
+ '<div class="wg-help">Применяй формулы для $n$-угольных призм/пирамид и формулу Эйлера. Решено: <b id="i1-count-score">0</b> / 6.</div>'
+ '<div id="i1-count-q" style="margin:8px 0"></div><div class="actions"><input class="tinp" id="i1-count-inp" placeholder="число"><button class="btn primary" id="i1-count-go">Проверить</button></div><div class="feedback" id="i1-count-fb"></div></div>';
/* Workshop 3: 3D cube rotator */
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 3</span><span class="wg-title">Куб с разных сторон</span></div>'
+ '<div class="wg-help">Крути куб слайдерами — видимые рёбра останутся сплошными, невидимые — пунктиром.</div>'
+ '<div id="i1-rot-viz" style="text-align:center"></div>'
+ '<div class="rot-row"><label>Поворот X <input id="i1-rot-x" type="range" min="-60" max="60" value="0" step="1"> <span id="i1-rot-xv" style="font-family:JetBrains Mono,monospace;color:var(--pri2)">0°</span></label>'
+ '<label>Поворот Y <input id="i1-rot-y" type="range" min="-60" max="60" value="0" step="1"> <span id="i1-rot-yv" style="font-family:JetBrains Mono,monospace;color:var(--pri2)">0°</span></label></div></div>';
/* Boss §1 */
html += '<div id="boss-1-slot"></div>';
html += readButton('p1');
html += secNavFor('p1');
box.innerHTML = html;
/* Wire workshops */
ensureStereo(()=>buildHeroSolids());
ensureStereo(()=>buildAnnotatedCube());
ensureStereo(()=>buildPrismDirect());
ensureStereo(()=>buildPrismOblique());
ensureStereo(()=>buildRotCube());
runQuizMC('i1-solid', I1_SOLID_ITEMS, 12, 'узнавание тел');
runQuizInput('i1-count', I1_COUNT_ITEMS, 15, 'счёт элементов');
/* Boss §1 */
const bossState = loadBossState('boss-1') || { stage:0, solved:false };
makeAndBindBoss('boss-1-slot', '1', BOSS_DEFS.b1, bossState,
()=>saveBossState('boss-1', bossState),
()=>{ bumpProgress('p1', 40); achievement('p1_done'); });
wireReadBtn('p1');
renderMath(box);
}
/* ===== §2 builder ===== */
function buildP2(){
const box = document.getElementById('p2-body'); if(!box) return;
let html = '';
html += makeCard('theory', '3 аксиомы стереометрии', '§ 2.1',
'<p><b>A1.</b> Через любые три точки, <b>не лежащие на одной прямой</b>, проходит <b>единственная</b> плоскость.</p>'
+ '<p><b>A2.</b> Если две точки прямой лежат в плоскости, то и <b>вся прямая</b> лежит в этой плоскости. Обозначение: $a \\subset \\alpha$.</p>'
+ '<p><b>A3.</b> Если две плоскости имеют общую точку, то они имеют <b>общую прямую</b> (линию пересечения).</p>'
+ '<div class="viz3d-row"><div class="viz3d-cell"><div id="viz2-a1"></div><div class="viz3d-label">A1: 3 точки</div></div><div class="viz3d-cell"><div id="viz2-a2"></div><div class="viz3d-label">A2: прямая в $\\alpha$</div></div><div class="viz3d-cell"><div id="viz2-a3"></div><div class="viz3d-label">A3: пересечение</div></div></div>');
html += makeCard('rule', '4 способа задать плоскость', '§ 2.2',
'<p>Из аксиомы A1 следуют 4 эквивалентных способа однозначно задать плоскость:</p>'
+ '<ul>'
+ '<li>3 точки, не лежащие на одной прямой;</li>'
+ '<li>прямая и точка вне её;</li>'
+ '<li>две пересекающиеся прямые;</li>'
+ '<li>две параллельные прямые.</li>'
+ '</ul>'
+ '<div class="viz3d-row"><div class="viz3d-cell"><div id="viz2-c1"></div><div class="viz3d-label">Прямая + точка</div></div><div class="viz3d-cell"><div id="viz2-c2"></div><div class="viz3d-label">2 пересек.</div></div><div class="viz3d-cell"><div id="viz2-c3"></div><div class="viz3d-label">2 парал.</div></div></div>');
html += makeCard('theory', 'Обозначения', '§ 2.3',
'<ul>'
+ '<li>$A \\in \\alpha$ — точка $A$ принадлежит плоскости $\\alpha$;</li>'
+ '<li>$a \\subset \\alpha$ — прямая $a$ лежит в $\\alpha$;</li>'
+ '<li>$\\alpha \\cap \\beta = c$ — плоскости пересекаются по прямой $c$;</li>'
+ '<li>$a \\parallel b$ — параллельные прямые;</li>'
+ '<li>$a \\parallel \\alpha$ — прямая параллельна плоскости.</li>'
+ '</ul>');
/* Workshop: which axiom */
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 1</span><span class="wg-title">Какая аксиома или следствие?</span></div>'
+ '<div class="wg-help">Для каждого утверждения выбери: A1, A2, A3 или следствие. Решено: <b id="i2-axiom-score">0</b> / 6.</div>'
+ '<div id="i2-axiom-q" style="margin:8px 0"></div><div class="opts-row" id="i2-axiom-opts"></div><div class="feedback" id="i2-axiom-fb"></div></div>';
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 2</span><span class="wg-title">Можно ли задать плоскость?</span></div>'
+ '<div class="wg-help">Решено: <b id="i2-plane-score">0</b> / 5.</div>'
+ '<div id="i2-plane-q" style="margin:8px 0"></div><div class="opts-row" id="i2-plane-opts"></div><div class="feedback" id="i2-plane-fb"></div></div>';
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 3</span><span class="wg-title">Сколько плоскостей?</span></div>'
+ '<div class="wg-help">Решено: <b id="i2-count-score">0</b> / 5.</div>'
+ '<div id="i2-count-q" style="margin:8px 0"></div><div class="opts-row" id="i2-count-opts"></div><div class="feedback" id="i2-count-fb"></div></div>';
html += '<div id="boss-2-slot"></div>';
html += readButton('p2');
html += secNavFor('p2');
box.innerHTML = html;
ensureStereo(()=>buildAxiomVizes());
runQuizMC('i2-axiom', I2_AXIOM_ITEMS, 12, 'аксиомы');
runQuizMC('i2-plane', I2_PLANE_ITEMS, 10, 'задание плоскости');
runQuizMC('i2-count', I2_COUNT_ITEMS, 10, 'счёт плоскостей');
const bossState = loadBossState('boss-2') || { stage:0, solved:false };
makeAndBindBoss('boss-2-slot', '2', BOSS_DEFS.b2, bossState,
()=>saveBossState('boss-2', bossState),
()=>{ bumpProgress('p2', 40); achievement('p2_done'); });
wireReadBtn('p2');
renderMath(box);
}
/* ===== §3 builder ===== */
let SECTION_STEP = 0;
function buildP3(){
const box = document.getElementById('p3-body'); if(!box) return;
let html = '';
html += makeCard('theory', 'Сечение многогранника', '§ 3.1',
'<p><b>Сечение</b> многогранника плоскостью $\\sigma$ — это многоугольник, образованный пересечением плоскости со всеми гранями многогранника.</p>'
+ '<p>Его стороны — отрезки пересечения с гранями; вершины — точки, где плоскость пересекает рёбра.</p>');
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Шаги построения</span><span class="wg-title">Сечение куба через $M, N, P$ — правильный шестиугольник</span></div>'
+ '<div class="wg-help">На рёбрах $AB$, $BC$, $CC_1$ куба отмечены середины $M$, $N$, $P$. Нажми «Шаг построения», чтобы увидеть, как через них проводится плоскость.</div>'
+ '<div id="viz3-hero" style="text-align:center;margin:10px 0"></div>'
+ '<div class="actions"><button class="btn primary" id="viz3-step-btn">Шаг построения &rarr;</button>'
+ '<span id="viz3-step-lab" style="font-family:JetBrains Mono,monospace;color:var(--pri2);font-weight:700">Шаг 1 / 4</span></div>'
+ '<div class="feedback ok" id="viz3-cap" style="display:block;margin-top:10px"></div></div>';
html += makeCard('rule', 'Метод следов', '§ 3.2',
'<p><b>Следом</b> называется линия пересечения плоскости сечения с одной из граней (обычно — с плоскостью основания).</p>'
+ '<p>Зная след в плоскости основания и одну точку выше, можно построить всё сечение, продолжая прямые и применяя аксиому A2.</p>'
+ '<div class="viz3d" id="viz3-trace"></div>'
+ '<p style="font-size:.86rem;color:var(--muted)">$M, N$ — точки на верхних рёбрах, $K$ — на боковом. <span style="color:#dc2626;font-weight:800">След</span> на основании показан пунктиром.</p>');
html += makeCard('theory', '3 типа сечений куба', '§ 3.3',
'<div class="viz3d-row"><div class="viz3d-cell"><div id="viz3-tri"></div><div class="viz3d-label">Треугольник</div></div><div class="viz3d-cell"><div id="viz3-quad"></div><div class="viz3d-label">Прямоугольник</div></div><div class="viz3d-cell"><div id="viz3-hex"></div><div class="viz3d-label">6-угольник (max)</div></div></div>'
+ '<p>Число сторон сечения $n$-гранного многогранника $\\le n$:</p>'
+ '<ul>'
+ '<li>Куб (6 граней) — max 6 сторон.</li>'
+ '<li>Тетраэдр (4 грани) — max 4 стороны.</li>'
+ '<li>$n$-угольная призма ($n+2$ граней) — max $n+2$ сторон.</li>'
+ '</ul>');
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 1</span><span class="wg-title">Какой многоугольник в сечении?</span></div>'
+ '<div class="wg-help">Решено: <b id="i3-type-score">0</b> / 6.</div>'
+ '<div id="i3-type-q" style="margin:8px 0"></div><div class="opts-row" id="i3-type-opts"></div><div class="feedback" id="i3-type-fb"></div></div>';
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 2</span><span class="wg-title">Максимальное число сторон</span></div>'
+ '<div class="wg-help">Решено: <b id="i3-max-score">0</b> / 5.</div>'
+ '<div id="i3-max-q" style="margin:8px 0"></div><div class="actions"><input class="tinp" id="i3-max-inp" placeholder="число"><button class="btn primary" id="i3-max-go">Проверить</button></div><div class="feedback" id="i3-max-fb"></div></div>';
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 3</span><span class="wg-title">Метод следов: верно или нет?</span></div>'
+ '<div class="wg-help">Решено: <b id="i3-trace-score">0</b> / 5.</div>'
+ '<div id="i3-trace-q" style="margin:8px 0"></div><div class="opts-row" id="i3-trace-opts"></div><div class="feedback" id="i3-trace-fb"></div></div>';
html += '<div id="boss-3-slot"></div>';
html += readButton('p3');
html += secNavFor('p3');
box.innerHTML = html;
ensureStereo(()=>buildSectionHero());
ensureStereo(()=>buildSectionTypes());
ensureStereo(()=>buildMethodOfTraces());
const stepBtn = document.getElementById('viz3-step-btn');
if(stepBtn){
stepBtn.addEventListener('click', ()=>{
SECTION_STEP = (SECTION_STEP + 1) % 4;
buildSectionHero();
stepBtn.textContent = SECTION_STEP === 3 ? 'Сначала' : 'Шаг построения →';
});
}
runQuizMC('i3-type', I3_TYPE_ITEMS, 14, 'тип сечения');
runQuizInput('i3-max', I3_MAX_ITEMS, 14, 'max сторон');
runQuizMC('i3-trace', I3_TRACE_ITEMS, 10, 'метод следов');
const bossState = loadBossState('boss-3') || { stage:0, solved:false };
makeAndBindBoss('boss-3-slot', '3', BOSS_DEFS.b3, bossState,
()=>saveBossState('boss-3', bossState),
()=>{ bumpProgress('p3', 40); achievement('p3_done'); });
wireReadBtn('p3');
renderMath(box);
}
/* ===== Final builder ===== */
function buildFinal(){
const box = document.getElementById('final-body'); if(!box) return;
let html = '';
html += '<div class="stub-note"><h3>Финальное испытание</h3><p>Победи 4 интегрированных боссов (элементы, аксиомы, сечения, сборная). После победы — ачивка «Введение в стереометрию пройдено» + 100 XP бонус.</p></div>';
html += '<div id="boss-f1-slot"></div>';
html += '<div id="boss-f2-slot"></div>';
html += '<div id="boss-f3-slot"></div>';
html += '<div id="boss-f4-slot"></div>';
html += '<div id="celebration" style="display:none;margin-top:18px"></div>';
html += secNavFor('final');
box.innerHTML = html;
['f1','f2','f3','f4'].forEach(id=>{
const def = FINAL_BOSS_DEFS[id];
const st = loadBossState('boss-'+id) || { stage:0, solved:false };
makeAndBindBoss('boss-'+id+'-slot', id, def, st,
()=>saveBossState('boss-'+id, st),
()=>{ checkFinalComplete(); });
});
checkFinalComplete();
renderMath(box);
}
function checkFinalComplete(){
const allBeat = ['f1','f2','f3','f4'].every(k=>{
const st = loadBossState('boss-'+k);
return st && st.solved;
});
if(!allBeat) return;
const cel = document.getElementById('celebration');
if(!cel || cel.dataset.shown === '1') return;
cel.dataset.shown = '1';
cel.style.display = 'block';
cel.innerHTML = '<div class="boss-card solved" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));text-align:center"><div style="font-family:Unbounded,sans-serif;font-size:1.4rem;font-weight:900;color:#92400e;margin-bottom:8px">&#9733; Раздел 1 пройден! &#9733;</div><div style="color:var(--text);margin-bottom:14px">Все 4 финальных босса побеждены. Введение в стереометрию — освоено.</div><div style="display:inline-block;padding:8px 18px;background:linear-gradient(135deg,#f59e0b,#dc2626);color:#fff;border-radius:99px;font-family:Unbounded,sans-serif;font-weight:800;font-size:.9rem">+ 100 XP бонус + ачивка stereo10_r1_master</div></div>';
const ach = JSON.parse(localStorage.getItem('geometry10_achievements')||'[]');
if(ach.indexOf('stereo10_r1_master') < 0){
ach.push('stereo10_r1_master');
localStorage.setItem('geometry10_achievements', JSON.stringify(ach));
addXp(100, 'r1-master');
achievement('r1_done', 'Введение в стереометрию пройдено!');
}
bumpProgress('final', 100);
}
/* ===== Boss state ===== */
function loadBossState(key){
try{ return JSON.parse(localStorage.getItem('geometry10_r1_'+key)||'null'); }catch(e){ return null; }
}
function saveBossState(key, state){
try{ localStorage.setItem('geometry10_r1_'+key, JSON.stringify(state)); }catch(e){}
}
/* ===== Quiz helpers ===== */
function runQuizMC(id, items, xpReward, label){
const state = JSON.parse(localStorage.getItem('geometry10_r1_quiz_'+id)||'null') || { idx:0, solved:0, awarded:false };
const qEl = document.getElementById(id+'-q');
const optsEl = document.getElementById(id+'-opts');
const fbEl = document.getElementById(id+'-fb');
const scoreEl = document.getElementById(id+'-score');
function save(){ localStorage.setItem('geometry10_r1_quiz_'+id, JSON.stringify(state)); }
function render(){
if(state.solved >= items.length){
qEl.innerHTML = '<b style="color:var(--ok)">Все задания решены!</b> +'+xpReward+' XP.';
optsEl.innerHTML = ''; fbEl.style.display='none';
if(scoreEl) scoreEl.textContent = state.solved;
if(!state.awarded){ state.awarded = true; save(); addXp(xpReward, 'quiz-'+id); }
return;
}
const it = items[state.idx % items.length];
qEl.innerHTML = it.q; optsEl.innerHTML = ''; fbEl.style.display='none';
if(scoreEl) scoreEl.textContent = state.solved;
it.opts.forEach((o,i)=>{
const b = document.createElement('button');
b.className = 'opt-btn'; b.innerHTML = o;
b.addEventListener('click', ()=>{
if(i === it.correct){
b.classList.add('correct');
state.solved++; state.idx++; save();
if(scoreEl) scoreEl.textContent = state.solved;
fbEl.className='feedback ok'; fbEl.innerHTML='&#10003; Верно. '+(it.explain||''); fbEl.style.display='block'; renderMath(fbEl);
setTimeout(render, 850);
} else {
b.classList.add('wrong');
fbEl.className='feedback fail'; fbEl.innerHTML='&#10007; Не так. '+(it.explain||''); fbEl.style.display='block'; renderMath(fbEl);
}
});
optsEl.appendChild(b);
});
renderMath(qEl);
}
render();
}
function runQuizInput(id, items, xpReward, label){
const state = JSON.parse(localStorage.getItem('geometry10_r1_quiz_'+id)||'null') || { idx:0, solved:0, awarded:false };
const qEl = document.getElementById(id+'-q');
const inp = document.getElementById(id+'-inp');
const go = document.getElementById(id+'-go');
const fbEl = document.getElementById(id+'-fb');
const scoreEl = document.getElementById(id+'-score');
function save(){ localStorage.setItem('geometry10_r1_quiz_'+id, JSON.stringify(state)); }
function render(){
if(state.solved >= items.length){
qEl.innerHTML = '<b style="color:var(--ok)">Все задания решены!</b> +'+xpReward+' XP.';
inp.value=''; inp.disabled=true; go.disabled=true;
if(scoreEl) scoreEl.textContent = state.solved;
if(!state.awarded){ state.awarded = true; save(); addXp(xpReward, 'quiz-'+id); }
return;
}
const it = items[state.idx % items.length];
qEl.innerHTML = it.q; inp.value=''; inp.disabled=false; go.disabled=false; fbEl.style.display='none';
if(scoreEl) scoreEl.textContent = state.solved;
renderMath(qEl);
}
function check(){
const it = items[state.idx % items.length];
const v = normalizeAns(inp.value);
const ans = Array.isArray(it.answer) ? it.answer.map(normalizeAns) : [normalizeAns(it.answer)];
if(ans.indexOf(v) >= 0){
state.solved++; state.idx++; save();
if(scoreEl) scoreEl.textContent = state.solved;
fbEl.className='feedback ok'; fbEl.innerHTML='&#10003; Верно. '+(it.explain||''); fbEl.style.display='block'; renderMath(fbEl);
setTimeout(render, 850);
} else {
fbEl.className='feedback fail'; fbEl.innerHTML='&#10007; Не так. '+(it.explain||''); fbEl.style.display='block'; renderMath(fbEl);
}
}
go.addEventListener('click', check);
inp.addEventListener('keydown', e=>{ if(e.key==='Enter'){ e.preventDefault(); check(); } });
render();
}
/* ===== Stereo3D viz builders ===== */
function buildHeroSolids(){
const S = window.STEREO3D; if(!S) return;
const defs = [
{ label:'Призма (6-уг)', fn:sc=>sc.addPrism({n:6,baseRadius:1.1,height:1.8,color:'#dbeafe'}) },
{ label:'Пирамида (4-уг)', fn:sc=>sc.addPyramid({n:4,baseRadius:1.2,height:1.9,color:'#fee2e2'}) },
{ label:'Цилиндр', fn:sc=>sc.addCylinder({segments:28,baseRadius:1.0,height:1.9,color:'#d1fae5'}) },
{ label:'Конус', fn:sc=>sc.addCone({segments:28,baseRadius:1.1,height:2.0,color:'#fef3c7'}) },
{ label:'Шар', fn:sc=>sc.addSphere({center:[0,0,0],radius:1.2,color:'#ede9fe'}) }
];
const row = document.getElementById('viz1-solids'); if(!row) return;
row.innerHTML = '';
defs.forEach(d=>{
const sc = new S.Scene(180, 170, {view:'CABINET', scale:34, bg:'transparent', border:'none'});
d.fn(sc);
const cell = document.createElement('div');
cell.className = 'viz3d-cell';
cell.innerHTML = sc.render() + '<div class="viz3d-label">'+d.label+'</div>';
row.appendChild(cell);
});
}
function buildAnnotatedCube(){
const S = window.STEREO3D; if(!S) return;
const sc = new S.Scene(380, 280, {view:'CABINET', scale:50});
sc.addCube({center:[0,0,0], size:2.0, labels:true});
sc.addEdge([-1,-1,-1],[1,1,1], {stroke:'#dc2626', width:3, dash:'5 3'});
sc.addFace([[-1,-1,-1],[1,-1,-1],[1,-1,1],[-1,-1,1]], {fill:'#a78bfa', opacity:0.30, stroke:'none'});
const el = document.getElementById('viz1-cube'); if(el) el.innerHTML = sc.render();
}
function buildPrismDirect(){
const S = window.STEREO3D; if(!S) return;
const sc = new S.Scene(200, 200, {view:'CABINET', scale:36});
sc.addPrism({n:6, baseRadius:1.2, height:2.0, color:'#dbeafe'});
const el = document.getElementById('viz1-prism-direct'); if(el) el.innerHTML = sc.render();
}
function buildPrismOblique(){
const S = window.STEREO3D; if(!S) return;
const sc = new S.Scene(200, 200, {view:'CABINET', scale:36});
const n=6, r=1.2, h=2.0, shift=0.7;
const bottom=[], top=[];
for(let i=0;i<n;i++){
const a = 2*Math.PI*i/n - Math.PI/2;
bottom.push([r*Math.cos(a), r*Math.sin(a), -h/2]);
top.push([r*Math.cos(a)+shift, r*Math.sin(a), +h/2]);
}
const verts = bottom.concat(top);
const faces=[]; const bi=[]; for(let k=0;k<n;k++) bi.push(k);
const ti=[]; for(let k2=0;k2<n;k2++) ti.push(k2+n);
faces.push(bi); faces.push(ti.slice().reverse());
for(let j=0;j<n;j++){ const jj=(j+1)%n; faces.push([j,jj,jj+n,j+n]); }
const edges=[]; for(let e=0;e<n;e++){ const ee=(e+1)%n; edges.push([e,ee]); edges.push([e+n,ee+n]); edges.push([e,e+n]); }
sc._addPolyhedron(verts, faces, edges, [], {color:'#fef3c7', opacity:0.4});
const el = document.getElementById('viz1-prism-oblique'); if(el) el.innerHTML = sc.render();
}
function buildRotCube(){
const S = window.STEREO3D; if(!S) return;
const holder = document.getElementById('i1-rot-viz');
const rx = document.getElementById('i1-rot-x'); if(!rx || !holder) return;
const ry = document.getElementById('i1-rot-y');
const rxv = document.getElementById('i1-rot-xv');
const ryv = document.getElementById('i1-rot-yv');
function draw(){
const ax = +rx.value * Math.PI/180;
const ay = +ry.value * Math.PI/180;
rxv.textContent = rx.value+'\xB0';
ryv.textContent = ry.value+'\xB0';
const sc = new S.Scene(340, 260, {view:'CABINET', scale:54, rotX:ax, rotY:ay});
sc.addCube({center:[0,0,0], size:2.0, labels:true, color:'#dbeafe'});
holder.innerHTML = sc.render();
}
rx.addEventListener('input', draw);
ry.addEventListener('input', draw);
draw();
}
function buildAxiomVizes(){
const S = window.STEREO3D; if(!S) return;
(()=>{
const sc = new S.Scene(200, 180, {view:'CABINET', scale:38});
sc.addPlane([0,0,0],[0,0,1],{size:1.8, label:'α'});
sc.addVertex([-0.9,-0.5,0],'A',{dx:-12,dy:-8,color:'#dc2626'});
sc.addVertex([0.9,-0.4,0],'B',{dx:8,dy:-8,color:'#dc2626'});
sc.addVertex([0.2,0.9,0],'C',{dx:8,dy:-8,color:'#dc2626'});
const el = document.getElementById('viz2-a1'); if(el) el.innerHTML = sc.render();
})();
(()=>{
const sc = new S.Scene(200, 180, {view:'CABINET', scale:38});
sc.addPlane([0,0,0],[0,0,1],{size:1.8, label:'α'});
sc.addEdge([-1.4,-0.7,0],[1.4,0.7,0],{stroke:'#dc2626', width:2.4});
sc.addLabel('a',[1.4,0.7,0],{dx:12,dy:-4,color:'#dc2626',fontSize:14,anchor:'start'});
const el = document.getElementById('viz2-a2'); if(el) el.innerHTML = sc.render();
})();
(()=>{
const sc = new S.Scene(200, 180, {view:'CABINET', scale:38});
sc.addPlane([0,0,0],[0,0,1],{size:1.4, label:'α', opacity:0.20});
sc.addPlane([0,0,0],[Math.cos(Math.PI/4),0,Math.sin(Math.PI/4)],{size:1.4, label:'β', opacity:0.22, fill:'#fde047'});
sc.addEdge([-1.2,0,0],[1.2,0,0],{stroke:'#dc2626', width:2.6});
sc.addLabel('l',[1.2,0,0],{dx:12,dy:-4,color:'#dc2626',fontSize:14,anchor:'start'});
const el = document.getElementById('viz2-a3'); if(el) el.innerHTML = sc.render();
})();
(()=>{
const sc = new S.Scene(200, 180, {view:'CABINET', scale:38});
sc.addPlane([0,0,0],[0,0,1],{size:1.8, label:'α'});
sc.addEdge([-1.4,-0.4,0],[1.4,0.4,0],{stroke:'#1e3a8a', width:2.2});
sc.addLabel('a',[1.4,0.4,0],{dx:12,dy:-4,color:'#1e3a8a',fontSize:13,anchor:'start'});
sc.addVertex([0.3,-0.8,0],'M',{dx:8,dy:-8,color:'#dc2626'});
const el = document.getElementById('viz2-c1'); if(el) el.innerHTML = sc.render();
})();
(()=>{
const sc = new S.Scene(200, 180, {view:'CABINET', scale:38});
sc.addPlane([0,0,0],[0,0,1],{size:1.8, label:'α'});
sc.addEdge([-1.4,-0.7,0],[1.4,0.7,0],{stroke:'#1e3a8a', width:2.2});
sc.addEdge([-0.9,0.8,0],[1.0,-0.9,0],{stroke:'#dc2626', width:2.2});
sc.addVertex([0.05,0.04,0],'O',{dx:8,dy:-10,color:'#0b1d33'});
const el = document.getElementById('viz2-c2'); if(el) el.innerHTML = sc.render();
})();
(()=>{
const sc = new S.Scene(200, 180, {view:'CABINET', scale:38});
sc.addPlane([0,0,0],[0,0,1],{size:1.8, label:'α'});
sc.addEdge([-1.4,-0.5,0],[1.4,-0.5,0],{stroke:'#1e3a8a', width:2.2});
sc.addEdge([-1.4,0.5,0],[1.4,0.5,0],{stroke:'#dc2626', width:2.2});
const el = document.getElementById('viz2-c3'); if(el) el.innerHTML = sc.render();
})();
}
function buildSectionHero(){
const S = window.STEREO3D; if(!S) return;
const sc = new S.Scene(400, 320, {view:'CABINET', scale:55});
sc.addCube({center:[0,0,0], size:2.0, labels:true, color:'#dbeafe', opacity:0.18});
if(SECTION_STEP >= 1){
sc.addVertex([0,-1,-1],'M',{dx:-14,dy:14,color:'#dc2626'});
sc.addVertex([1,0,-1],'N',{dx:10,dy:14,color:'#dc2626'});
sc.addVertex([1,1,0],'P',{dx:14,dy:-2,color:'#dc2626'});
}
if(SECTION_STEP >= 2){
sc.addEdge([0,-1,-1],[1,0,-1],{stroke:'#dc2626', width:2.6});
sc.addEdge([1,0,-1],[1,1,0],{stroke:'#dc2626', width:2.6});
}
if(SECTION_STEP >= 3){
sc.addEdge([1,1,0],[0,1,1],{stroke:'#dc2626', width:2.6});
sc.addEdge([0,1,1],[-1,0,1],{stroke:'#dc2626', width:2.6});
sc.addEdge([-1,0,1],[-1,-1,0],{stroke:'#dc2626', width:2.6});
sc.addEdge([-1,-1,0],[0,-1,-1],{stroke:'#dc2626', width:2.6});
sc.addVertex([0,1,1],'R',{dx:-12,dy:-10,color:'#dc2626'});
sc.addVertex([-1,0,1],'S',{dx:-22,dy:-4,color:'#dc2626'});
sc.addVertex([-1,-1,0],'Q',{dx:-22,dy:6,color:'#dc2626'});
}
if(SECTION_STEP >= 4){
sc.addFace([[0,-1,-1],[1,0,-1],[1,1,0],[0,1,1],[-1,0,1],[-1,-1,0]], {fill:'#fca5a5', opacity:0.45, stroke:'#dc2626', strokeWidth:2});
}
const el = document.getElementById('viz3-hero'); if(el) el.innerHTML = sc.render();
const lab = document.getElementById('viz3-step-lab');
if(lab) lab.textContent = 'Шаг '+(SECTION_STEP+1)+' / 4';
const cap = document.getElementById('viz3-cap');
const capTexts = [
'Шаг 1: на рёбрах $AB$, $BC$, $CC_1$ отмечены середины $M, N, P$.',
'Шаг 2: точки $M, N, P$ определяют плоскость. Отрезки $MN$ и $NP$ лежат на гранях куба.',
'Шаг 3: продолжая прямые, находим ещё 3 точки $R$, $S$, $Q$ — итого 6 вершин.',
'Шаг 4: готовый многоугольник $MNPRSQ$ — <b>правильный шестиугольник</b>.'
];
if(cap){ cap.innerHTML = capTexts[SECTION_STEP]; renderMath(cap); }
}
function buildSectionTypes(){
const S = window.STEREO3D; if(!S) return;
(()=>{
const sc = new S.Scene(200, 200, {view:'CABINET', scale:38});
sc.addCube({center:[0,0,0], size:2.0, labels:false, color:'#dbeafe', opacity:0.18});
sc.addFace([[-0.4,-1,-1],[1,0.4,-1],[1,-1,0.4]], {fill:'#fde047', opacity:0.55, stroke:'#d97706', strokeWidth:2});
const el = document.getElementById('viz3-tri'); if(el) el.innerHTML = sc.render();
})();
(()=>{
const sc = new S.Scene(200, 200, {view:'CABINET', scale:38});
sc.addCube({center:[0,0,0], size:2.0, labels:false, color:'#dbeafe', opacity:0.18});
sc.addFace([[-1,0,-1],[1,0,-1],[1,0,1],[-1,0,1]], {fill:'#86efac', opacity:0.55, stroke:'#059669', strokeWidth:2});
const el = document.getElementById('viz3-quad'); if(el) el.innerHTML = sc.render();
})();
(()=>{
const sc = new S.Scene(200, 200, {view:'CABINET', scale:38});
sc.addCube({center:[0,0,0], size:2.0, labels:false, color:'#dbeafe', opacity:0.18});
sc.addFace([[0,-1,-1],[1,0,-1],[1,1,0],[0,1,1],[-1,0,1],[-1,-1,0]], {fill:'#fca5a5', opacity:0.55, stroke:'#dc2626', strokeWidth:2});
const el = document.getElementById('viz3-hex'); if(el) el.innerHTML = sc.render();
})();
}
function buildMethodOfTraces(){
const S = window.STEREO3D; if(!S) return;
const sc = new S.Scene(400, 320, {view:'CABINET', scale:55});
sc.addCube({center:[0,0,0], size:2.0, labels:true, color:'#dbeafe', opacity:0.18});
const M = [-0.3, -1, 1]; const N = [1, 0.3, 1]; const K = [1, 1, -0.4];
sc.addVertex(M, 'M', {dx:-14, dy:-8, color:'#dc2626'});
sc.addVertex(N, 'N', {dx:12, dy:-8, color:'#dc2626'});
sc.addVertex(K, 'K', {dx:14, dy:6, color:'#dc2626'});
sc.addEdge(M, N, {stroke:'#dc2626', width:2.4});
sc.addEdge(N, K, {stroke:'#dc2626', width:2.4});
sc.addEdge([-0.3,-1,-1],[1,0.3,-1], {stroke:'#d97706', width:2.4, dash:'5 3'});
sc.addLabel('след', [-0.5,-0.4,-1], {color:'#d97706', fontSize:13, dy:14});
const el = document.getElementById('viz3-trace'); if(el) el.innerHTML = sc.render();
}
/* ===== Quiz data ===== */
const I1_SOLID_ITEMS = [
{ q:'Тело с двумя равными $n$-угольными гранями в параллельных плоскостях и $n$ параллелограммами по бокам.', opts:['Призма','Пирамида','Цилиндр','Конус'], correct:0, explain:'Это определение призмы.' },
{ q:'Тело с одной многоугольной гранью и треугольными боковыми гранями с общей вершиной.', opts:['Призма','Пирамида','Цилиндр','Конус'], correct:1, explain:'Пирамида.' },
{ q:'Вращение прямоугольника вокруг одной из его сторон даёт…', opts:['Шар','Цилиндр','Конус','Призму'], correct:1, explain:'Цилиндр.' },
{ q:'Вращение прямоугольного треугольника вокруг катета даёт…', opts:['Конус','Цилиндр','Шар','Пирамиду'], correct:0, explain:'Конус.' },
{ q:'Вращение полукруга вокруг диаметра даёт…', opts:['Цилиндр','Конус','Шар','Тор'], correct:2, explain:'Шар.' },
{ q:'Многогранник с 4 равными правильными треугольниками — это…', opts:['Куб','Тетраэдр','Октаэдр','Икосаэдр'], correct:1, explain:'Правильный тетраэдр.' }
];
const I1_COUNT_ITEMS = [
{ q:'Сколько рёбер у шестиугольной призмы?', answer:'18', explain:'$3n = 18$.' },
{ q:'Сколько граней у пятиугольной пирамиды?', answer:'6', explain:'$n+1 = 6$.' },
{ q:'Сколько вершин у куба?', answer:'8', explain:'8 вершин.' },
{ q:'Сколько пространственных диагоналей у параллелепипеда?', answer:'4', explain:'4 диагонали.' },
{ q:'Многогранник: $В=6, Г=8$. Найди $Р$ по Эйлеру.', answer:'12', explain:'$6-Р+8=2 \\Rightarrow Р=12$. Октаэдр.' },
{ q:'Сколько рёбер у 7-угольной пирамиды?', answer:'14', explain:'$2n = 14$.' }
];
const I2_AXIOM_ITEMS = [
{ q:'«Через три точки $A, B, C$ проведена единственная плоскость».', opts:['A1','A2','A3','Следствие'], correct:0, explain:'Это A1.' },
{ q:'«Точки $M, N \\in \\alpha$, значит $MN \\subset \\alpha$».', opts:['A1','A2','A3','Следствие'], correct:1, explain:'A2.' },
{ q:'«Плоскости пересекаются — у них есть общая прямая».', opts:['A1','A2','A3','Следствие'], correct:2, explain:'A3.' },
{ q:'«Через прямую и точку вне её — единственная плоскость».', opts:['A1','A2','A3','Следствие'], correct:3, explain:'Следствие из A1.' },
{ q:'«Через две пересекающиеся прямые — единственная плоскость».', opts:['A1','A2','A3','Следствие'], correct:3, explain:'Следствие.' },
{ q:'«Если 2 точки прямой в плоскости — прямая в плоскости».', opts:['A1','A2','A3','Следствие'], correct:1, explain:'A2.' }
];
const I2_PLANE_ITEMS = [
{ q:'Даны 3 неколлинеарных точки. Можно задать плоскость?', opts:['Да, единственную','Бесконечно','Нельзя'], correct:0, explain:'A1.' },
{ q:'Даны 3 точки на одной прямой. Сколько плоскостей?', opts:['Одна','Бесконечно','Ни одной'], correct:1, explain:'Бесконечно вращающихся вокруг прямой.' },
{ q:'Через 2 пересекающиеся прямые проходит…', opts:['1 плоскость','2','Бесконечно'], correct:0, explain:'Единственная.' },
{ q:'Через 2 скрещивающиеся прямые можно провести одну плоскость?', opts:['Да','Нет','Иногда'], correct:1, explain:'Скрещ. — определение: не в одной плоскости.' },
{ q:'Через прямую и точку вне её — сколько плоскостей?', opts:['1','2','Бесконечно'], correct:0, explain:'Единственная.' }
];
const I2_COUNT_ITEMS = [
{ q:'Сколько плоскостей через 4 точки общего положения?', opts:['1','3','4','6'], correct:2, explain:'$C_4^3=4$.' },
{ q:'Через 2 параллельные прямые проходит…', opts:['1 плоскость','2','Бесконечно'], correct:0, explain:'Единственная.' },
{ q:'Через сколько точек проходит не более 1 плоскости?', opts:['2','3','4','5'], correct:1, explain:'3 точки определяют плоскость.' },
{ q:'Сколько общих точек у 2 пересекающихся плоскостей?', opts:['Одна','Конечно','Бесконечно'], correct:2, explain:'По прямой = бесконечно.' },
{ q:'Сколько плоскостей содержат данную прямую?', opts:['1','2','Бесконечно'], correct:2, explain:'Через прямую — бесконечно плоскостей.' }
];
const I3_TYPE_ITEMS = [
{ q:'Плоскость пересекает 3 ребра, выходящих из одной вершины куба, близко к ней. Сечение?', opts:['Треугольник','4-угольник','6-угольник'], correct:0, explain:'Угловой срез = треугольник.' },
{ q:'Плоскость параллельна одной грани куба. Сечение?', opts:['Треугольник','Квадрат','6-угольник','Эллипс'], correct:1, explain:'Квадрат, равный основанию.' },
{ q:'Плоскость через 6 средин рёбер на 3 парах противоположных граней. Сечение?', opts:['Треугольник','Прямоугольник','Правильный 6-угольник','Окружность'], correct:2, explain:'Правильный шестиугольник.' },
{ q:'Сечение куба может быть пятиугольником?', opts:['Да','Нет','Только при наклоне'], correct:0, explain:'Да: плоскость пересекает 5 граней.' },
{ q:'Max сторон сечения тетраэдра?', opts:['3','4','5','6'], correct:1, explain:'4 грани ⇒ max 4.' },
{ q:'Сечение, параллельное основанию пирамиды, подобно…', opts:['Боковой грани','Основанию','Высоте','Не подобно'], correct:1, explain:'Подобно основанию.' }
];
const I3_MAX_ITEMS = [
{ q:'Max сторон сечения куба?', answer:'6', explain:'6 граней.' },
{ q:'Max сторон сечения тетраэдра?', answer:'4', explain:'4 грани.' },
{ q:'Max сторон сечения 6-угольной призмы?', answer:'8', explain:'$6+2=8$ граней.' },
{ q:'Сечение 5-угольной пирамиды — max сторон?', answer:'5', explain:'5 граней (без второго основания).' },
{ q:'У многогранника $Г=10$ граней. Max сторон сечения?', answer:'10', explain:'Не больше числа граней.' }
];
const I3_TRACE_ITEMS = [
{ q:'След плоскости сечения — это её линия пересечения с гранью.', opts:['Верно','Неверно'], correct:0, explain:'Точно.' },
{ q:'Плоскость, параллельная основанию пирамиды, имеет след на основании.', opts:['Верно','Неверно'], correct:1, explain:'Парал. плоскости не пересекаются.' },
{ q:'Зная след в основании и одну точку выше, можно построить всё сечение.', opts:['Верно','Неверно'], correct:0, explain:'Это и есть метод следов.' },
{ q:'След проводится с помощью аксиомы A3.', opts:['Верно','Неверно'], correct:0, explain:'Да, A3 — пересечение плоскостей.' },
{ q:'Сечение всегда лежит в одной плоскости.', opts:['Верно','Неверно'], correct:0, explain:'По определению.' }
];
/* ===== Bosses ===== */
const BOSS_DEFS = {
b1: { title:'Босс §1 — Пространственные фигуры', tag:'§1', xp:60, stages:[
{ q:'Сколько рёбер у пятиугольной призмы?', type:'input', a:'15', explain:'$3n=15$.' },
{ q:'Вершин у $n$-угольной пирамиды при $n=8$?', type:'input', a:'9', explain:'$n+1=9$.' },
{ q:'Вращение прямоугольника вокруг стороны даёт…', type:'mc', opts:['Шар','Цилиндр','Конус','Призму'], correct:1, explain:'Цилиндр.' },
{ q:'Пространственных диагоналей у куба?', type:'input', a:'4', explain:'4.' },
{ q:'$В=12, Р=30$. Найди $Г$ по Эйлеру.', type:'input', a:'20', explain:'$12-30+Г=2 \\Rightarrow Г=20$. Икосаэдр.' }
]},
b2: { title:'Босс §2 — Прямые и плоскости', tag:'§2', xp:65, stages:[
{ q:'Через прямую проходит сколько плоскостей?', type:'mc', opts:['1','2','3','Бесконечно'], correct:3, explain:'Бесконечно.' },
{ q:'Через 3 точки на одной прямой?', type:'mc', opts:['1','2','3','Бесконечно'], correct:3, explain:'Бесконечно.' },
{ q:'2 плоскости с общей точкой пересекаются по…', type:'mc', opts:['Точке','Прямой','Плоскости','Не пересек.'], correct:1, explain:'По прямой (A3).' },
{ q:'Сколько способов задать плоскость?', type:'mc', opts:['2','3','4','5'], correct:2, explain:'4 способа.' },
{ q:'Сколько плоскостей задают 4 точки общего положения?', type:'input', a:'4', explain:'$C_4^3=4$.' }
]},
b3: { title:'Босс §3 — Построения сечений', tag:'§3', xp:70, stages:[
{ q:'Сечение куба может быть шестиугольником?', type:'mc', opts:['Да','Нет','Только наклонный'], correct:0, explain:'Да.' },
{ q:'Max сторон сечения куба?', type:'input', a:'6', explain:'6.' },
{ q:'Max сторон сечения тетраэдра?', type:'input', a:'4', explain:'4.' },
{ q:'Плоскость, парал. основанию пирамиды, даёт сечение, $?$ основанию.', type:'mc', opts:['Равное','Подобное','Перпендикулярное','Совпадающее'], correct:1, explain:'Подобное.' },
{ q:'Сечение куба через 6 средин рёбер — это…', type:'mc', opts:['Квадрат','Треугольник','Правильный 6-угольник','Эллипс'], correct:2, explain:'Правильный 6-угольник.' }
]}
};
const FINAL_BOSS_DEFS = {
f1: { title:'Финал · Элементы тел', tag:'Финал R1', xp:35, stages:[
{ q:'Рёбер у 7-угольной призмы?', type:'input', a:'21', explain:'$3n=21$.' },
{ q:'Граней у 6-угольной пирамиды?', type:'input', a:'7', explain:'$n+1=7$.' },
{ q:'Вершин у тетраэдра?', type:'input', a:'4', explain:'4.' },
{ q:'$В=20, Р=30 \\Rightarrow Г=?$', type:'input', a:'12', explain:'$Г=12$. Додекаэдр.' }
]},
f2: { title:'Финал · Аксиомы', tag:'Финал R1', xp:35, stages:[
{ q:'Через прямую и точку вне её проходит:', type:'mc', opts:['1 плоскость','2','Бесконечно'], correct:0, explain:'Единственная.' },
{ q:'Две плоскости пересекаются по:', type:'mc', opts:['Точке','Прямой','Плоскости','Не пересек.'], correct:1, explain:'По прямой.' },
{ q:'Если 2 точки прямой в плоскости, то…', type:'mc', opts:['$a \\parallel \\alpha$','$a \\subset \\alpha$','$a \\cap \\alpha$','Неверно'], correct:1, explain:'A2.' },
{ q:'Скрещ. прямые лежат в одной плоскости?', type:'mc', opts:['Да','Нет','Иногда'], correct:1, explain:'По определению — нет.' }
]},
f3: { title:'Финал · Сечения', tag:'Финал R1', xp:35, stages:[
{ q:'Сечением куба может быть 5-угольник?', type:'mc', opts:['Да','Нет','Только правильный'], correct:0, explain:'Да.' },
{ q:'Max сторон сечения параллелепипеда?', type:'input', a:'6', explain:'$n+2=6$.' },
{ q:'Парал. основанию призмы сечение:', type:'mc', opts:['Подобное','Равное','Меньше','$\\perp$'], correct:1, explain:'Равное основанию.' },
{ q:'След плоскости сечения — это:', type:'mc', opts:['Точка','Линия пересечения с гранью','Касательная','Высота'], correct:1, explain:'Линия пересечения.' }
]},
f4: { title:'Финал · Сборная', tag:'Финал R1', xp:45, stages:[
{ q:'Куб: сколько рёбер пересекает плоскость, если сечение — 6-угольник?', type:'input', a:'6', explain:'6 сторон ⇒ 6 рёбер.' },
{ q:'$В=8, Р=12, Г=6$. Что это?', type:'mc', opts:['Тетраэдр','Куб','Октаэдр','Додекаэдр'], correct:1, explain:'Куб.' },
{ q:'Пирамида с 5-уг. основанием. Парал. сечение имеет сторон:', type:'mc', opts:['3','4','5','6'], correct:2, explain:'5 (подобно основанию).' },
{ q:'Сколько плоскостей задают вершины тетраэдра?', type:'input', a:'4', explain:'$C_4^3=4$.' },
{ q:'Боковых рёбер у 9-уг. призмы?', type:'input', a:'9', explain:'$n=9$.' }
]}
};
/* ===== Init ===== */
function init(){
loadProgress();
initTheme();
buildParaSelector();
goTo('p1');
refreshProgressUI();
if(!STATE.achievements.has('start')) achievement('start');
}
if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
</script>
</body>
</html>