'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 с)' } ] } } }; /* ───────────────────────────────────────────────────────────────────────── Глава III — «Функции» (созвездие): Квантик едет по кривой y=f(x), которую СОБИРАЕТ игрок (слайдеры коэффициентов). Препятствия — зоны (Ф3 движка): forbidden — задел → проигрыш (fail); target — добрался → победа (goal); collect — задел → звезда-бонус (sticky). Реюз: plot+runner рисует кривую и ведёт по ней героя (одна скомпил. функция), зоны дают булевы env-поля .hit. Без eval — только SimExpr-выражения. ──────────────────────────────────────────────────────────────────────── */ var FORB = '#F87171'; // запретная зона (красный) var TARG = '#34D399'; // целевая зона (зелёный) var COIN = '#FBBF24'; // зона-сбор (золото) var CURVE = '#67E8F9'; // цвет кривой-«дороги» /* helper: герой-точка, едет по бегунку кривой (id 'curve'): x=curve.runX, y=curve.runY. glow+trail (визуал P2). НЕ тело (кинематический проход) → не само-ссылка. */ function graphHero() { return { id: 'ball', type: 'point', r: 7, color: HERO, x: 'curve.runX', y: 'curve.runY', glow: true, glowColor: HERO, trail: true, trailColor: HERO, trailFade: true }; } /* helper: plot-«дорога» = кривая f(x) с бегунком. exprStr — выражение кривой. */ function road(exprStr, a, b, dur) { return { id: 'curve', type: 'plot', expr: exprStr, var: 'x', range: [a, b], samples: 220, color: CURVE, width: 3, glow: true, glowColor: CURVE, runner: { duration: dur, hold: true } }; } function rectZone(id, kind, x, y, w, h, label) { var col = kind === 'target' ? TARG : kind === 'collect' ? COIN : FORB; return { type: 'zone', id: id, kind: kind, shape: 'rect', x: x, y: y, w: w, h: h, color: col, label: label || '' }; } function circZone(id, kind, x, y, r, label) { var col = kind === 'target' ? TARG : kind === 'collect' ? COIN : FORB; return { type: 'zone', id: id, kind: kind, shape: 'circle', x: x, y: y, r: r, color: col, label: label || '' }; } function startMarker(x, y) { return { type: 'circle', x: x, y: y, r: 0.16, color: '#94A3B8', width: 0, fill: '#94A3B8' }; } /* Уровень 7: «Луч через ворота» — прямая f(x)=a·x+b. Сверху и снизу в центре — запретные брусья; проведи луч между ними в зелёный портал справа. Монета по центру. */ var graphLine7 = { id: 'graph-line-7', title: 'Луч через ворота', chapter: 'functions', order: 7, unlockStars: 9, par_ms: 5200, subject: 'algebra', hint: 'Квантик поедет по прямой y = a·x + b. Подбери наклон a и сдвиг b, чтобы луч прошёл между брусьями в воротах и достиг зелёного портала. Монета — точно в центре прохода.', spec: { specVersion: 1, meta: { title: 'Луч через ворота', desc: 'Линейная функция: y = a·x + b.' }, viewport: { xmin: -1, xmax: 11, ymin: -1, ymax: 8, grid: true, axes: true, bg: BG }, time: { duration: 5, loop: false }, params: [ { name: 'a', label: 'Наклон a', min: -0.5, max: 1.5, step: 0.05, value: 0.2 }, { name: 'b', label: 'Сдвиг b', min: 0, max: 5, step: 0.1, value: 1 } ], objects: [ // ворота в центре: верхний и нижний брус (проход 1..6 по y при x≈5) rectZone('topbar', 'forbidden', 5, 7.0, 4, 2.4, 'стена'), rectZone('botbar', 'forbidden', 5, -0.2, 4, 2.4, 'стена'), // монета в центре прохода circZone('coin', 'collect', 5, 3, 0.7, 'монета'), // целевой портал справа circZone('gate', 'target', 10, 5, 0.95, 'портал'), startMarker(0, 1), road('a*x + b', 0, 10, 5), graphHero(), { type: 'readout', label: 'a', expr: 'a', precision: 2 }, { type: 'readout', label: 'b', expr: 'b', precision: 1 } ], goal: { title: 'Проведи луч в портал', hint: 'Достигни зелёного портала, не задев красные брусья. Бонус: собери монету.', when: 'gate.hit', fail: 'topbar.hit || botbar.hit', stars: [ { when: 'coin.hit', label: 'Собрал монету' }, { when: 'gate.hit && b < 1.6', label: 'Низкий старт (b < 1.6)' } ] } } }; /* Уровень 8: «Синус сквозь врата» — f(x)=A·sin(k·x). Сверху/снизу — зубцы-стены с просветами; подбери амплитуду и частоту, чтобы волна прошла сквозь все просветы. */ var graphSine8 = { id: 'graph-sine-8', title: 'Синус сквозь врата', chapter: 'functions', order: 8, unlockStars: 11, par_ms: 6000, subject: 'algebra', hint: 'Квантик едет по волне y = A·sin(k·x). Подбери амплитуду A и частоту k, чтобы волна прошла сквозь просветы между зубцами. Монеты — в гребне и впадине волны.', spec: { specVersion: 1, meta: { title: 'Синус сквозь врата', desc: 'Синусоида: y = A·sin(k·x).' }, viewport: { xmin: -0.5, xmax: 12.5, ymin: -5, ymax: 5, grid: true, axes: true, bg: BG }, time: { duration: 6, loop: false }, params: [ { name: 'A', label: 'Амплитуда A', min: 1, max: 4, step: 0.1, value: 2 }, { name: 'k', label: 'Частота k', min: 0.3, max: 1.6, step: 0.05, value: 0.6 } ], objects: [ // зубцы: верхний брус над гребнем (x≈π/2k) и нижний под впадиной (x≈3π/2k) // при целевом k≈0.79 (π/4): гребень x≈2, впадина x≈6, гребень x≈10 rectZone('spikeTop', 'forbidden', 2, 4.4, 1.2, 2.4, ''), rectZone('spikeBot', 'forbidden', 6, -4.4, 1.2, 2.4, ''), rectZone('spikeTop2', 'forbidden', 10, 4.4, 1.2, 2.4, ''), // монеты в гребне/впадине circZone('coinTop', 'collect', 2, 3, 0.6, ''), circZone('coinBot', 'collect', 6, -3, 0.6, ''), // портал на третьем гребне circZone('gate', 'target', 11.2, 0, 1.0, 'портал'), startMarker(0, 0), road('A*sin(k*x)', 0, 11.5, 6), graphHero(), { type: 'readout', label: 'A', expr: 'A', precision: 1 }, { type: 'readout', label: 'k', expr: 'k', precision: 2 } ], goal: { title: 'Проведи волну в портал', hint: 'Подбери волну так, чтобы пройти все просветы и достичь портала. Бонус: собери монеты в гребне и впадине.', when: 'gate.hit', fail: 'spikeTop.hit || spikeBot.hit || spikeTop2.hit', stars: [ { when: 'coinTop.hit', label: 'Монета-гребень' }, { when: 'coinBot.hit', label: 'Монета-впадина' } ] } } }; /* Уровень 9: «Парабола над ямой» — f(x)=a·(x-3)^2+k (вершина в (3,k)... меняем). В центре — запретная яма; перебрось параболу-дугу над ней в портал. */ var graphParab9 = { id: 'graph-parab-9', title: 'Парабола над ямой', chapter: 'functions', order: 9, unlockStars: 13, par_ms: 6000, subject: 'algebra', hint: 'Квантик едет по параболе y = a·(x − 5)² + k. Подбери раскрытие a (вниз — отрицательное) и высоту вершины k, чтобы дуга прошла над ямой в портал. Монета — на вершине дуги.', spec: { specVersion: 1, meta: { title: 'Парабола над ямой', desc: 'Квадратичная: y = a·(x − 5)² + k.' }, viewport: { xmin: -1, xmax: 11, ymin: -1, ymax: 9, grid: true, axes: true, bg: BG }, time: { duration: 6, loop: false }, params: [ { name: 'a', label: 'Раскрытие a', min: -0.6, max: -0.1, step: 0.02, value: -0.3 }, { name: 'k', label: 'Высота вершины k', min: 4, max: 8, step: 0.1, value: 6 } ], objects: [ // яма в центре (запретная) — занимает низ по центру rectZone('pit', 'forbidden', 5, 1.4, 5.0, 3.0, 'яма'), // монета на вершине circZone('coin', 'collect', 5, 6, 0.7, 'монета'), // портал у правого края, низко (нужно спуститься на нисходящей ветви) circZone('gate', 'target', 9.5, 4.4, 0.95, 'портал'), startMarker(0.5, 0), // ориентир старта (фактический старт = f(0)) road('a*(x-5)^2 + k', 0, 9.5, 6), graphHero(), { type: 'readout', label: 'a', expr: 'a', precision: 2 }, { type: 'readout', label: 'k', expr: 'k', precision: 1 } ], goal: { title: 'Дугой над ямой в портал', hint: 'Перебрось дугу над ямой к порталу. Бонус: задень монету на вершине.', when: 'gate.hit', fail: 'pit.hit', stars: [ { when: 'coin.hit', label: 'Собрал монету' }, { when: 'gate.hit && k >= 6.4', label: 'Высокая дуга (k ≥ 6.4)' } ] } } }; /* Уровень 10: «Угол модуля» — f(x)=a·abs(x-h)+1. V-образная кривая: подбери раскрытие a и положение вершины h, чтобы «галочка» обошла два бруса и попала в портал. */ var graphAbs10 = { id: 'graph-abs-10', title: 'Угол модуля', chapter: 'functions', order: 10, unlockStars: 15, par_ms: 6000, subject: 'algebra', hint: 'Квантик едет по «галочке» y = a·|x − m| + 1. Подбери крутизну a и положение вершины m, чтобы остриё прошло в проход и луч попал в портал. Монета — у самого острия.', spec: { specVersion: 1, meta: { title: 'Угол модуля', desc: 'Модуль: y = a·|x − m| + 1.' }, viewport: { xmin: -1, xmax: 11, ymin: -1, ymax: 9, grid: true, axes: true, bg: BG }, time: { duration: 6, loop: false }, params: [ // m = положение вершины (имя 'h' зарезервировано движком под высоту вьюпорта!) { name: 'a', label: 'Крутизна a', min: 0.6, max: 2.2, step: 0.05, value: 1.2 }, { name: 'm', label: 'Вершина m', min: 3, max: 7, step: 0.1, value: 5 } ], objects: [ // нижние брусья слева и справа от прохода-острия (вершина должна попасть в проход) rectZone('floorL', 'forbidden', 2.4, -0.2, 2.4, 1.4, ''), rectZone('floorR', 'forbidden', 7.6, -0.2, 2.4, 1.4, ''), // монета у острия (низко, по центру) circZone('coin', 'collect', 5, 1, 0.7, 'монета'), // портал справа-вверху (на восходящей ветви) circZone('gate', 'target', 9.6, 6.4, 1.0, 'портал'), startMarker(0, 7), road('a*abs(x-m) + 1', 0, 9.6, 6), graphHero(), { type: 'readout', label: 'a', expr: 'a', precision: 2 }, { type: 'readout', label: 'm', expr: 'm', precision: 1 } ], goal: { title: 'Галочкой в портал', hint: 'Проведи остриё галочки в проход между брусьями и попади в портал. Бонус: задень монету у острия.', when: 'gate.hit', fail: 'floorL.hit || floorR.hit', stars: [ { when: 'coin.hit', label: 'Собрал монету' }, { when: 'gate.hit && m >= 4.6 && m <= 5.4', label: 'Вершина по центру' } ] } } }; /* Уровень 11 (капстоун): «Экспонента-подъём» — f(x)=c·exp(r·x). Барьер-«потолок» слева заставляет держать низкий старт; затем экспонента взмывает в высокий портал. */ var graphExp11 = { id: 'graph-exp-11', title: 'Экспонента-подъём', chapter: 'functions', order: 11, unlockStars: 17, par_ms: 6500, subject: 'algebra', hint: 'Квантик едет по экспоненте y = c·e^(r·x). Подбери начальную высоту c и скорость роста r, чтобы пройти под потолком слева, но взмыть в высокий портал справа. Монета — на разгоне.', spec: { specVersion: 1, meta: { title: 'Экспонента-подъём', desc: 'Экспонента: y = c·e^(r·x).' }, viewport: { xmin: -0.5, xmax: 8.5, ymin: -0.5, ymax: 9, grid: true, axes: true, bg: BG }, time: { duration: 6.5, loop: false }, params: [ { name: 'c', label: 'Старт c', min: 0.2, max: 1.5, step: 0.05, value: 0.6 }, { name: 'r', label: 'Рост r', min: 0.2, max: 0.7, step: 0.02, value: 0.4 } ], objects: [ // потолок слева (низкий старт): запретный брус над началом x∈[1,4], y от 2.5 вверх rectZone('ceil', 'forbidden', 2.5, 4.0, 3.2, 3.0, 'потолок'), // пол-яма в начале (нельзя слишком низко/нулём): брус снизу x∈[0,5] rectZone('floor', 'forbidden', 2.4, -0.5, 5.0, 1.0, ''), circZone('coin', 'collect', 5, 3.3, 0.7, 'монета'), circZone('gate', 'target', 7.6, 7.2, 1.05, 'портал'), startMarker(0, 0.6), road('c*exp(r*x)', 0, 7.7, 6.5), graphHero(), { type: 'readout', label: 'c', expr: 'c', precision: 2 }, { type: 'readout', label: 'r', expr: 'r', precision: 2 } ], goal: { title: 'Взмой в портал', hint: 'Пройди под потолком слева и взмой экспонентой в высокий портал. Бонус: задень монету на разгоне.', when: 'gate.hit', fail: 'ceil.hit || floor.hit', stars: [ { when: 'coin.hit', label: 'Собрал монету' }, { when: 'gate.hit && r >= 0.42', label: 'Крутой рост (r ≥ 0.42)' } ] } } }; /* ───────────────────────────────────────────────────────────────────────── Глава IV — «Квантовые законы» (созвездие): фирменные способности Квантика. Всё выражено через БЕЗОПАСНУЮ модель спеки (params/зоны/plot), без новых механик движка и без eval. Суперпозиция — у уровня ДВА тела-копии Квантика (ball + ball2), один общий «закон» (params) рулит обеими; победа — когда ОБЕ копии у своих порталов (goal.when ссылается на ball.* и ball2.*). Реюз мульти-body физики. Коллапс/прицел — на сцене всегда есть пунктирная «предсказанная траектория» (plot выражения пути от текущего закона). Способность «Прицел» в игре ставит паузу — игрок целится по предсказанной кривой до запуска. Туннелирование — на пути стоит запретная стена-зона (tunnelable). Проигрыш только когда стена задета И заряд не потрачен: fail:'wall.hit && tunnel < 1'. Игровой слой тратит ЭНЕРГИЮ (заработанную в SR-комнате) и делает inst.setParam('tunnel', 1) → стена временно проницаема. tunnel — НЕ слайдер (управляется только способностью); по умолчанию отсутствует в env → 0 (неизвестная переменная SimExpr = 0) → стена сплошная. ──────────────────────────────────────────────────────────────────────── */ var PHANTOM = '#C4B5FD'; // полупрозрачный «фантом» — вторая копия (ball2) var AIM = '#38BDF8'; // предсказанная траектория (пунктир) var TUNNEL_WALL = '#F472B6'; // tunnelable-стена (магента — «квантовый барьер») /* helper: вторая копия-герой (ball2) — стартует из (sx,sy), полупрозрачный фантом. */ function hero2(sx, sy, vxExpr, vyExpr) { return { id: 'ball2', type: 'point', r: 7, color: PHANTOM, opacity: 0.78, x: sx, y: sy, glow: true, glowColor: PHANTOM, trail: true, trailColor: PHANTOM, trailFade: true, body: { mass: 1, vx: vxExpr, vy: vyExpr } }; } /* helper: портал-кольцо «фантомного» цвета (цель второй копии). */ function portal2Objs(px, py, r) { return [ { type: 'circle', x: px, y: py, r: r, color: PHANTOM, width: 3, glow: true, glowColor: PHANTOM }, { type: 'circle', x: px, y: py, r: r * 0.45, color: PHANTOM, width: 2, opacity: 0.7 }, { type: 'label', x: px, y: py + r + 0.9, text: 'портал-2', color: PHANTOM, size: 12 } ]; } /* helper: пунктирная предсказанная траектория (plot) — для «прицела». */ function aimPath(exprStr, a, b) { return { id: 'aim', type: 'plot', expr: exprStr, var: 'x', range: [a, b], samples: 120, color: AIM, width: 1.6, lineStyle: 'dashed', opacity: 0.6 }; } /* Уровень 12 (обучение суперпозиции): «Раздвоение» — две копии Квантика летят симметрично (вправо и влево) под ОДНИМ законом. Один (θ,v) обязан попасть в ОБА зеркальных портала. Симметрия гарантирует решение. */ var L12_PX = 6.5, L12_PY = 0.7, L12_PR = 0.95; // правый портал (для ball) var L12_PX2 = -6.5; // левый портал (для ball2, зеркало) var superpos12 = { id: 'quantum-superpos-12', title: 'Раздвоение', chapter: 'quantum', order: 12, unlockStars: 19, par_ms: 1700, subject: 'physics', hint: 'Квантик в суперпозиции — он сразу в двух местах! Обе копии летят под ОДНИМ законом, зеркально. Подбери угол и скорость так, чтобы каждая копия попала в свой портал.', spec: { specVersion: 1, meta: { title: 'Суперпозиция: раздвоение', desc: 'Два тела, один закон. Симметричный бросок.' }, viewport: { xmin: -9, xmax: 9, ymin: -1.2, ymax: 7, grid: true, axes: true, bg: BG }, params: [ { name: 'theta', label: 'Угол', min: 20, max: 80, step: 1, value: 50, unit: '°' }, { name: 'v', label: 'Скорость', min: 5, max: 18, step: 0.5, value: 9, unit: 'м/с' } ], physics: { enabled: true, gravity: { x: 0, y: -9.8 } }, objects: [ { type: 'segment', x1: -9, y1: 0, x2: 9, y2: 0, color: GROUND, width: 2 } ].concat(portalObjs(L12_PX, L12_PY, L12_PR), portal2Objs(L12_PX2, L12_PY, L12_PR), [ // ball летит вправо, ball2 — зеркально влево (тот же закон) hero(0, 0, 'v*cos(theta*pi/180)', 'v*sin(theta*pi/180)'), hero2(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 - ' + L12_PX + ', ball.y - ' + L12_PY + ') < ' + L12_PR + ' && hypot(ball2.x - (' + L12_PX2 + '), ball2.y - ' + L12_PY + ') < ' + L12_PR, fail: 't > 6', stars: [ { when: 't*1000 <= 1700', label: 'Быстро (≤1.7 с)' }, { when: 'theta >= 55', label: 'Высокая дуга (θ ≥ 55°)' } ] } } }; /* Уровень 13 (применение суперпозиции): «Две двери» — копии стартуют из РАЗНЫХ точек, но скорости связаны одним законом (ball вправо-вверх, ball2 влево-вверх одним законом). Обе зеркальные дуги должны перелететь центральную стену и одновременно войти в свои порталы на вершинах дуг — нужен точный общий закон. */ var L13_PX = 5.0, L13_PY = 3.2, L13_PR = 0.85; // правый портал (вершина дуги ball) var L13_PX2 = -5.0; // левый портал (зеркало, ball2) var L13_WALLH = 2.0; // центральная стена — дуги обязаны её перелететь var superpos13 = { id: 'quantum-superpos-13', title: 'Две двери', chapter: 'quantum', order: 13, unlockStars: 20, par_ms: 2000, subject: 'physics', hint: 'Две копии летят зеркально из центра — вправо и влево, под одним законом. Перебрось обе дуги через центральную стену так, чтобы вершина каждой дуги попала в свой портал.', spec: { specVersion: 1, meta: { title: 'Суперпозиция: две двери', desc: 'Один закон ведёт обе зеркальные копии в свои двери.' }, viewport: { xmin: -7.5, xmax: 7.5, ymin: -1.2, ymax: 6, grid: true, axes: true, bg: BG }, params: [ { name: 'theta', label: 'Угол', min: 25, max: 80, step: 1, value: 50, unit: '°' }, { name: 'v', label: 'Скорость', min: 6, max: 16, step: 0.5, value: 10, unit: 'м/с' } ], physics: { enabled: true, gravity: { x: 0, y: -9.8 } }, objects: [ { type: 'segment', x1: -7.5, y1: 0, x2: 7.5, y2: 0, color: GROUND, width: 2 }, // центральная стена-препятствие (только визуал + ориентир; дуги должны быть выше) { type: 'segment', x1: 0, y1: 0, x2: 0, y2: L13_WALLH, color: '#475569', width: 6 }, { type: 'label', x: 0, y: L13_WALLH + 0.5, text: 'стена', color: '#94A3B8', size: 11 } ].concat(portalObjs(L13_PX, L13_PY, L13_PR), portal2Objs(L13_PX2, L13_PY, L13_PR), [ // обе копии стартуют из центра (0, 0.2): ball вправо, ball2 зеркально влево hero(0, 0.2, 'v*cos(theta*pi/180)', 'v*sin(theta*pi/180)'), hero2(0, 0.2, '-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 - ' + L13_PX + ', ball.y - ' + L13_PY + ') < ' + L13_PR + ' && hypot(ball2.x - (' + L13_PX2 + '), ball2.y - ' + L13_PY + ') < ' + L13_PR, fail: 't > 6', stars: [ { when: 't*1000 <= 2000', label: 'Быстро (≤2.0 с)' }, { when: 'v <= 11', label: 'Экономный бросок (v ≤ 11)' } ] } } }; /* Уровень 14 (обучение прицела/коллапса): «Прицел» — бросок под углом к порталу, на сцене всегда видна предсказанная траектория (пунктир). Способность «Прицел» (в игре) ставит паузу: целься по кривой до запуска. */ var L14_PX = 8.6, L14_PY = 4.4, L14_PR = 0.8; var L14_CX = 5.0, L14_CY = 5.2, L14_CR = 0.7; // предсказанная траектория: парабола броска y = tan(θ)·x − g·x² / (2·v²·cos²θ) var L14_AIM = 'tan(theta*pi/180)*x - 9.8*x^2 / (2*v^2*cos(theta*pi/180)^2)'; var aimer14 = { id: 'quantum-aim-14', title: 'Прицел', chapter: 'quantum', order: 14, unlockStars: 22, par_ms: 1600, subject: 'physics', hint: 'Пунктирная линия — предсказанная траектория Квантика для текущего закона. Способность «Прицел» ставит паузу: целься по кривой, затем коллапсируй (запусти) в портал.', spec: { specVersion: 1, meta: { title: 'Коллапс: прицел', desc: 'Предсказанная траектория параболы броска.' }, viewport: { xmin: -1, xmax: 11, ymin: -1.2, 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: 6, max: 18, step: 0.5, value: 11, unit: 'м/с' } ], physics: { enabled: true, gravity: { x: 0, y: -9.8 } }, objects: [ { type: 'segment', x1: -1, y1: 0, x2: 11, y2: 0, color: GROUND, width: 2 }, aimPath(L14_AIM, 0, 10.5) ].concat(crystalObjs(L14_CX, L14_CY, L14_CR), portalObjs(L14_PX, L14_PY, L14_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 - ' + L14_PX + ', ball.y - ' + L14_PY + ') < ' + L14_PR, fail: 'ball.x > 10.6 || ball.y < -1.0', stars: [ { when: 'hypot(ball.x - ' + L14_CX + ', ball.y - ' + L14_CY + ') < ' + L14_CR, label: 'Собрал кристалл' }, { when: 't*1000 <= 1600', label: 'Быстро (≤1.6 с)' } ] } } }; /* Уровень 15 (обучение туннелирования): «Квантовый барьер» — Квантик едет по прямой к порталу, но на пути СТЕНА (tunnelable-зона). Без заряда стена сплошная (задел → проигрыш). Способность «Туннель» (за энергию) делает стену проницаемой: fail:'wall.hit && tunnel < 1'. */ var tunnel15 = { id: 'quantum-tunnel-15', title: 'Квантовый барьер', chapter: 'quantum', order: 15, unlockStars: 24, par_ms: 5200, subject: 'physics', hint: 'Стена-барьер преграждает путь напрямую, обойти её нельзя. Накопи энергию в комнате повторения и потрать заряд туннелирования — Квантик пройдёт СКВОЗЬ барьер.', spec: { specVersion: 1, meta: { title: 'Туннелирование: барьер', desc: 'Прохождение сквозь барьер за квантовый заряд.' }, viewport: { xmin: -1, xmax: 11, ymin: -1, ymax: 8, grid: true, axes: true, bg: BG }, time: { duration: 5, loop: false }, params: [ { name: 'a', label: 'Наклон a', min: -0.3, max: 0.5, step: 0.02, value: 0.1 }, { name: 'b', label: 'Высота b', min: 2.5, max: 4.5, step: 0.1, value: 3.5 } ], objects: [ // барьер ровно поперёк пути (вертикальный брус в центре, перекрывает весь коридор по y) { type: 'zone', id: 'wall', kind: 'forbidden', shape: 'rect', x: 5, y: 3.5, w: 0.8, h: 7.2, color: TUNNEL_WALL, label: 'барьер' }, // целевой портал справа circZone('gate', 'target', 9.5, 3.5, 0.95, 'портал'), // бонус-монета сразу за барьером circZone('coin', 'collect', 6.5, 3.5, 0.6, 'монета'), startMarker(0, 3.5), road('a*x + b', 0, 9.5, 5), graphHero(), { type: 'readout', label: 'a', expr: 'a', precision: 2 }, { type: 'readout', label: 'b', expr: 'b', precision: 1 } ], goal: { title: 'Пройди сквозь барьер', hint: 'Доберись до портала. Барьер задержит Квантика, пока не потрачен заряд туннелирования. Бонус: собери монету за барьером.', when: 'gate.hit', // проигрыш только если задел стену БЕЗ заряда туннеля (tunnel<1 ⇒ заряд не потрачен) fail: 'wall.hit && tunnel < 1', stars: [ { when: 'coin.hit', label: 'Собрал монету' }, { when: 'gate.hit && abs(b - 3.5) < 0.4', label: 'Ровный путь (b ≈ 3.5)' } ] } } }; /* Уровень 16 (капстоун главы): «Сквозь стену в дверь» — комбо: бегунок-парабола, барьер-зона поперёк дуги (нужен туннель) и целевой портал за ним; монета на дуге. */ var tunnel16 = { id: 'quantum-tunnel-16', title: 'Сквозь стену', chapter: 'quantum', order: 16, unlockStars: 26, par_ms: 6000, subject: 'physics', hint: 'Дуга-парабола ведёт к высокому порталу, но её пересекает квантовый барьер. Туннелируй сквозь него (за энергию) и подгони дугу так, чтобы попасть в дверь.', spec: { specVersion: 1, meta: { title: 'Туннель сквозь стену', desc: 'Парабола + барьер: туннель в портал.' }, viewport: { xmin: -1, xmax: 11, ymin: -1, ymax: 9, grid: true, axes: true, bg: BG }, time: { duration: 6, loop: false }, params: [ { name: 'a', label: 'Раскрытие a', min: -0.5, max: -0.15, step: 0.02, value: -0.3 }, { name: 'k', label: 'Высота вершины k', min: 5, max: 8, step: 0.1, value: 6.5 } ], objects: [ // барьер поперёк восходящей дуги (вертикальный брус) { type: 'zone', id: 'wall', kind: 'forbidden', shape: 'rect', x: 3.2, y: 4.0, w: 0.7, h: 8.4, color: TUNNEL_WALL, label: 'барьер' }, // монета у самой вершины высокой дуги (k≈7) — совместима со 2-й звездой k≥6.8 circZone('coin', 'collect', 5, 6.9, 0.85, 'монета'), circZone('gate', 'target', 9.3, 4.6, 0.95, 'портал'), startMarker(0, 0), road('a*(x-5)^2 + k', 0, 9.3, 6), graphHero(), { type: 'readout', label: 'a', expr: 'a', precision: 2 }, { type: 'readout', label: 'k', expr: 'k', precision: 1 } ], goal: { title: 'Туннелем в дальний портал', hint: 'Туннелируй сквозь барьер и проведи дугу в портал. Бонус: задень монету на вершине.', when: 'gate.hit', fail: 'wall.hit && tunnel < 1', stars: [ { when: 'coin.hit', label: 'Собрал монету' }, { when: 'gate.hit && k >= 6.8', label: 'Высокая дуга (k ≥ 6.8)' } ] } } }; var LEVELS = [ artillery1, arc2, bounce3, pendulum4, orbit5, slingshot6, graphLine7, graphSine8, graphParab9, graphAbs10, graphExp11, superpos12, superpos13, aimer14, tunnel15, tunnel16 ]; /* Метаданные глав (созвездий) — для заголовков/оформления карты. */ var CHAPTERS = { kinematics: { key: 'kinematics', title: 'Кинематика', subtitle: 'Полёт и гравитация', accent: '#22D3EE' }, dynamics: { key: 'dynamics', title: 'Динамика', subtitle: 'Силы, пружины, орбиты', accent: '#A78BFA' }, functions: { key: 'functions', title: 'Функции', subtitle: 'Едем по кривой y = f(x)', accent: '#67E8F9' }, quantum: { key: 'quantum', title: 'Квантовые законы', subtitle: 'Суперпозиция · прицел · туннель', accent: '#C4B5FD' }, // Авторённые учителями уровни (custom_sims cat='game') без явной главы — сюда. custom: { key: 'custom', title: 'Уровни учителей', subtitle: 'Авторённые уровни сообщества', accent: '#F472B6' } }; /* ── Авторённые уровни (Фаза 5): custom_sims с cat='game' ─────────────────── Реестр становится «асинхронным»: встроенные уровни доступны сразу (offline), а опубликованные/свои игровые спеки подмешиваются после ensureCustom(). Запись уровня из строки custom_sims: id='custom:', метаданные — из spec.game (chapter/order/par_ms/unlockStars), spec — как есть. */ var CUSTOM = []; // смёрженные записи авторённых уровней var _customPromise = null; // кэш промиса загрузки (грузим один раз) /* Строка из LS.customSimsList/Get -> запись реестра уровня (или null). */ function customToLevel(row) { if (!row || !row.spec || typeof row.spec !== 'object') return null; var spec = row.spec; if (!spec.goal) return null; // не игровой уровень — пропускаем var gm = (spec.game && typeof spec.game === 'object') ? spec.game : {}; var dbid = row.id; return { id: 'custom:' + dbid, dbid: dbid, title: row.title || (spec.meta && spec.meta.title) || 'Уровень', chapter: gm.chapter || 'custom', order: (typeof gm.order === 'number') ? gm.order : (1000 + Number(dbid)), unlockStars: (typeof gm.unlockStars === 'number') ? gm.unlockStars : 0, par_ms: (typeof gm.par_ms === 'number') ? gm.par_ms : undefined, subject: row.subject || (spec.goal && spec.goal.subject) || undefined, hint: (spec.goal && spec.goal.hint) || '', spec: spec, _custom: true }; } /* Загрузить опубликованные + свои игровые custom_sims и смёржить. Возвращает Promise (кэшируется). Тихо игнорирует ошибки/отсутствие LS. */ function ensureCustom() { if (_customPromise) return _customPromise; var LS = global.LS; if (!LS || !LS.customSimsList || !LS.customSimGet) { _customPromise = Promise.resolve([]); return _customPromise; } _customPromise = LS.customSimsList().then(function (r) { var sims = (r && r.sims) || []; // только игровая категория (список не содержит spec — его берём отдельно) var games = sims.filter(function (s) { return s && s.cat === 'game'; }); return Promise.all(games.map(function (s) { return LS.customSimGet(s.id).then(function (g) { return customToLevel(g && g.sim); }).catch(function () { return null; }); })); }).then(function (records) { CUSTOM = records.filter(Boolean); return CUSTOM.slice(); }).catch(function () { CUSTOM = []; return []; }); return _customPromise; } function list() { return LEVELS.concat(CUSTOM); } function get(id) { var all = list(); for (var i = 0; i < all.length; i++) if (all[i].id === id) return all[i]; return null; } /* Достать уровень по id асинхронно — для deep-link `custom:`, когда он может ещё не быть в смёрженном списке (напр. свой draft). Резолвит через LS.customSimGet с проверкой доступа на сервере (own|published|admin). */ function getAsync(id) { var found = get(id); if (found) return Promise.resolve(found); var m = /^custom:(\d+)$/.exec(String(id || '')); var LS = global.LS; if (!m || !LS || !LS.customSimGet) return Promise.resolve(null); return LS.customSimGet(m[1]).then(function (g) { var lvl = customToLevel(g && g.sim); if (lvl) { // подмешать в кэш, чтобы повторное открытие/«Дальше» нашло его синхронно if (!CUSTOM.some(function (c) { return c.id === lvl.id; })) CUSTOM.push(lvl); } return lvl; }).catch(function () { return null; }); } function chapter(key) { return CHAPTERS[key] || { key: key, title: key, subtitle: '', accent: '#22D3EE' }; } global.QuantikLevels = { list: list, get: get, getAsync: getAsync, ensureCustom: ensureCustom, chapter: chapter, LEVELS: LEVELS, CHAPTERS: CHAPTERS, customToLevel: customToLevel }; })(typeof window !== 'undefined' ? window : this);