feat(algebra-8): общая система опыта для главы 1 и главы 2

Раньше: каждая глава хранила XP отдельно (algebra8_ch1_xp +
algebra8_ch2_xp), формулы уровня были разные (дискретная таблица в
ch1, формула sqrt в ch2), визуально XP-карты различались.

Теперь:
- Один ключ localStorage: 'algebra8_xp' для обеих глав.
- При первой загрузке (в любой главе) — single-shot миграция:
  если новый ключ отсутствует, суммирует старые ch1 + ch2 и
  сохраняет под единый ключ. Старые ключи не удаляются (на всякий).
- Единая таблица уровней XP_LEVELS = [0, 50, 120, 220, 350, 520,
  740, 1000, 1300, 1700, 2200] (11 уровней, MAX = Ур. 11).
- Единые функции calcLevel(xp) и _xpForLevel(lv).
- XP-карта в сайдбаре главы 2 теперь идентична главе 1:
  градиент acc→pri-soft, .xp-card-title, .xp-bar, .xp-fill, .xp-nums.
- Hero badge «★ Ур. N · NN XP» добавлен в hero обоих глав.
- addXp в ch2: при повышении уровня — popup с номером уровня + confetti.
- addXp в ch1: refreshProgressUI вызывается, чтобы обновлять hero
  badge сразу после начисления.
This commit is contained in:
Maxim Dolgolyov
2026-05-27 15:41:54 +03:00
parent 58998a59c0
commit 9199427dfd
2 changed files with 83 additions and 24 deletions
+19 -3
View File
@@ -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}
<div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div>
<span id="hero-hp-text" class="hp-text">0%</span>
</div>
<div id="hero-xp-badge" class="hero-xp-badge" title="Опыт"></div>
</div>
</section>
@@ -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 = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg> Ур. ' + STATE.level + ' · ' + (STATE.xp || 0) + ' XP';
}
}
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!