feat(gamification): Phase 1 — full kill-switch + textbook XP wrapping
Until now the 'gamification' feature flag did nothing: it had no row in
app_settings, the admin couldn't toggle it, awardXP/awardCoins ignored
it, and the CSS only hid three dashboard widgets — XP bars in textbooks
stayed visible regardless.
Phase 1 closes every hole.
Backend (source of truth):
• migration 029 seeds feature_gamification_enabled=1
• new isGamificationEnabled() helper in gamification/_shared.js with a
30s cache + invalidateGamificationCache() for instant admin toggles
• awardXP / awardCoins / updateStreak / unlockAchievement /
checkAchievements all bail out when the flag is off
• /api/gamification/* and /api/shop/* (user routes) return 404 when
disabled; admin routes remain open so the switch itself is reachable
• adminController.updateFeatures gains 'gamification' in the allow-list
and invalidates the cache on flip
Frontend:
• LS.isGamificationEnabled() (synchronous, populated by loadFeatures)
so xp.js + applyCosmetics can bail without a round-trip
• xp.js load/add/flush become no-ops when the flag is off
• applyCosmetics skips the round-trip when off
• CSS .no-gamification rule expanded to cover .hero-xp-badge, .po-xp,
.xp-card, .xp-bar, #frames-section, and a universal [data-gamified]
hook for future blocks
Textbooks (Variant 2 of the plan):
• backend/scripts/wrap_textbook_xp.py — idempotent script that adds
data-gamified to 167 XP tags across 63 textbook files (chapters +
hubs, all subjects/grades). Single CSS rule now hides everything.
Verified end-to-end: with the flag off, awardXP/awardCoins write nothing;
flipping back restores normal behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
wrap_textbook_xp.py — Add `data-gamified` attribute to XP UI in textbooks.
|
||||
|
||||
Part of Phase 1 of the gamification kill-switch (Variant 2 of plan §E.3).
|
||||
The CSS rule `body.no-gamification [data-gamified] { display:none }`
|
||||
catches every wrapped block in one selector, so future authors don't have
|
||||
to invent new class names just to be hidden by the master switch.
|
||||
|
||||
What we wrap:
|
||||
• <div id="hero-xp-badge" ...> — chapter & hub hero badges
|
||||
• <div class="po-xp" ...> — hub overall-progress XP pill
|
||||
• <div class="xp-card" ...> — chapter inline progress card
|
||||
• inline JS that generates `<div class="xp-card">` HTML strings
|
||||
• `.xp-bar` containers when they're standalone
|
||||
|
||||
Idempotent: if `data-gamified` is already present on an opening tag the
|
||||
script skips it. Reports each file touched.
|
||||
|
||||
Run from repo root:
|
||||
python backend/scripts/wrap_textbook_xp.py
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2] / "frontend" / "textbooks"
|
||||
if not ROOT.is_dir():
|
||||
print(f"[wrap-xp] textbook dir not found: {ROOT}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Selectors that mark an opening tag as an XP block. The first group
|
||||
# captures `<tag` + attributes UP TO (but not including) the existing
|
||||
# `class="..."` / `id="..."` we matched, so we can splice in
|
||||
# `data-gamified` before the closing `>`.
|
||||
PATTERNS = [
|
||||
# <... id="hero-xp-badge" ...>
|
||||
re.compile(r'(<[a-zA-Z][^>]*?\bid="hero-xp-badge"[^>]*?)(>)'),
|
||||
# <... class="po-xp" ...> or class="...po-xp..."
|
||||
re.compile(r'(<[a-zA-Z][^>]*?\bclass="[^"]*\bpo-xp\b[^"]*"[^>]*?)(>)'),
|
||||
# <... class="...xp-card..."> — covers HTML and inline JS `'<div class="xp-card">'`
|
||||
re.compile(r'(<[a-zA-Z][^>]*?\bclass="[^"]*\bxp-card\b[^"]*"[^>]*?)(>)'),
|
||||
# Standalone class="hero-xp-badge"
|
||||
re.compile(r'(<[a-zA-Z][^>]*?\bclass="[^"]*\bhero-xp-badge\b[^"]*"[^>]*?)(>)'),
|
||||
]
|
||||
|
||||
# Already-wrapped check
|
||||
RX_HAS_ATTR = re.compile(r'\bdata-gamified\b')
|
||||
|
||||
|
||||
def patch_file(path: Path) -> int:
|
||||
"""Return number of opening tags newly given data-gamified."""
|
||||
text = path.read_text(encoding="utf-8")
|
||||
new = text
|
||||
changes = 0
|
||||
for rx in PATTERNS:
|
||||
def sub(m, _rx=rx):
|
||||
nonlocal changes
|
||||
head, close = m.group(1), m.group(2)
|
||||
if RX_HAS_ATTR.search(head):
|
||||
return m.group(0)
|
||||
changes += 1
|
||||
sep = "" if head.endswith(" ") else " "
|
||||
return f'{head}{sep}data-gamified{close}'
|
||||
new = rx.sub(sub, new)
|
||||
if new != text:
|
||||
path.write_text(new, encoding="utf-8")
|
||||
return changes
|
||||
|
||||
|
||||
def main():
|
||||
files = sorted(ROOT.glob("*.html"))
|
||||
total_files = 0
|
||||
total_tags = 0
|
||||
for f in files:
|
||||
try:
|
||||
n = patch_file(f)
|
||||
except Exception as e:
|
||||
print(f"[wrap-xp] {f.name}: ERROR {e}", file=sys.stderr)
|
||||
continue
|
||||
if n:
|
||||
print(f"[wrap-xp] {f.name}: +{n} tag(s)")
|
||||
total_files += 1
|
||||
total_tags += n
|
||||
print(f"[wrap-xp] done — patched {total_tags} tags across {total_files} files "
|
||||
f"(scanned {len(files)})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -509,10 +509,12 @@ function getFeatures(_req, res) {
|
||||
/* ── PATCH /api/admin/features ──────────────────────────────────────── */
|
||||
function updateFeatures(req, res) {
|
||||
const allowed = ['crossword', 'hangman', 'pet', 'red_book', 'collection',
|
||||
'flashcards', 'knowledge_map', 'board', 'biochem', 'live_quiz', 'classroom'];
|
||||
'flashcards', 'knowledge_map', 'board', 'biochem', 'live_quiz', 'classroom',
|
||||
'gamification'];
|
||||
const updates = req.body;
|
||||
const stmt = db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)");
|
||||
const getOld = db.prepare("SELECT value FROM app_settings WHERE key = ?");
|
||||
let touchedGamification = false;
|
||||
for (const [name, enabled] of Object.entries(updates)) {
|
||||
if (!allowed.includes(name)) continue;
|
||||
const settingKey = `feature_${name}_enabled`;
|
||||
@@ -521,6 +523,15 @@ function updateFeatures(req, res) {
|
||||
const newVal = enabled ? '1' : '0';
|
||||
stmt.run(settingKey, newVal);
|
||||
audit(req, 'feature.update', `feature:${name}`, `${oldVal} -> ${newVal}`);
|
||||
if (name === 'gamification') touchedGamification = true;
|
||||
}
|
||||
// Invalidate the gamification cache so the kill-switch takes effect
|
||||
// immediately (otherwise awardXP keeps running for up to 30s).
|
||||
if (touchedGamification) {
|
||||
try {
|
||||
const { invalidateGamificationCache } = require('./gamification/_shared');
|
||||
invalidateGamificationCache();
|
||||
} catch { /* defensive — shouldn't fail */ }
|
||||
}
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
@@ -187,8 +187,42 @@ const stmts = {
|
||||
ORDER BY up.purchased_at DESC LIMIT 20`),
|
||||
};
|
||||
|
||||
/* ── Kill-switch ─────────────────────────────────────────────────
|
||||
The 'gamification' feature flag is the master switch. When admin
|
||||
turns it off, ALL XP/coin awards become no-ops, /api/gamification
|
||||
routes return 404, and the front-end hides every XP/coin/streak
|
||||
element via body.no-gamification.
|
||||
|
||||
The flag is read often (every awardXP call) so we cache the value
|
||||
and only re-read after a short TTL. Toggling the flag also calls
|
||||
invalidateGamificationCache() from adminController so the change
|
||||
takes effect immediately without waiting for the TTL. */
|
||||
const _gamCache = { value: null, expiresAt: 0 };
|
||||
const _GAM_TTL_MS = 30_000;
|
||||
const _stmtFeatureGam = db.prepare("SELECT value FROM app_settings WHERE key = 'feature_gamification_enabled'");
|
||||
|
||||
function isGamificationEnabled() {
|
||||
const now = Date.now();
|
||||
if (_gamCache.value !== null && now < _gamCache.expiresAt) return _gamCache.value;
|
||||
let on = true;
|
||||
try {
|
||||
const row = _stmtFeatureGam.get();
|
||||
// Default ON if row missing (migration 029 seeds '1', but be defensive).
|
||||
on = !row || row.value === '1';
|
||||
} catch { on = true; }
|
||||
_gamCache.value = on;
|
||||
_gamCache.expiresAt = now + _GAM_TTL_MS;
|
||||
return on;
|
||||
}
|
||||
|
||||
function invalidateGamificationCache() {
|
||||
_gamCache.value = null;
|
||||
_gamCache.expiresAt = 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
xpToLevel, levelMinXp, levelMaxXp, rankName, RANKS,
|
||||
GOAL_TIERS, ACHIEVEMENT_DEFS, AVATAR_FRAMES,
|
||||
stmts,
|
||||
isGamificationEnabled, invalidateGamificationCache,
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ const sse = require('../../sse');
|
||||
const { pushParentNotif } = require('../../utils/notifications');
|
||||
const {
|
||||
stmts, xpToLevel, levelMinXp, levelMaxXp, rankName,
|
||||
GOAL_TIERS, ACHIEVEMENT_DEFS,
|
||||
GOAL_TIERS, ACHIEVEMENT_DEFS, isGamificationEnabled,
|
||||
} = require('./_shared');
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
@@ -16,12 +16,14 @@ const {
|
||||
/* ── Coins ─────────────────────────────────────────────────────────── */
|
||||
function awardCoins(userId, amount /*, reason */) {
|
||||
if (!amount || amount <= 0) return;
|
||||
if (!isGamificationEnabled()) return; // master kill-switch
|
||||
stmts.incrCoins.run(amount, userId);
|
||||
}
|
||||
|
||||
/* ── XP ────────────────────────────────────────────────────────────── */
|
||||
function awardXP(userId, amount, reason) {
|
||||
if (!amount || amount <= 0) return;
|
||||
if (!isGamificationEnabled()) return; // master kill-switch
|
||||
stmts.insertXpLog.run(userId, amount, reason);
|
||||
stmts.incrXP.run(amount, userId);
|
||||
const user = stmts.getXP.get(userId);
|
||||
@@ -52,6 +54,7 @@ function getXPInfo(userId) {
|
||||
|
||||
/* ── Streak (called by onTestFinished) ─────────────────────────────── */
|
||||
function updateStreak(userId) {
|
||||
if (!isGamificationEnabled()) return;
|
||||
const user = stmts.getStreak.get(userId);
|
||||
if (!user) return;
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
@@ -103,6 +106,7 @@ function pushAchievementNotif(userId, ach) {
|
||||
}
|
||||
|
||||
function unlockAchievement(userId, slug) {
|
||||
if (!isGamificationEnabled()) return false;
|
||||
const ach = stmts.getAchBySlug.get(slug);
|
||||
if (!ach) return false;
|
||||
const exists = stmts.hasUserAch.get(userId, ach.id);
|
||||
@@ -114,6 +118,7 @@ function unlockAchievement(userId, slug) {
|
||||
}
|
||||
|
||||
function checkAchievements(userId) {
|
||||
if (!isGamificationEnabled()) return;
|
||||
const row = stmts.getUserForAch.get(userId);
|
||||
if (!row) return;
|
||||
const { test_count: testCount, perfect_count: perfectCount, class_count: classCount } = row;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- 029: Seed the gamification feature flag
|
||||
--
|
||||
-- Until now there was no row for feature_gamification_enabled in
|
||||
-- app_settings — meaning /api/features returned `gamification: undefined`
|
||||
-- and the front-end kill-switch (`body.no-gamification`) never engaged,
|
||||
-- regardless of admin intent.
|
||||
--
|
||||
-- Default = ON. Admins can disable via PATCH /api/admin/features
|
||||
-- once 'gamification' is added to the allowed list (handled in
|
||||
-- adminController.js, not this migration).
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
INSERT OR IGNORE INTO app_settings (key, value)
|
||||
VALUES ('feature_gamification_enabled', '1');
|
||||
@@ -8,6 +8,18 @@ const {
|
||||
onLabExperiment, selfAward,
|
||||
adminAward, adminReset, adminGamStats, adminGetUser
|
||||
} = require('../controllers/gamificationController');
|
||||
const { isGamificationEnabled } = require('../controllers/gamification/_shared');
|
||||
|
||||
/* When gamification is globally disabled, user-facing routes return 404.
|
||||
Admin routes (under /admin/*) stay accessible so the kill-switch itself
|
||||
can still be flipped from the panel. */
|
||||
function gamGate(req, res, next) {
|
||||
if (req.path.startsWith('/admin/')) return next();
|
||||
if (!isGamificationEnabled()) {
|
||||
return res.status(404).json({ error: 'Gamification disabled' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
const labLimiter = rateLimit({ windowMs: 60_000, max: 30, message: 'Слишком частые запросы лаборатории' });
|
||||
const labSchema = { body: { reactionsDiscovered: { type: 'number', min: 0, max: 100, integer: true } } };
|
||||
@@ -22,6 +34,7 @@ const selfAwardSchema = {
|
||||
};
|
||||
|
||||
router.use(authMiddleware);
|
||||
router.use(gamGate);
|
||||
|
||||
router.get('/me', getMe);
|
||||
router.get('/achievements', getAchievements);
|
||||
|
||||
@@ -6,6 +6,16 @@ const {
|
||||
getItems, purchaseItem, getPurchases, getCoins, getMyActive, activateItem,
|
||||
adminGetItems, adminCreateItem, adminUpdateItem, adminDeleteItem, adminAwardCoins, adminShopStats
|
||||
} = require('../controllers/shopController');
|
||||
const { isGamificationEnabled } = require('../controllers/gamification/_shared');
|
||||
|
||||
/* Same kill-switch as gamification routes — shop is part of the gam loop. */
|
||||
function shopGate(req, res, next) {
|
||||
if (req.path.startsWith('/admin/')) return next();
|
||||
if (!isGamificationEnabled()) {
|
||||
return res.status(404).json({ error: 'Gamification disabled' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
const purchaseLimiter = rateLimit({ windowMs: 60_000, max: 10, message: 'Слишком много покупок, подождите минуту' });
|
||||
const activateSchema = { body: { type: { type: 'string', oneOf: ['frame', 'title', 'effect'] } } };
|
||||
@@ -20,6 +30,7 @@ const awardCoinsSchema = { body: {
|
||||
}};
|
||||
|
||||
router.use(authMiddleware);
|
||||
router.use(shopGate);
|
||||
|
||||
router.get('/items', getItems);
|
||||
router.post('/items/:id/purchase', requirePermission('shop.purchase'), purchaseLimiter, purchaseItem);
|
||||
|
||||
+21
-1
@@ -1017,13 +1017,33 @@ body {
|
||||
/* Student without a class: hide leaderboard */
|
||||
body.no-class #lb-section { display: none !important; }
|
||||
|
||||
/* Gamification kill-switch.
|
||||
When admin turns off the feature, body.no-gamification is set by
|
||||
api.js/hideDisabledFeatures and EVERY XP / coin / streak / shop /
|
||||
achievement / frame element must vanish — across the whole app,
|
||||
not just the dashboard. The rules below cover:
|
||||
• dashboard widgets (.gam-bar, .lb-widget)
|
||||
• profile tabs (achievements, shop, frames)
|
||||
• textbook XP badges and progress cards (.hero-xp-badge, .po-xp,
|
||||
.xp-bar, .xp-card)
|
||||
• a catch-all [data-gamified] hook that wraps any future block —
|
||||
authors of new pages should wrap XP UI in a <div data-gamified>
|
||||
instead of inventing new classes. */
|
||||
body.no-gamification .gam-bar,
|
||||
body.no-gamification .lb-widget,
|
||||
body.no-gamification .achievements-section,
|
||||
body.no-gamification #tab-btn-achievements,
|
||||
body.no-gamification #tab-btn-shop,
|
||||
body.no-gamification #tab-achievements,
|
||||
body.no-gamification #tab-shop { display: none !important; }
|
||||
body.no-gamification #tab-shop,
|
||||
body.no-gamification #frames-section,
|
||||
body.no-gamification .hero-xp-badge,
|
||||
body.no-gamification .po-xp,
|
||||
body.no-gamification .xp-card,
|
||||
body.no-gamification .xp-bar,
|
||||
body.no-gamification .xp-pill,
|
||||
body.no-gamification .xp-badge,
|
||||
body.no-gamification [data-gamified] { display: none !important; }
|
||||
|
||||
/* ══════════════════════════════════════════
|
||||
RESPONSIVE — SMALL PHONES (≤ 480px)
|
||||
|
||||
@@ -233,7 +233,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -565,7 +565,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -204,7 +204,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -423,7 +423,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -208,7 +208,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -429,7 +429,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -137,7 +137,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
|
||||
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
|
||||
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
|
||||
</section>
|
||||
|
||||
<div class="ch-grid">
|
||||
|
||||
@@ -253,7 +253,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -426,7 +426,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -233,7 +233,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -406,7 +406,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -254,7 +254,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -432,7 +432,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -228,7 +228,7 @@ html.dark .final-cta-sub{color:#fcd34d}
|
||||
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
|
||||
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
|
||||
</section>
|
||||
|
||||
<div class="ch-grid">
|
||||
|
||||
@@ -286,7 +286,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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 id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -541,7 +541,7 @@ function buildSidebar(id){
|
||||
const xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level + 1);
|
||||
const xpInLv = STATE.xp - xpForLv, xpRange = xpNext - xpForLv;
|
||||
const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
|
||||
html += '<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>' + xpNext + ' XP</span></div></div>';
|
||||
html += '<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>' + xpNext + ' XP</span></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>';
|
||||
|
||||
@@ -253,7 +253,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -528,7 +528,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -246,7 +246,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -484,7 +484,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -241,7 +241,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -462,7 +462,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -142,7 +142,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
|
||||
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
|
||||
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
|
||||
</section>
|
||||
|
||||
<div class="ch-grid">
|
||||
|
||||
@@ -788,7 +788,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 id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1341,8 +1341,8 @@ function buildSidebar(id){
|
||||
const xpInLevel = STATE.xp - xpForLevel;
|
||||
const xpRange = xpNext - xpForLevel;
|
||||
const xpPct = xpRange > 0 ? Math.round(xpInLevel / xpRange * 100) : 100;
|
||||
let html = `<div class="xp-card">
|
||||
<div class="xp-card-title">
|
||||
let html = `<div class="xp-card" data-gamified>
|
||||
<div class="xp-card-title" data-gamified>
|
||||
<span>XP-прогресс</span>
|
||||
<span class="xp-level">Ур. ${STATE.level}</span>
|
||||
</div>
|
||||
|
||||
@@ -373,7 +373,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 id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -764,8 +764,8 @@ function buildSidebar(id){
|
||||
const xpInLv = STATE.xp - xpForLv;
|
||||
const xpRange = xpNext - xpForLv;
|
||||
const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
|
||||
html += `<div class="xp-card">
|
||||
<div class="xp-card-title">
|
||||
html += `<div class="xp-card" data-gamified>
|
||||
<div class="xp-card-title" data-gamified>
|
||||
<span>XP-прогресс</span>
|
||||
<span class="xp-level">Ур. ${STATE.level}</span>
|
||||
</div>
|
||||
|
||||
@@ -299,7 +299,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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 id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -679,8 +679,8 @@ function buildSidebar(id){
|
||||
const xpInLv = STATE.xp - xpForLv;
|
||||
const xpRange = xpNext - xpForLv;
|
||||
const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
|
||||
html += `<div class="xp-card">
|
||||
<div class="xp-card-title">
|
||||
html += `<div class="xp-card" data-gamified>
|
||||
<div class="xp-card-title" data-gamified>
|
||||
<span>XP-прогресс</span>
|
||||
<span class="xp-level">Ур. ${STATE.level}</span>
|
||||
</div>
|
||||
|
||||
@@ -147,7 +147,7 @@ html.dark .ch-tag.indigo{color:#818cf8}
|
||||
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
|
||||
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
|
||||
</section>
|
||||
|
||||
<div class="ch-grid">
|
||||
|
||||
@@ -229,7 +229,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -411,7 +411,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -228,7 +228,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -406,7 +406,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -229,7 +229,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -407,7 +407,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -224,7 +224,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -410,7 +410,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -229,7 +229,7 @@ html.dark .final-cta-sub{color:#fcd34d}
|
||||
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
|
||||
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
|
||||
</section>
|
||||
|
||||
<div class="ch-grid">
|
||||
|
||||
@@ -233,7 +233,7 @@ html.dark .final-cta-sub{color:#fcd34d}
|
||||
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
|
||||
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
|
||||
</section>
|
||||
|
||||
<div class="ch-grid">
|
||||
|
||||
@@ -212,7 +212,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -370,7 +370,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -206,7 +206,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -365,7 +365,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -206,7 +206,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -369,7 +369,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -209,7 +209,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -374,7 +374,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -256,7 +256,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -426,7 +426,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -256,7 +256,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -446,7 +446,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -257,7 +257,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -464,7 +464,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -258,7 +258,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -438,7 +438,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -236,7 +236,7 @@ html.dark .final-cta-sub{color:#fcd34d}
|
||||
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
|
||||
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
|
||||
</section>
|
||||
|
||||
<div class="ch-grid">
|
||||
|
||||
@@ -212,7 +212,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -437,7 +437,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -207,7 +207,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -432,7 +432,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -208,7 +208,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -407,7 +407,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -196,7 +196,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -425,7 +425,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -198,7 +198,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -405,7 +405,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -159,7 +159,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
|
||||
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
|
||||
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
|
||||
</section>
|
||||
|
||||
<div class="ch-grid">
|
||||
|
||||
@@ -286,7 +286,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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 id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -558,7 +558,7 @@ function buildSidebar(id){
|
||||
const xpForLv = _xpForLevel(STATE.level), xpNext = _xpForLevel(STATE.level + 1);
|
||||
const xpInLv = STATE.xp - xpForLv, xpRange = xpNext - xpForLv;
|
||||
const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
|
||||
html += `<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>${xpNext} XP</span></div></div>`;
|
||||
html += `<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>${xpNext} XP</span></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>';
|
||||
|
||||
@@ -262,7 +262,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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 id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -432,7 +432,7 @@ function buildSidebar(id){
|
||||
const box=document.getElementById('sidebar-content'); const sb=SIDEBARS[id]||SIDEBARS.p1; let html='';
|
||||
const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+=`<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>${xpNext} XP</span></div></div>`;
|
||||
html+=`<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>${xpNext} XP</span></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" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#065f46;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"><rect x="3" y="3" width="18" height="14" rx="1"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">${tip.html}</div></div>`;
|
||||
|
||||
@@ -254,7 +254,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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 id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -368,7 +368,7 @@ const TIPS=[
|
||||
{sec:'final3',html:'Три признака подобия — главный инструмент геометрических доказательств.'},
|
||||
];
|
||||
|
||||
function buildSidebar(id){const box=document.getElementById('sidebar-content');const sb=SIDEBARS[id]||SIDEBARS.p1;let html='';const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;html+=`<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>${xpNext} XP</span></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" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#4c1d95;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"><polygon points="12,2 22,20 2,20"/></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=>{html+=`<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ${text}</div>`;});html+='</div>';}box.innerHTML=html;if(window.renderMathInElement)try{renderMath(box);}catch(e){}}
|
||||
function buildSidebar(id){const box=document.getElementById('sidebar-content');const sb=SIDEBARS[id]||SIDEBARS.p1;let html='';const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;html+=`<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>${xpNext} XP</span></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" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#4c1d95;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"><polygon points="12,2 22,20 2,20"/></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=>{html+=`<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ${text}</div>`;});html+='</div>';}box.innerHTML=html;if(window.renderMathInElement)try{renderMath(box);}catch(e){}}
|
||||
|
||||
function initTheme(){const t=localStorage.getItem('geometry8_ch3_theme')||'light';if(t==='dark')document.documentElement.classList.add('dark');document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';document.getElementById('theme-btn').addEventListener('click',()=>{document.documentElement.classList.toggle('dark');const dark=document.documentElement.classList.contains('dark');localStorage.setItem('geometry8_ch3_theme',dark?'dark':'light');document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';});}
|
||||
function renderMath(root){if(window.renderMathInElement){try{renderMathInElement(root,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false});}catch(e){}}}
|
||||
|
||||
@@ -260,7 +260,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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 id="hero-xp-badge" class="hero-xp-badge" title="Опыт" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -402,7 +402,7 @@ const TIPS=[
|
||||
{sec:'final4',html:'Ключевая теорема: вписанный угол = ½ дуги. Из неё следуют большинство остальных.'},
|
||||
];
|
||||
|
||||
function buildSidebar(id){const box=document.getElementById('sidebar-content');const sb=SIDEBARS[id]||SIDEBARS.p1;let html='';const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;html+=`<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>${xpNext} XP</span></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" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#0e4f6b;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"><circle cx="12" cy="12" r="9"/></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=>{html+=`<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ${text}</div>`;});html+='</div>';}box.innerHTML=html;if(window.renderMathInElement)try{renderMath(box);}catch(e){}}
|
||||
function buildSidebar(id){const box=document.getElementById('sidebar-content');const sb=SIDEBARS[id]||SIDEBARS.p1;let html='';const xpForLv=_xpForLevel(STATE.level),xpNext=_xpForLevel(STATE.level+1);const xpInLv=STATE.xp-xpForLv,xpRange=xpNext-xpForLv,xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;html+=`<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>${xpNext} XP</span></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" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#0e4f6b;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"><circle cx="12" cy="12" r="9"/></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=>{html+=`<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ${text}</div>`;});html+='</div>';}box.innerHTML=html;if(window.renderMathInElement)try{renderMath(box);}catch(e){}}
|
||||
|
||||
function initTheme(){const t=localStorage.getItem('geometry8_ch4_theme')||'light';if(t==='dark')document.documentElement.classList.add('dark');document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';document.getElementById('theme-btn').addEventListener('click',()=>{document.documentElement.classList.toggle('dark');const dark=document.documentElement.classList.contains('dark');localStorage.setItem('geometry8_ch4_theme',dark?'dark':'light');document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';});}
|
||||
function renderMath(root){if(window.renderMathInElement){try{renderMathInElement(root,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false});}catch(e){}}}
|
||||
|
||||
@@ -162,7 +162,7 @@ main{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
|
||||
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
|
||||
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
|
||||
</section>
|
||||
|
||||
<div class="ch-grid">
|
||||
|
||||
@@ -248,7 +248,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -433,7 +433,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -229,7 +229,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -402,7 +402,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -229,7 +229,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -402,7 +402,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -244,7 +244,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -421,7 +421,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -261,7 +261,7 @@ html.dark .final-cta-sub{color:#fcd34d}
|
||||
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
|
||||
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
|
||||
</section>
|
||||
|
||||
<div class="ch-grid">
|
||||
|
||||
@@ -267,7 +267,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -476,7 +476,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -257,7 +257,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -441,7 +441,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -261,7 +261,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -465,7 +465,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -254,7 +254,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -423,7 +423,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -259,7 +259,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -453,7 +453,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -256,7 +256,7 @@ input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -435,7 +435,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
|
||||
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+v:'')+'</div>'; });
|
||||
html+='</div>';
|
||||
|
||||
@@ -246,7 +246,7 @@ html.dark .final-cta-sub{color:#fcd34d}
|
||||
<div id="overall-text" style="font-size:1.05rem;font-weight:700">Загрузка...</div>
|
||||
<div class="po-bar"><div id="overall-fill" class="po-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none">0 XP</div>
|
||||
<div id="hero-xp-badge" class="po-xp" style="display:none" data-gamified>0 XP</div>
|
||||
</section>
|
||||
|
||||
<div class="ch-grid">
|
||||
|
||||
@@ -194,7 +194,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -367,7 +367,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -189,7 +189,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -371,7 +371,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -189,7 +189,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -388,7 +388,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -185,7 +185,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -346,7 +346,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -185,7 +185,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -346,7 +346,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -185,7 +185,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -356,7 +356,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -185,7 +185,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -367,7 +367,7 @@ function buildSidebar(id){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></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>';
|
||||
|
||||
@@ -155,7 +155,7 @@ a{color:inherit;text-decoration:none}
|
||||
<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"></div>
|
||||
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -240,7 +240,7 @@ function buildSidebar(){
|
||||
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
|
||||
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
|
||||
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
|
||||
html+='<div class="xp-card"><div class="xp-card-title"><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><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>'+xpNext+' XP</span></div></div>';
|
||||
html+='<div class="sidecard"><h4>Курс физики 11</h4>'
|
||||
+ '<div class="sidecard-row"><b>Глав</b> — 8</div>'
|
||||
+ '<div class="sidecard-row"><b>Параграфов</b> — 45</div>'
|
||||
|
||||
@@ -806,14 +806,24 @@ function initPage({ requireLogin = true } = {}) {
|
||||
|
||||
/* ── Feature flags (cached per page load, bust on demand) ───────────── */
|
||||
let _featuresCache = null;
|
||||
/* Synchronous mirror of features.gamification, populated on first
|
||||
loadFeatures() resolve. Used by xp.js to bail out without network. */
|
||||
let _gamificationEnabled = null;
|
||||
function isGamificationEnabled() {
|
||||
// Default to true when the cache hasn't resolved yet — page-load order
|
||||
// means some code runs before /api/features. The CSS kill-switch
|
||||
// (body.no-gamification) catches the visual side regardless.
|
||||
return _gamificationEnabled !== false;
|
||||
}
|
||||
async function loadFeatures() {
|
||||
if (_featuresCache) return _featuresCache;
|
||||
try {
|
||||
_featuresCache = await apiFetch('/api/features');
|
||||
} catch { _featuresCache = {}; }
|
||||
_gamificationEnabled = _featuresCache.gamification !== false;
|
||||
return _featuresCache;
|
||||
}
|
||||
function clearFeaturesCache() { _featuresCache = null; }
|
||||
function clearFeaturesCache() { _featuresCache = null; _gamificationEnabled = null; }
|
||||
|
||||
/**
|
||||
* Show board sidebar link only for teachers/admins and students in a class.
|
||||
@@ -1045,6 +1055,7 @@ window.LS = {
|
||||
patch: (path, body) => apiFetch(path, { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
applyCosmetics: applyCosmetics,
|
||||
refreshNavAvatar,
|
||||
isGamificationEnabled,
|
||||
loadFeatures,
|
||||
clearFeaturesCache,
|
||||
hideDisabledFeatures,
|
||||
@@ -1060,6 +1071,9 @@ window.LS = {
|
||||
═══════════════════════════════════════════════════════════════════════ */
|
||||
async function applyCosmetics() {
|
||||
if (!isLoggedIn()) return;
|
||||
// Skip the round-trip entirely when gamification is off. Server will
|
||||
// 404 anyway, but a synchronous bail-out keeps the network quiet.
|
||||
if (!isGamificationEnabled()) return;
|
||||
try {
|
||||
const c = await getMyActiveCosmetics();
|
||||
if (!c) return;
|
||||
|
||||
@@ -73,8 +73,19 @@
|
||||
try { localStorage.setItem(LS_KEY, String(xp)); } catch (e) {}
|
||||
}
|
||||
|
||||
/* ── Master kill-switch ──
|
||||
If the gamification feature is globally disabled, every public
|
||||
method becomes a no-op so textbooks don't display XP bars and
|
||||
don't burn CPU/network on now-pointless syncs. The body class
|
||||
`.no-gamification` (set by api.js) hides the DOM in parallel. */
|
||||
function _gamOff() {
|
||||
return window.LS && typeof window.LS.isGamificationEnabled === 'function'
|
||||
&& window.LS.isGamificationEnabled() === false;
|
||||
}
|
||||
|
||||
/* ── Загрузка с сервера + merge ── */
|
||||
async function load() {
|
||||
if (_gamOff()) return null;
|
||||
try {
|
||||
const data = await _apiFetch(ENDPOINT_ME, { method: 'GET' });
|
||||
const serverXp = data.xp || 0;
|
||||
@@ -87,6 +98,7 @@
|
||||
return _state;
|
||||
} catch (e) {
|
||||
if (e && e.status === 401) return null; // не авторизован — тихо пропускаем
|
||||
if (e && e.status === 404) return null; // геймификация выключена — тихо
|
||||
return null; // сетевая ошибка — не ломаем учебник
|
||||
}
|
||||
}
|
||||
@@ -124,6 +136,7 @@
|
||||
function flush() {
|
||||
if (_debounceTimer) { clearTimeout(_debounceTimer); _debounceTimer = null; }
|
||||
if (_pending <= 0) return;
|
||||
if (_gamOff()) { _pending = 0; return; } // drop pending — feature off
|
||||
const amount = _pending;
|
||||
_pending = 0;
|
||||
// source для flush — generic, реальный source записывается в каждом add()
|
||||
@@ -152,6 +165,7 @@
|
||||
/* ── add: sync обновление localStorage + постановка в очередь ── */
|
||||
function add(amount, source) {
|
||||
if (!amount || amount <= 0) return;
|
||||
if (_gamOff()) return; // no-op when gamification globally off
|
||||
// Немедленно обновляем локальный XP
|
||||
const newXp = _localXp() + amount;
|
||||
_saveLocal(newXp);
|
||||
|
||||
Reference in New Issue
Block a user