033c941b02
- js/textbook-xp-widget.js: shared модуль (monkey-patch addXp + para-pill auto-award для учебников без addXp) - physics8_thermal/electro/optics: добавлены теги /js/xp.js и /js/textbook-xp-widget.js — теперь все 74 addXp-хука пробрасываются в глобальный gamification (через self-award endpoint с дебаунсом) - chemistry_9 + physics_9: те же теги. Каждый первый клик по .para-pill даёт +5 XP в систему (без правок 23000 LOC) - Изначальный XP в учебниках не теряется — localStorage остаётся кешем, сервер — источник правды
109 lines
3.9 KiB
JavaScript
109 lines
3.9 KiB
JavaScript
'use strict';
|
|
/**
|
|
* textbook-xp-widget.js — синхронизация XP учебников с системной геймификацией.
|
|
*
|
|
* Для учебников с локальным window.addXp:
|
|
* monkey-patch addXp, чтобы каждый вызов также дёргал window.LS.xp.add.
|
|
*
|
|
* Для всех учебников (в т.ч. chemistry_9, physics_9 без addXp):
|
|
* первый клик по .para-pill[data-para] за сессию/навсегда даёт +5 XP.
|
|
*
|
|
* Зависимость: /js/xp.js должен загрузиться раньше (defer, порядок тегов).
|
|
*/
|
|
(function () {
|
|
/* ── Получить slug текущей страницы ── */
|
|
function _slug() {
|
|
var m = location.pathname.match(/\/textbook\/([^/?#]+)/);
|
|
if (m) return m[1];
|
|
return location.pathname.split('/').pop()
|
|
.replace(/\.html$/i, '')
|
|
.replace(/_/g, '-');
|
|
}
|
|
|
|
/* ── Monkey-patch window.addXp (для учебников, где он есть) ── */
|
|
function _patchAddXp(slug) {
|
|
var orig = window.addXp;
|
|
if (typeof orig !== 'function') return;
|
|
window.addXp = function patchedAddXp(amount, source) {
|
|
var ret = orig.apply(this, arguments);
|
|
// пробрасываем в глобальный XP только при успехе оригинала
|
|
if (window.LS && window.LS.xp && amount > 0) {
|
|
var src = slug + '-' + (source || 'misc');
|
|
window.LS.xp.add(amount, src);
|
|
}
|
|
return ret;
|
|
};
|
|
}
|
|
|
|
/* ── Para-pill auto-award ── */
|
|
function _initParaAward(slug) {
|
|
var STORAGE_KEY = slug + '_xp_paras';
|
|
|
|
function _loadSeen() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function _saveSeen(arr) {
|
|
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(arr)); } catch (e) {}
|
|
}
|
|
|
|
document.body.addEventListener('click', function (e) {
|
|
var pill = e.target.closest('.para-pill[data-para]');
|
|
if (!pill) return;
|
|
var paraId = pill.getAttribute('data-para');
|
|
if (!paraId) return;
|
|
|
|
// динамически сгенерированные пилюли могут содержать шаблонные значения
|
|
if (paraId.indexOf("'") !== -1 || paraId.indexOf('+') !== -1) return;
|
|
|
|
// пропустить, если LS.xp недоступен (не авторизован)
|
|
if (!window.LS || !window.LS.xp) return;
|
|
|
|
var seen = _loadSeen();
|
|
if (seen.indexOf(paraId) !== -1) return; // уже начислено
|
|
|
|
seen.push(paraId);
|
|
_saveSeen(seen);
|
|
window.LS.xp.add(5, slug + '-para-' + paraId);
|
|
}, /* capture = */ false);
|
|
}
|
|
|
|
/* ── Обновление #hero-xp-badge при изменении XP (обратная совместимость) ── */
|
|
function _bindBadge() {
|
|
var badge = document.getElementById('hero-xp-badge');
|
|
if (!badge) return;
|
|
if (!window.LS || !window.LS.xp) return;
|
|
window.LS.xp.on('change', function (state) {
|
|
if (!state) return;
|
|
badge.textContent = state.xp + ' XP · Ур. ' + state.level;
|
|
});
|
|
}
|
|
|
|
/* ── Инициализация (ждём DOM + xp.js) ── */
|
|
function _init() {
|
|
var slug = _slug();
|
|
|
|
// Загрузить XP с сервера (merge локального + серверного)
|
|
if (window.LS && window.LS.xp) {
|
|
window.LS.xp.load();
|
|
}
|
|
|
|
_patchAddXp(slug);
|
|
_initParaAward(slug);
|
|
_bindBadge();
|
|
}
|
|
|
|
// defer гарантирует DOM готов к моменту выполнения,
|
|
// но xp.js тоже defer — порядок тегов определяет порядок выполнения.
|
|
// На случай гонки: если xp.js загружен первым — LS.xp уже есть.
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', _init);
|
|
} else {
|
|
_init();
|
|
}
|
|
})();
|