Files
Learn_System/frontend/js/game/levels.js
T
Maxim Dolgolyov 0f3e12426a @
feat(quantik-game): фаза 2 — карта-созвездие + мир + XP/скины (MVP-мир)

Одиночный уровень → играбельный мир: карта-созвездие из 6 физ-уровней
(2 главы, нарастающая сложность), разблокировка по звёздам, клиентский
XP/уровень игрока, пикер из 8 скинов (тинт героя+нарратора), нарратор
PetSprite на интро/победе (mood по звёздам). Навигация карта→интро→игра→
успех→карта/дальше; кнопка «Дальше» пересчитывает nextPlayable после
дозагрузки прогресса (фикс stale-hasNext). Логика прогресса — чистый
модуль progress-logic.js (unlock/XP/группировка). Только фронт, без
бэкенда: XP агрегируется из game_progress (Ф1). Каждый уровень проверен
на реальном движке (выигрываем + обе звезды достижимы); цепочка
разблокировки доказуемо проходима. npm test 251/8 baseline; lint:routes 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-13 16:24:31 +03:00

402 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* ════════════════════════════════════════════════════════════════════════
Квантик — Законы Мира · Реестр уровней (Фаза 2 — мир из 6 физ-уровней).
Уровень = СПЕКА SimForge (данные, не код) + блок `goal` (победа), который
движок (_sim_engine.js) умеет с Фазы 0. Игрок не управляет героем напрямую —
он «чинит закон мира»: крутит слайдеры params (угол/скорость/жёсткость…),
затем «Запуск», и симуляция проигрывается к цели.
── МЕТАДАННЫЕ УРОВНЯ (Фаза 2) ───────────────────────────────────────────
{
id, // == level_id для /api/game/progress
title, // отображаемое имя узла карты
chapter, // ключ главы-созвездия (группировка на карте)
order, // глобальный порядок (для «предыдущих» при разблокировке и «Дальше»)
unlockStars, // порог: сумма звёзд во ВСЕХ предыдущих уровнях, чтобы открыть (деф. 0)
par_ms?, // норматив времени для 3-й звезды (мс мирового времени)
subject?, // тема (физика)
hint?, // подсказка-нарратив для интро
spec // обычная спека SimForge с блоком goal
}
Звёзды: зв.1 — достичь цели; зв.2 — собрать бонус (кристалл); зв.3 — уложиться
в par_ms. par-звезда выражается напрямую через мировое время t: `t*1000 <= PAR`
(вычисляется в момент победы; идентификатор tries для неё не нужен).
ИСТОЧНИК УРОВНЕЙ: встроенные данные здесь (window.QuantikLevels). Авторённые
уровни (custom_sims cat='game') подмешаются в Фазе 5 (реестр станет асинхронным).
⛔ Без eval/Function. Все «числовые» поля могут быть числом ИЛИ строкой-
выражением (их безопасно вычисляет SimExpr на клиенте).
════════════════════════════════════════════════════════════════════════ */
(function (global) {
var BG = '#0D0D1A';
var GROUND = '#334155';
var HERO = '#22D3EE'; // дефолтный цвет героя (тинтуется скином — см. quantik-game.js)
var PORTAL = '#A78BFA'; // фиолет — цель
var CRYSTAL = '#F472B6'; // розовый — бонус
/* helper: общий объект «герой-тело» (point с body) — стартует из (sx,sy). */
function hero(sx, sy, vxExpr, vyExpr) {
return {
id: 'ball', type: 'point', r: 7, color: HERO,
x: sx, y: sy,
glow: true, glowColor: HERO, trail: true, trailColor: HERO, trailFade: true,
body: { mass: 1, vx: vxExpr, vy: vyExpr }
};
}
/* helper: светящийся портал-кольцо (визуал цели). */
function portalObjs(px, py, r) {
return [
{ type: 'circle', x: px, y: py, r: r, color: PORTAL, width: 3, glow: true, glowColor: PORTAL },
{ type: 'circle', x: px, y: py, r: r * 0.45, color: PORTAL, width: 2, opacity: 0.7 },
{ type: 'label', x: px, y: py + r + 0.9, text: 'портал', color: PORTAL, size: 12 }
];
}
function crystalObjs(cx, cy, r) {
return [
{ type: 'circle', x: cx, y: cy, r: r, color: CRYSTAL, width: 2, glow: true, glowColor: CRYSTAL },
{ type: 'label', x: cx, y: cy + r + 0.8, text: 'кристалл', color: CRYSTAL, size: 11 }
];
}
/* ─────────────────────────────────────────────────────────────────────────
Глава I — «Кинематика» (созвездие): полёт под действием гравитации.
──────────────────────────────────────────────────────────────────────── */
/* Уровень 1: «Артиллерия Квантика» — базовый бросок под углом. */
var L1_PX = 8, L1_PY = 0.7, L1_PR = 0.75;
var L1_CX = 4, L1_CY = 2.7, L1_CR = 0.7;
var artillery1 = {
id: 'phys-artillery-1',
title: 'Артиллерия',
chapter: 'kinematics',
order: 1,
unlockStars: 0,
par_ms: 1500,
subject: 'physics',
hint: 'Подбери угол и скорость, чтобы Квантик долетел до портала. Собери кристалл по дороге — это вторая звезда. Быстрый бросок даст третью.',
spec: {
specVersion: 1,
meta: { title: 'Артиллерия Квантика', desc: 'Закон движения: бросок под углом к горизонту.' },
viewport: { xmin: -1, xmax: 12, ymin: -1.2, ymax: 7, grid: true, axes: true, bg: BG },
params: [
{ name: 'theta', label: 'Угол', min: 10, max: 80, step: 1, value: 45, unit: '°' },
{ name: 'v', label: 'Скорость', min: 5, max: 20, step: 0.5, value: 10, unit: 'м/с' }
],
physics: { enabled: true, gravity: { x: 0, y: -9.8 } },
objects: [
{ type: 'segment', x1: -1, y1: 0, x2: 12, y2: 0, color: GROUND, width: 2 }
].concat(crystalObjs(L1_CX, L1_CY, L1_CR), portalObjs(L1_PX, L1_PY, L1_PR), [
hero(0, 0, 'v*cos(theta*pi/180)', 'v*sin(theta*pi/180)'),
{ type: 'readout', label: 'v', expr: 'v', unit: 'м/с', precision: 1 },
{ type: 'readout', label: 'θ', expr: 'theta', unit: '°', precision: 0 }
]),
goal: {
title: 'Попади в портал',
hint: 'Достигни портала. Бонус: собери кристалл и уложись в норматив.',
when: 'hypot(ball.x - ' + L1_PX + ', ball.y - ' + L1_PY + ') < ' + L1_PR,
fail: 'ball.x > 11.5 || ball.y < -1.0',
stars: [
{ when: 'hypot(ball.x - ' + L1_CX + ', ball.y - ' + L1_CY + ') < ' + L1_CR, label: 'Собрал кристалл' },
{ when: 't*1000 <= 1500', label: 'Быстро (≤1.5 с)' }
]
}
}
};
/* Уровень 2: «Перелёт через стену» — между стартом и порталом стоит высокая
стена; нужно перебросить Квантика по дуге. Кристалл — на вершине дуги. */
var L2_PX = 9.5, L2_PY = 0.7, L2_PR = 0.8;
var L2_WALLX = 5, L2_WALLH = 3.6; // вертикальная стена-препятствие
var L2_CX = 5, L2_CY = 4.4, L2_CR = 0.7; // кристалл над стеной
var arc2 = {
id: 'phys-arc-2',
title: 'Перелёт через стену',
chapter: 'kinematics',
order: 2,
unlockStars: 1,
par_ms: 1800,
subject: 'physics',
hint: 'Стена преграждает прямой путь. Подбери крутую дугу — переброс Квантика через гребень в портал. Кристалл ждёт на вершине.',
spec: {
specVersion: 1,
meta: { title: 'Перелёт через стену', desc: 'Дальность и высота броска: перебрось препятствие.' },
viewport: { xmin: -1, xmax: 13, ymin: -1.2, ymax: 8, grid: true, axes: true, bg: BG },
params: [
{ name: 'theta', label: 'Угол', min: 20, max: 85, step: 1, value: 60, unit: '°' },
{ name: 'v', label: 'Скорость', min: 6, max: 22, step: 0.5, value: 12, unit: 'м/с' }
],
physics: { enabled: true, gravity: { x: 0, y: -9.8 } },
objects: [
{ type: 'segment', x1: -1, y1: 0, x2: 13, y2: 0, color: GROUND, width: 2 },
// стена-препятствие
{ type: 'segment', x1: L2_WALLX, y1: 0, x2: L2_WALLX, y2: L2_WALLH, color: '#475569', width: 6 },
{ type: 'label', x: L2_WALLX, y: L2_WALLH + 0.5, text: 'стена', color: '#94A3B8', size: 11 }
].concat(crystalObjs(L2_CX, L2_CY, L2_CR), portalObjs(L2_PX, L2_PY, L2_PR), [
hero(0, 0, 'v*cos(theta*pi/180)', 'v*sin(theta*pi/180)'),
{ type: 'readout', label: 'v', expr: 'v', unit: 'м/с', precision: 1 },
{ type: 'readout', label: 'θ', expr: 'theta', unit: '°', precision: 0 }
]),
goal: {
title: 'Перебрось стену в портал',
hint: 'Перелети стену и попади в портал. Бонус: задень кристалл на вершине.',
when: 'hypot(ball.x - ' + L2_PX + ', ball.y - ' + L2_PY + ') < ' + L2_PR,
// проигрыш: врезался в стену (рядом с ней и ниже её верха) либо улетел/упал за поле
fail: '(abs(ball.x - ' + L2_WALLX + ') < 0.22 && ball.y < ' + L2_WALLH + ') || ball.x > 12.5 || ball.y < -1.0',
stars: [
{ when: 'hypot(ball.x - ' + L2_CX + ', ball.y - ' + L2_CY + ') < ' + L2_CR, label: 'Собрал кристалл' },
{ when: 't*1000 <= 1800', label: 'Быстро (≤1.8 с)' }
]
}
}
};
/* Уровень 3: «Отскок» — рабочая зона как упругий ящик (пол + правая стена).
Прямого пути к высокому порталу слева-вверху нет: брось вправо, упругий
отскок от правой стены и пола приводит Квантика по ломаной к порталу.
Кристалл — у правой стены (на дуге до отскока). Тюнинг угла/скорости/упругости. */
var L3_PX = 1.6, L3_PY = 4.6, L3_PR = 0.95; // портал слева-вверху
var L3_CX = 8.4, L3_CY = 3.4, L3_CR = 0.85; // кристалл у правой стены
var bounce3 = {
id: 'phys-bounce-3',
title: 'Отскок',
chapter: 'kinematics',
order: 3,
unlockStars: 2,
par_ms: 2800,
subject: 'physics',
hint: 'Портал слева-вверху, прямого пути нет. Брось вправо — упругая стена отразит Квантика обратно. Поиграй упругостью: чем жёстче отскок, тем выше дуга назад.',
spec: {
specVersion: 1,
meta: { title: 'Отскок', desc: 'Упругое столкновение: отскок от стены.' },
viewport: { xmin: -1, xmax: 11, ymin: -1.0, ymax: 8, grid: true, axes: true, bg: BG },
params: [
{ name: 'theta', label: 'Угол', min: 20, max: 75, step: 1, value: 50, unit: '°' },
{ name: 'v', label: 'Скорость', min: 8, max: 24, step: 0.5, value: 16, unit: 'м/с' },
{ name: 'el', label: 'Упругость', min: 0.55, max: 0.95, step: 0.01, value: 0.8 }
],
physics: {
enabled: true,
gravity: { x: 0, y: -9.8 },
restitution: 'el',
walls: [{ side: 'bottom' }, { side: 'right' }]
},
objects: [
{ type: 'segment', x1: -1, y1: 0, x2: 11, y2: 0, color: GROUND, width: 3 },
{ type: 'segment', x1: 11, y1: 0, x2: 11, y2: 8, color: '#475569', width: 4 },
{ type: 'label', x: 10.2, y: 7.2, text: 'упр. стена', color: '#94A3B8', size: 11 }
].concat(crystalObjs(L3_CX, L3_CY, L3_CR), portalObjs(L3_PX, L3_PY, L3_PR), [
hero(0, 0.2, 'v*cos(theta*pi/180)', 'v*sin(theta*pi/180)'),
{ type: 'readout', label: 'упр', expr: 'el', precision: 2 },
{ type: 'readout', label: 'v', expr: 'v', unit: 'м/с', precision: 1 }
]),
goal: {
title: 'Отскоком в портал',
hint: 'Отрази Квантика от правой стены так, чтобы он вернулся в портал слева-вверху. Бонус: задень кристалл у стены.',
when: 'hypot(ball.x - ' + L3_PX + ', ball.y - ' + L3_PY + ') < ' + L3_PR,
fail: 't > 8',
stars: [
{ when: 'hypot(ball.x - ' + L3_CX + ', ball.y - ' + L3_CY + ') < ' + L3_CR, label: 'Собрал кристалл' },
{ when: 't*1000 <= 2800', label: 'Быстро (≤2.8 с)' }
]
}
}
};
/* ─────────────────────────────────────────────────────────────────────────
Глава II — «Динамика» (созвездие): силы, пружины, орбиты.
──────────────────────────────────────────────────────────────────────── */
/* Уровень 4: «Маятник» — Квантик подвешен на пружине к якорю сверху; даём ему
горизонтальный толчок. Пружина (закон Гука) тянет назад — он качается дугой.
Подбери начальную скорость и жёсткость, чтобы нижняя точка дуги прошла через
портал. Без гравитации вниз — чистая пружинная динамика к центру. */
var L4_ANCHOR_X = 4, L4_ANCHOR_Y = 7.4;
var L4_REST = 1.2; // короткая длина покоя -> пружина растянута -> сильный возврат
var L4_START_Y = 2.6; // тело висит ниже якоря (растяжение ~3.6)
var L4_PX = 7.4, L4_PY = 4.6, L4_PR = 1.0; // портал на правом плече дуги
var L4_CX = 4.0, L4_CY = 6.0, L4_CR = 0.9; // кристалл у верхней точки качания (ближе к якорю)
var pendulum4 = {
id: 'phys-pendulum-4',
title: 'Маятник',
chapter: 'dynamics',
order: 4,
unlockStars: 4,
par_ms: 3600,
subject: 'physics',
hint: 'Квантик висит на растянутой пружине у якоря. Толкни его вбок — закон Гука раскачает дугу вверх. Подбери толчок и жёсткость, чтобы плечо дуги достало портал.',
spec: {
specVersion: 1,
meta: { title: 'Маятник на пружине', desc: 'Закон Гука: гармонические колебания.' },
viewport: { xmin: -2, xmax: 11, ymin: -1, ymax: 9, grid: true, axes: true, bg: BG },
params: [
{ name: 'push', label: 'Толчок', min: 4, max: 18, step: 0.5, value: 10, unit: 'м/с' },
{ name: 'k', label: 'Жёсткость', min: 8, max: 50, step: 1, value: 24 }
],
physics: {
enabled: true,
gravity: { x: 0, y: 0 }, // чисто пружинная динамика (анализ закона Гука)
springs: [
{ a: [L4_ANCHOR_X, L4_ANCHOR_Y], b: 'ball', k: 'k', length: L4_REST }
]
},
objects: [
// якорь
{ type: 'circle', x: L4_ANCHOR_X, y: L4_ANCHOR_Y, r: 0.18, color: '#94A3B8', width: 0, fill: '#94A3B8' },
{ type: 'label', x: L4_ANCHOR_X, y: L4_ANCHOR_Y + 0.6, text: 'якорь', color: '#94A3B8', size: 11 },
// линия-пружина (визуальная связь якорь→тело)
{ type: 'segment', x1: L4_ANCHOR_X, y1: L4_ANCHOR_Y, x2: 'ball.x', y2: 'ball.y', color: '#475569', width: 1, lineStyle: 'dashed' }
].concat(crystalObjs(L4_CX, L4_CY, L4_CR), portalObjs(L4_PX, L4_PY, L4_PR), [
// тело висит ниже якоря, горизонтальный толчок вправо
hero(L4_ANCHOR_X, L4_START_Y, 'push', '0'),
{ type: 'readout', label: 'толчок', expr: 'push', unit: 'м/с', precision: 1 },
{ type: 'readout', label: 'k', expr: 'k', precision: 0 }
]),
goal: {
title: 'Качни в портал',
hint: 'Раскачай Квантика на пружине так, чтобы дуга прошла через портал. Бонус: задень кристалл у верхней точки.',
when: 'hypot(ball.x - ' + L4_PX + ', ball.y - ' + L4_PY + ') < ' + L4_PR,
fail: 't > 12',
stars: [
{ when: 'hypot(ball.x - ' + L4_CX + ', ball.y - ' + L4_CY + ') < ' + L4_CR, label: 'Собрал кристалл' },
{ when: 't*1000 <= 3600', label: 'Быстро (≤3.6 с)' }
]
}
}
};
/* Уровень 5: «Орбита» — центральная пружина-«гравитационный колодец» к центру
(закон Гука к центру == гармонический осциллятор == эллиптические орбиты).
Даём тангенциальную скорость; подбери её и силу колодца, чтобы орбита прошла
через портал-кольцо на её пути. */
var L5_CENTER_X = 4, L5_CENTER_Y = 3;
var L5_START_X = 4, L5_START_Y = 6; // старт над центром (радиус 3)
var L5_PX = 6.6, L5_PY = 3, L5_PR = 0.95; // портал на правом плече орбиты (внутри замкнутого витка)
var L5_CX = 4, L5_CY = 0.1, L5_CR = 0.9; // кристалл в нижней точке орбиты
var orbit5 = {
id: 'phys-orbit-5',
title: 'Орбита',
chapter: 'dynamics',
order: 5,
unlockStars: 6,
par_ms: 4200,
subject: 'physics',
hint: 'Колодец притягивает Квантика к центру (закон Гука). Дай ему боковой разгон — он выйдет на орбиту. Подбери скорость и силу колодца, чтобы виток прошёл сквозь портал.',
spec: {
specVersion: 1,
meta: { title: 'Орбита', desc: 'Центральная сила: замкнутая орбита через цель.' },
viewport: { xmin: -2, xmax: 11, ymin: -3, ymax: 9, grid: true, axes: true, bg: BG },
params: [
{ name: 'vt', label: 'Боковой разгон', min: 2, max: 14, step: 0.25, value: 6, unit: 'м/с' },
{ name: 'g', label: 'Сила колодца', min: 4, max: 30, step: 0.5, value: 12 }
],
physics: {
enabled: true,
gravity: { x: 0, y: 0 },
// пружина к центру с нулевой длиной покоя == центральная гармоническая сила F=-k·r
springs: [
{ a: [L5_CENTER_X, L5_CENTER_Y], b: 'ball', k: 'g', length: 0 }
]
},
objects: [
// центр-колодец
{ type: 'circle', x: L5_CENTER_X, y: L5_CENTER_Y, r: 0.3, color: '#F59E0B', width: 0, fill: '#F59E0B', glow: true, glowColor: '#F59E0B' },
{ type: 'label', x: L5_CENTER_X, y: L5_CENTER_Y - 0.9, text: 'колодец', color: '#F59E0B', size: 11 }
].concat(crystalObjs(L5_CX, L5_CY, L5_CR), portalObjs(L5_PX, L5_PY, L5_PR), [
// старт над центром, скорость вправо (vt) -> орбита по часовой
hero(L5_START_X, L5_START_Y, 'vt', '0'),
{ type: 'readout', label: 'разгон', expr: 'vt', unit: 'м/с', precision: 2 },
{ type: 'readout', label: 'сила', expr: 'g', precision: 1 }
]),
goal: {
title: 'Выйди на орбиту через портал',
hint: 'Орбита Квантика должна пройти через портал-кольцо. Бонус: задень кристалл в дальней точке.',
when: 'hypot(ball.x - ' + L5_PX + ', ball.y - ' + L5_PY + ') < ' + L5_PR,
fail: 't > 14',
stars: [
{ when: 'hypot(ball.x - ' + L5_CX + ', ball.y - ' + L5_CY + ') < ' + L5_CR, label: 'Собрал кристалл' },
{ when: 't*1000 <= 4200', label: 'Быстро (≤4.2 с)' }
]
}
}
};
/* Уровень 6: «Гравитационный манёвр» — капстоун главы. Гравитация тянет вниз,
но в центре поля — притягивающий колодец (пружина к центру), искривляющий путь.
Брось Квантика так, чтобы колодец завернул его дугу в портал в дальнем верхнем
углу. Комбинируем бросок под углом, гравитацию и центральную силу. */
var L6_WELL_X = 5, L6_WELL_Y = 3;
var L6_PX = 9.4, L6_PY = 5.6, L6_PR = 1.0; // портал в дальнем верхнем углу
var L6_CX = 5, L6_CY = 4.3, L6_CR = 0.85; // кристалл над колодцем (на восходящей дуге)
var slingshot6 = {
id: 'phys-slingshot-6',
title: 'Гравиманёвр',
chapter: 'dynamics',
order: 6,
unlockStars: 8,
par_ms: 3400,
subject: 'physics',
hint: 'Гравитация тянет вниз, а колодец в центре притягивает к себе. Брось Квантика так, чтобы колодец завернул его дугу в портал в дальнем верхнем углу. Подбери угол, скорость и силу колодца.',
spec: {
specVersion: 1,
meta: { title: 'Гравитационный манёвр', desc: 'Гравитация + центральная сила: манёвр у колодца.' },
viewport: { xmin: -1, xmax: 11, ymin: -1, ymax: 8, grid: true, axes: true, bg: BG },
params: [
{ name: 'theta', label: 'Угол', min: 20, max: 80, step: 1, value: 55, unit: '°' },
{ name: 'v', label: 'Скорость', min: 8, max: 22, step: 0.5, value: 14, unit: 'м/с' },
{ name: 'g', label: 'Сила колодца', min: 0, max: 16, step: 0.5, value: 6 }
],
physics: {
enabled: true,
gravity: { x: 0, y: -9.8 },
springs: [
{ a: [L6_WELL_X, L6_WELL_Y], b: 'ball', k: 'g', length: 0 }
]
},
objects: [
{ type: 'segment', x1: -1, y1: 0, x2: 11, y2: 0, color: GROUND, width: 3 },
// колодец-масса
{ type: 'circle', x: L6_WELL_X, y: L6_WELL_Y, r: 0.3, color: '#F59E0B', width: 0, fill: '#F59E0B', glow: true, glowColor: '#F59E0B' },
{ type: 'label', x: L6_WELL_X, y: L6_WELL_Y - 0.9, text: 'колодец', color: '#F59E0B', size: 11 }
].concat(crystalObjs(L6_CX, L6_CY, L6_CR), portalObjs(L6_PX, L6_PY, L6_PR), [
hero(0, 0.2, 'v*cos(theta*pi/180)', 'v*sin(theta*pi/180)'),
{ type: 'readout', label: 'сила', expr: 'g', precision: 1 },
{ type: 'readout', label: 'θ', expr: 'theta', unit: '°', precision: 0 }
]),
goal: {
title: 'Манёвром в портал',
hint: 'Используй колодец, чтобы завернуть дугу Квантика в дальний верхний портал. Бонус: задень кристалл на восходящей дуге.',
when: 'hypot(ball.x - ' + L6_PX + ', ball.y - ' + L6_PY + ') < ' + L6_PR,
fail: 'ball.y < -1 || t > 10',
stars: [
{ when: 'hypot(ball.x - ' + L6_CX + ', ball.y - ' + L6_CY + ') < ' + L6_CR, label: 'Собрал кристалл' },
{ when: 't*1000 <= 3400', label: 'Быстро (≤3.4 с)' }
]
}
}
};
var LEVELS = [artillery1, arc2, bounce3, pendulum4, orbit5, slingshot6];
/* Метаданные глав (созвездий) — для заголовков/оформления карты. */
var CHAPTERS = {
kinematics: { key: 'kinematics', title: 'Кинематика', subtitle: 'Полёт и гравитация', accent: '#22D3EE' },
dynamics: { key: 'dynamics', title: 'Динамика', subtitle: 'Силы, пружины, орбиты', accent: '#A78BFA' }
};
function list() { return LEVELS.slice(); }
function get(id) {
for (var i = 0; i < LEVELS.length; i++) if (LEVELS[i].id === id) return LEVELS[i];
return null;
}
function chapter(key) { return CHAPTERS[key] || { key: key, title: key, subtitle: '', accent: '#22D3EE' }; }
global.QuantikLevels = {
list: list, get: get, chapter: chapter,
LEVELS: LEVELS, CHAPTERS: CHAPTERS
};
})(typeof window !== 'undefined' ? window : this);