feat(xp): textbook XP синхронизируется с системной геймификацией
- backend: POST /api/gamification/self-award (rate-limited, validated) - frontend/js/xp.js: load/add/flush/on клиент, ~150 LOC, дебаунс 300мс, keepalive fetch на unload/visibilitychange hidden - algebra_8.html и algebra_8_ch2.html: XP_LEVELS заменён на единую формулу с сервером; addXp/loadProgress подключены к window.LS.xp - При первой загрузке: merge max(local, server); далее сервер — источник правды
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
<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>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Manrope:wght@400;500;600;700;800&family=Unbounded:wght@400;700;800;900&display=swap" rel="stylesheet">
|
||||
<script src="/js/api.js" defer></script>
|
||||
<script src="/js/xp.js" defer></script>
|
||||
<style>
|
||||
:root{
|
||||
--pri:#e91e63; --pri2:#c2185b; --pri-soft:#fce7f3;
|
||||
@@ -1460,6 +1462,18 @@ function init(){
|
||||
initMobileSidebar();
|
||||
goTo('p1'); // строит только §1, остальные — лениво при переходе
|
||||
setTimeout(()=>achievement('start','Начало пути по корням!'), 800);
|
||||
// Sync XP с сервером: если серверный XP выше — обновляем локальный прогресс
|
||||
if(window.LS && window.LS.xp){
|
||||
window.LS.xp.load().then(function(s){
|
||||
if(s && s.xp > STATE.xp){
|
||||
STATE.xp = s.xp;
|
||||
STATE.level = calcLevel(STATE.xp);
|
||||
saveProgress();
|
||||
refreshProgressUI();
|
||||
if(STATE.current) buildSidebar(STATE.current);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
@@ -5902,22 +5916,11 @@ document.addEventListener('DOMContentLoaded', ()=>setTimeout(initWave3, 100));
|
||||
WAVE 4 — GAMIFICATION
|
||||
════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── XP levels ── */
|
||||
const XP_LEVELS = [0, 50, 120, 220, 350, 520, 740, 1000, 1300, 1700, 2200];
|
||||
/* ── XP levels — единая формула с сервером ── */
|
||||
const XP_LEVELS = null; // legacy — теперь уровень считается формулой
|
||||
|
||||
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];
|
||||
}
|
||||
function calcLevel(xp){ return Math.floor(Math.sqrt((xp || 0) / 100)) + 1; }
|
||||
function _xpForLevel(lv){ return (lv - 1) * (lv - 1) * 100; }
|
||||
|
||||
function addXp(amount, source){
|
||||
if(!amount || amount <= 0) return;
|
||||
@@ -5926,6 +5929,7 @@ function addXp(amount, source){
|
||||
STATE.xp += amount;
|
||||
STATE.level = calcLevel(STATE.xp);
|
||||
saveProgress();
|
||||
if(window.LS && window.LS.xp) window.LS.xp.add(amount, 'algebra8-ch1-' + (source || 'misc'));
|
||||
refreshProgressUI(); // обновляет XP-бейдж в hero
|
||||
|
||||
if(STATE.level > prevLevel){
|
||||
@@ -5951,7 +5955,7 @@ function addXp(amount, source){
|
||||
if(fill) fill.style.width = xpPct + '%';
|
||||
const xpNums = box.querySelectorAll('.xp-nums span');
|
||||
if(xpNums[0]) xpNums[0].textContent = STATE.xp + ' XP';
|
||||
if(xpNums[1]) xpNums[1].textContent = STATE.level < 10 ? xpNext + ' XP' : 'MAX';
|
||||
if(xpNums[1]) xpNums[1].textContent = STATE.level < 30 ? xpNext + ' XP' : 'MAX';
|
||||
const lvEl = box.querySelector('.xp-level');
|
||||
if(lvEl) lvEl.textContent = 'Ур. ' + STATE.level;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
<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>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Manrope:wght@400;500;600;700;800&family=Unbounded:wght@400;700;800;900&display=swap" rel="stylesheet">
|
||||
<script src="/js/api.js" defer></script>
|
||||
<script src="/js/xp.js" defer></script>
|
||||
<style>
|
||||
:root{
|
||||
--pri:#e91e63; --pri2:#c2185b; --pri-soft:#fce7f3;
|
||||
@@ -454,20 +456,11 @@ const STATE = {
|
||||
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];
|
||||
}
|
||||
/* Уровни — единая формула с сервером (xpToLevel из _shared.js) */
|
||||
const XP_LEVELS = null; // legacy — теперь уровень считается формулой
|
||||
|
||||
function calcLevel(xp){ return Math.floor(Math.sqrt((xp || 0) / 100)) + 1; }
|
||||
function _xpForLevel(lv){ return (lv - 1) * (lv - 1) * 100; }
|
||||
|
||||
const ACH_LABELS = {
|
||||
start: 'Начало главы 2!',
|
||||
@@ -555,6 +548,7 @@ function addXp(n, src){
|
||||
STATE.xp = Math.max(0, (STATE.xp || 0) + n);
|
||||
STATE.level = calcLevel(STATE.xp);
|
||||
saveProgress();
|
||||
if(window.LS && window.LS.xp) window.LS.xp.add(n, 'algebra8-ch2-' + (src || 'misc'));
|
||||
refreshProgressUI();
|
||||
if(STATE.level > prev){
|
||||
const pop = document.getElementById('ach-popup');
|
||||
@@ -729,7 +723,7 @@ function buildSidebar(id){
|
||||
<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>${STATE.level < XP_LEVELS.length ? xpNext + ' XP' : 'MAX'}</span></div>
|
||||
<div class="xp-nums"><span>${STATE.xp} XP</span><span>${STATE.level < 30 ? xpNext + ' XP' : 'MAX'}</span></div>
|
||||
</div>`;
|
||||
|
||||
// Шпаргалка
|
||||
@@ -1214,6 +1208,18 @@ function init(){
|
||||
refreshProgressUI();
|
||||
goTo('p7');
|
||||
setTimeout(()=>achievement('start','Начало главы 2!'), 600);
|
||||
// Sync XP с сервером: если серверный XP выше — обновляем локальный прогресс
|
||||
if(window.LS && window.LS.xp){
|
||||
window.LS.xp.load().then(function(s){
|
||||
if(s && s.xp > STATE.xp){
|
||||
STATE.xp = s.xp;
|
||||
STATE.level = calcLevel(STATE.xp);
|
||||
saveProgress();
|
||||
refreshProgressUI();
|
||||
if(STATE.current) buildSidebar(STATE.current);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user