Files
Learn_System/js/textbook-xp-widget.js
T
Maxim Dolgolyov 033c941b02 feat(xp): physics8 + chem9 + phys9 синхронизируют XP с системной геймификацией
- 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 остаётся
  кешем, сервер — источник правды
2026-05-27 16:36:43 +03:00

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();
}
})();