feat(algebra-8 ch2): XP-карта, бейдж в hero, совет дня, фикс sidebar 'Финал'
- SIDEBARS.final2: убрал stub 'будет в Wave 4', добавил 5 строк по финалу (7 боссов, типы заданий, награда, практика, серия). - XP card в сайдбаре: уровень (Lv N), текущий XP, прогресс-бар до следующего уровня, остаток XP. Формула: Lv = floor(sqrt(xp/50)). - XP badge в hero (рядом с прогрессом): жёлто-розовая пилюля «★ Lv N · NN XP», обновляется при каждом addXp. - TIPS: 7 советов (по одному на каждый §+финал). В сайдбаре отдельная карточка «Подсказка» с жёлтым градиентом — контекстная под текущий параграф. - refreshProgressUI: после изменения XP пересобирает сайдбар, чтобы карточки опыта/совета оставались актуальными.
This commit is contained in:
@@ -266,6 +266,11 @@ input,select,textarea{font-family:inherit}
|
||||
.eq-show{font-family:'JetBrains Mono',monospace}
|
||||
.pipe-tabs .btn.active{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
|
||||
|
||||
/* XP badge in hero */
|
||||
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(233,30,99,.22);font-family:'Unbounded',sans-serif}
|
||||
.hero-xp-badge svg{flex-shrink:0}
|
||||
.xp-card .hp-bar{box-shadow:inset 0 1px 2px rgba(0,0,0,.05)}
|
||||
|
||||
/* GLOSSARY tooltip */
|
||||
.gloss-term{border-bottom:1.5px dotted var(--sec-acc,var(--pri));cursor:help;color:var(--sec-acc-d,var(--pri2));font-weight:600;padding:0 1px}
|
||||
.gloss-term:hover{background:var(--sec-acc-soft,var(--pri-soft));border-radius:3px}
|
||||
@@ -360,6 +365,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>
|
||||
|
||||
@@ -526,6 +532,15 @@ function refreshProgressUI(){
|
||||
const fl = el.querySelector('.psel-prog-fill');
|
||||
if(fl) fl.style.width = (STATE.progress[k]||0) + '%';
|
||||
});
|
||||
const xpBadge = document.getElementById('hero-xp-badge');
|
||||
if(xpBadge){
|
||||
const lv = levelFromXp(STATE.xp || 0);
|
||||
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> Lv ' + lv + ' · ' + (STATE.xp || 0) + ' XP';
|
||||
}
|
||||
// sidebar XP card sync
|
||||
if(STATE.current && document.getElementById('sidebar-content')){
|
||||
try { buildSidebar(STATE.current); } catch(e){}
|
||||
}
|
||||
}
|
||||
function achievement(id, text){
|
||||
if(STATE.achievements.has(id)) return;
|
||||
@@ -636,16 +651,62 @@ const SIDEBARS = {
|
||||
['Дробное','умножить на ОЗ, проверить ОДЗ'],
|
||||
['ОДЗ','знаменатель $\\neq 0$'],
|
||||
]},
|
||||
final2:{ title:'Финал', rows:[['Итоги главы','будет в Wave 4']]},
|
||||
final2:{ title:'Финал главы', rows:[
|
||||
['7 боссов','один на каждый параграф + общий'],
|
||||
['Тип задач','select / yes-no / input'],
|
||||
['Награда','«Магистр квадратных уравнений»'],
|
||||
['Практика','случайные задачи всей главы'],
|
||||
['Серия 5×','+ достижение «Серия из 5 верных»'],
|
||||
]},
|
||||
};
|
||||
const TIPS = [
|
||||
{ sec:'p7', html:'Если в неполном <b>ax² + c = 0</b> знаки <i>a</i> и <i>c</i> одинаковые — корней нет. Проверяй знак $-c/a$.' },
|
||||
{ sec:'p8', html:'$D = b^2 - 4ac$ — даже не считай корни, если $D < 0$.' },
|
||||
{ sec:'p9', html:'Перед использованием Виета убедись, что <b>a = 1</b>. Иначе формулы дают $-b/a$ и $c/a$.' },
|
||||
{ sec:'p10', html:'Если $D < 0$, на множители первой степени <b>не</b> раскладывается. Не пытайся!' },
|
||||
{ sec:'p11', html:'В задачах на движение часто помогает «общее время»: $\\dfrac{s_1}{v_1} + \\dfrac{s_2}{v_2}$.' },
|
||||
{ sec:'p12', html:'После $t = x^2$ всегда проверяй $t \\geq 0$ — отрицательные $t$ дают пустое множество.' },
|
||||
{ sec:'final2', html:'Не бойся пробовать боссов несколько раз: ошибка не «съедает» прогресс.' },
|
||||
];
|
||||
function 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 = `<div class="sidecard"><h4>${sb.title}</h4>`;
|
||||
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 += `<div class="sidecard xp-card">
|
||||
<h4>Опыт <span style="float:right;color:var(--pri)">Lv ${lv}</span></h4>
|
||||
<div class="xp-nums" style="display:flex;justify-content:space-between;font-size:.84rem;font-weight:700;color:var(--pri2);margin-bottom:5px"><span>${xp} XP</span><span style="color:var(--muted);font-weight:600">${next} XP</span></div>
|
||||
<div class="hp-bar" style="height:6px;background:var(--pri-soft);border-radius:3px;overflow:hidden"><div style="height:100%;width:${pct}%;background:linear-gradient(90deg,var(--pri),var(--acc));transition:width .4s"></div></div>
|
||||
<div style="margin-top:6px;font-size:.74rem;color:var(--muted);text-align:center">До Lv ${lv + 1}: ${Math.max(0, next - xp)} XP</div>
|
||||
</div>`;
|
||||
|
||||
// Шпаргалка
|
||||
html += `<div class="sidecard"><h4>${sb.title}</h4>`;
|
||||
sb.rows.forEach(([k,v])=>{
|
||||
html += `<div class="sidecard-row"><b>${k}</b> ${v ? '— ' + v : ''}</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
// Совет дня
|
||||
const tip = TIPS.find(t => t.sec === id) || TIPS[0];
|
||||
html += `<div class="sidecard tip-card" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)">
|
||||
<h4 style="color:#92400e;display:flex;align-items:center;gap:6px">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/><circle cx="12" cy="12" r="4"/></svg>
|
||||
Подсказка
|
||||
</h4>
|
||||
<div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">${tip.html}</div>
|
||||
</div>`;
|
||||
|
||||
// Достижения
|
||||
if(STATE.achievements.size > 0){
|
||||
html += `<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">${STATE.achievements.size}</span></h4>`;
|
||||
[...STATE.achievements.values()].slice(-4).forEach(text=>{
|
||||
@@ -653,6 +714,7 @@ function buildSidebar(id){
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
box.innerHTML = html;
|
||||
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user