diff --git a/frontend/textbooks/algebra_8.html b/frontend/textbooks/algebra_8.html index c67e6b9..382bdaa 100644 --- a/frontend/textbooks/algebra_8.html +++ b/frontend/textbooks/algebra_8.html @@ -65,6 +65,8 @@ input,select,textarea{font-family:inherit} .hero h2{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} +.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(233,30,99,.22);font-family:'Unbounded',sans-serif} +.hero-xp-badge svg{flex-shrink:0} .btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s} .btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(233,30,99,.28)} .btn-secondary{padding:10px 18px;background:var(--card);color:var(--pri2);border:1.5px solid var(--pri);border-radius:11px;font-weight:700;font-size:.88rem;transition:background .15s} @@ -784,6 +786,7 @@ input,select,textarea{font-family:inherit}
0% +
@@ -1031,8 +1034,15 @@ function loadProgress(){ } const sb = localStorage.getItem('algebra8_ch1_squaresBest'); if(sb) STATE.squaresBest = +sb; - const xp = localStorage.getItem('algebra8_ch1_xp'); - if(xp){ STATE.xp = +xp; STATE.level = calcLevel(STATE.xp); } + // Общий XP для всех глав. Если ещё нет — собираем из старых ch1/ch2 ключей (single-shot миграция). + let xp = localStorage.getItem('algebra8_xp'); + if(xp === null){ + const c1 = +(localStorage.getItem('algebra8_ch1_xp') || 0); + const c2 = +(localStorage.getItem('algebra8_ch2_xp') || 0); + xp = c1 + c2; + try { localStorage.setItem('algebra8_xp', String(xp)); } catch(e){} + } + STATE.xp = +xp || 0; STATE.level = calcLevel(STATE.xp); const sk = localStorage.getItem('algebra8_ch1_streak'); if(sk){ const o = JSON.parse(sk); STATE.streak = o.streak||0; STATE.maxStreak = o.max||0; } const dc = localStorage.getItem('algebra8_ch1_daily'); @@ -1046,7 +1056,7 @@ function saveProgress(){ localStorage.setItem('algebra8_ch1_progress', JSON.stringify(STATE.progress)); localStorage.setItem('algebra8_ch1_achievements', JSON.stringify(Object.fromEntries(STATE.achievements))); if(isFinite(STATE.squaresBest)) localStorage.setItem('algebra8_ch1_squaresBest', String(STATE.squaresBest)); - localStorage.setItem('algebra8_ch1_xp', String(STATE.xp)); + localStorage.setItem('algebra8_xp', String(STATE.xp)); localStorage.setItem('algebra8_ch1_streak', JSON.stringify({streak:STATE.streak, max:STATE.maxStreak})); localStorage.setItem('algebra8_ch1_daily', JSON.stringify(STATE.dailyChallenge)); localStorage.setItem('algebra8_ch1_bossResults', JSON.stringify(STATE.bossResults)); @@ -1082,6 +1092,11 @@ function refreshProgressUI(){ }); // check 95% for final chapter modal if(t >= 95) _maybeShowFinalChapter(); + // XP badge in hero — единый стиль с главой 2 + const xpBadge = document.getElementById('hero-xp-badge'); + if(xpBadge){ + xpBadge.innerHTML = ' Ур. ' + STATE.level + ' · ' + (STATE.xp || 0) + ' XP'; + } } let _finalShown = false; function _maybeShowFinalChapter(){ @@ -5911,6 +5926,7 @@ function addXp(amount, source){ STATE.xp += amount; STATE.level = calcLevel(STATE.xp); saveProgress(); + refreshProgressUI(); // обновляет XP-бейдж в hero if(STATE.level > prevLevel){ // Level up! diff --git a/frontend/textbooks/algebra_8_ch2.html b/frontend/textbooks/algebra_8_ch2.html index 0491f89..49c25ec 100644 --- a/frontend/textbooks/algebra_8_ch2.html +++ b/frontend/textbooks/algebra_8_ch2.html @@ -269,7 +269,13 @@ input,select,textarea{font-family:inherit} /* XP badge in hero */ .hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(233,30,99,.22);font-family:'Unbounded',sans-serif} .hero-xp-badge svg{flex-shrink:0} -.xp-card .hp-bar{box-shadow:inset 0 1px 2px rgba(0,0,0,.05)} +/* XP card — единый стиль с главой 1 */ +.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px} +.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between} +.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif} +.xp-bar{height:9px;background:rgba(3,169,244,.15);border-radius:6px;overflow:hidden;margin:7px 0} +.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)} +.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between} /* GLOSSARY tooltip */ .gloss-term{border-bottom:1.5px dotted var(--sec-acc,var(--pri));cursor:help;color:var(--sec-acc-d,var(--pri2));font-weight:600;padding:0 1px} @@ -445,8 +451,24 @@ const STATE = { progress: { p7:0, p8:0, p9:0, p10:0, p11:0, p12:0, final2:0 }, achievements: new Map(), xp: 0, + level: 1, }; +/* Уровни — общая таблица с главой 1 */ +const XP_LEVELS = [0, 50, 120, 220, 350, 520, 740, 1000, 1300, 1700, 2200]; +function calcLevel(xp){ + let lv = 1; + for(let i = 0; i < XP_LEVELS.length; i++){ + if(xp >= XP_LEVELS[i]) lv = i + 1; + else break; + } + return Math.min(lv, XP_LEVELS.length); +} +function _xpForLevel(lv){ + const idx = Math.max(0, Math.min(lv - 1, XP_LEVELS.length - 1)); + return XP_LEVELS[idx]; +} + const ACH_LABELS = { start: 'Начало главы 2!', p7_constr: 'Конструктор уравнений', @@ -504,15 +526,22 @@ function loadProgress(){ for(const [id, t] of Object.entries(p)) STATE.achievements.set(id, (t && t !== id) ? t : (ACH_LABELS[id] || id)); } } - const xp = localStorage.getItem('algebra8_ch2_xp'); - if(xp) STATE.xp = +xp; + // Общий XP для всех глав. Если ещё нет — собираем из старых ch1/ch2 ключей (single-shot миграция). + let xp = localStorage.getItem('algebra8_xp'); + if(xp === null){ + const c1 = +(localStorage.getItem('algebra8_ch1_xp') || 0); + const c2 = +(localStorage.getItem('algebra8_ch2_xp') || 0); + xp = c1 + c2; + try { localStorage.setItem('algebra8_xp', String(xp)); } catch(e){} + } + STATE.xp = +xp || 0; STATE.level = calcLevel(STATE.xp); }catch(e){} } function saveProgress(){ try{ localStorage.setItem('algebra8_ch2_progress', JSON.stringify(STATE.progress)); localStorage.setItem('algebra8_ch2_achievements', JSON.stringify(Object.fromEntries(STATE.achievements))); - localStorage.setItem('algebra8_ch2_xp', String(STATE.xp)); + localStorage.setItem('algebra8_xp', String(STATE.xp)); }catch(e){} } function bumpProgress(key, delta){ @@ -520,7 +549,23 @@ function bumpProgress(key, delta){ saveProgress(); refreshProgressUI(); } -function addXp(n, src){ STATE.xp = (STATE.xp||0) + n; saveProgress(); refreshProgressUI(); } +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(STATE.level > prev){ + const pop = document.getElementById('ach-popup'); + if(pop){ + document.getElementById('ach-text').textContent = 'Уровень ' + STATE.level + '!'; + pop.classList.add('show'); + setTimeout(()=>pop.classList.remove('show'), 2600); + } + if(window.confetti) try { confetti(); } catch(e){} + } +} function refreshProgressUI(){ const total = Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0) / 7); const f = document.getElementById('hero-hp-fill'); @@ -534,8 +579,7 @@ function refreshProgressUI(){ }); const xpBadge = document.getElementById('hero-xp-badge'); if(xpBadge){ - const lv = levelFromXp(STATE.xp || 0); - xpBadge.innerHTML = ' Lv ' + lv + ' · ' + (STATE.xp || 0) + ' XP'; + xpBadge.innerHTML = ' Ур. ' + STATE.level + ' · ' + (STATE.xp || 0) + ' XP'; } // sidebar XP card sync if(STATE.current && document.getElementById('sidebar-content')){ @@ -668,25 +712,24 @@ const TIPS = [ { sec:'p12', html:'После $t = x^2$ всегда проверяй $t \\geq 0$ — отрицательные $t$ дают пустое множество.' }, { sec:'final2', html:'Не бойся пробовать боссов несколько раз: ошибка не «съедает» прогресс.' }, ]; -function levelFromXp(xp){ return Math.floor(Math.sqrt(xp / 50)); } -function xpForLevel(lv){ return 50 * lv * lv; } - function buildSidebar(id){ const box = document.getElementById('sidebar-content'); const sb = SIDEBARS[id] || SIDEBARS.p7; let html = ''; - // XP card - const xp = STATE.xp || 0; - const lv = levelFromXp(xp); - const cur = xpForLevel(lv); - const next = xpForLevel(lv + 1); - const pct = next > cur ? Math.round((xp - cur) / (next - cur) * 100) : 100; - html += `
-

Опыт Lv ${lv}

-
${xp} XP${next} XP
-
-
До Lv ${lv + 1}: ${Math.max(0, next - xp)} XP
+ // XP card — единый стиль с главой 1 + const xpForLv = _xpForLevel(STATE.level); + const xpNext = _xpForLevel(STATE.level + 1); + const xpInLv = STATE.xp - xpForLv; + const xpRange = xpNext - xpForLv; + const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100; + html += `
+
+ XP-прогресс + Ур. ${STATE.level} +
+
+
${STATE.xp} XP${STATE.level < XP_LEVELS.length ? xpNext + ' XP' : 'MAX'}
`; // Шпаргалка