@
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> @
This commit is contained in:
+357
-63
@@ -1,107 +1,401 @@
|
||||
'use strict';
|
||||
/* ════════════════════════════════════════════════════════════════════════
|
||||
Квантик — Законы Мира · Реестр уровней (Фаза 1, MVP).
|
||||
Квантик — Законы Мира · Реестр уровней (Фаза 2 — мир из 6 физ-уровней).
|
||||
|
||||
Уровень = СПЕКА SimForge (данные, не код) + блок `goal` (победа), который
|
||||
движок (_sim_engine.js) умеет с Фазы 0. Игрок не управляет героем напрямую —
|
||||
он «чинит закон мира»: крутит слайдеры params (угол/скорость), затем «Запуск»,
|
||||
и симуляция проигрывается к цели.
|
||||
он «чинит закон мира»: крутит слайдеры params (угол/скорость/жёсткость…),
|
||||
затем «Запуск», и симуляция проигрывается к цели.
|
||||
|
||||
ИСТОЧНИК УРОВНЕЙ (решение зафиксировано в CONTEXT.md):
|
||||
— СЕЙЧАС (Фаза 1): встроенные данные здесь, window.QuantikLevels.
|
||||
— ПОЗЖЕ (Фаза 5): уровни авторятся в sim-builder и хранятся в custom_sims
|
||||
(cat='game'); реестр пополнится загрузкой опубликованных спек с сервера.
|
||||
── МЕТАДАННЫЕ УРОВНЯ (Фаза 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 для неё не нужен).
|
||||
|
||||
Форма записи уровня:
|
||||
{ id, title, subject?, hint?, spec }
|
||||
где spec — обычная спека SimForge с блоком goal. id == level_id для
|
||||
/api/game/progress (LS.gameProgressSubmit(id, ...)).
|
||||
ИСТОЧНИК УРОВНЕЙ: встроенные данные здесь (window.QuantikLevels). Авторённые
|
||||
уровни (custom_sims cat='game') подмешаются в Фазе 5 (реестр станет асинхронным).
|
||||
|
||||
⛔ Без eval/Function. Все «числовые» поля могут быть числом ИЛИ строкой-
|
||||
выражением (их безопасно вычисляет SimExpr на клиенте).
|
||||
════════════════════════════════════════════════════════════════════════ */
|
||||
(function (global) {
|
||||
|
||||
/* ── Уровень 1: «Артиллерия Квантика» ──────────────────────────────────
|
||||
Герой — светящаяся точка-тело (body) с кометной трассой (P2). Запускается
|
||||
из начала координат под углом θ со скоростью v; гравитация тянет вниз.
|
||||
Цель — попасть в портал; бонус-звезда — собрать кристалл по дороге.
|
||||
Параметры подобраны так, чтобы уровень был ПРОХОДИМ в пределах слайдеров. */
|
||||
var PORTAL_X = 8; // центр портала по X (мир)
|
||||
var PORTAL_Y = 0; // центр портала по Y (на «земле» y=0)
|
||||
var PORTAL_R = 0.7; // радиус попадания
|
||||
var STAR_X = 4; // бонус-кристалл (на восходящей ветви хорошей дуги)
|
||||
var STAR_Y = 2.6;
|
||||
var STAR_R = 0.65;
|
||||
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: 'Артиллерия Квантика',
|
||||
title: 'Артиллерия',
|
||||
chapter: 'kinematics',
|
||||
order: 1,
|
||||
unlockStars: 0,
|
||||
par_ms: 1500,
|
||||
subject: 'physics',
|
||||
hint: 'Подберите угол и скорость, чтобы Квантик долетел до портала. Соберите кристалл по дороге — это бонусная звезда.',
|
||||
hint: 'Подбери угол и скорость, чтобы Квантик долетел до портала. Собери кристалл по дороге — это вторая звезда. Быстрый бросок даст третью.',
|
||||
spec: {
|
||||
specVersion: 1,
|
||||
meta: { title: 'Артиллерия Квантика', desc: 'Закон движения: бросок под углом к горизонту.' },
|
||||
viewport: { xmin: -1, xmax: 12, ymin: -1.2, ymax: 7, grid: true, axes: true, bg: '#0D0D1A' },
|
||||
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: 'м/с' }
|
||||
{ name: 'v', label: 'Скорость', min: 5, max: 20, step: 0.5, value: 10, unit: 'м/с' }
|
||||
],
|
||||
physics: {
|
||||
enabled: true,
|
||||
gravity: { x: 0, y: -9.8 }
|
||||
},
|
||||
physics: { enabled: true, gravity: { x: 0, y: -9.8 } },
|
||||
objects: [
|
||||
// «Земля» — линия y=0 для ориентира.
|
||||
{ type: 'segment', x1: -1, y1: 0, x2: 12, y2: 0, color: '#334155', width: 2 },
|
||||
|
||||
// Бонус-кристалл (звезда). Контурный кружок-маркер.
|
||||
{ type: 'circle', x: STAR_X, y: STAR_Y, r: STAR_R, color: '#FBBF24', width: 2, glow: true },
|
||||
{ type: 'label', x: STAR_X, y: STAR_Y + 0.7, text: 'кристалл', color: '#FBBF24', size: 12 },
|
||||
|
||||
// Портал — цель. Светящийся кружок.
|
||||
{ type: 'circle', x: PORTAL_X, y: PORTAL_Y + PORTAL_R, r: PORTAL_R, color: '#22D3EE', width: 3, glow: true, glowColor: '#22D3EE' },
|
||||
{ type: 'label', x: PORTAL_X, y: PORTAL_Y + 2.0, text: 'портал', color: '#22D3EE', size: 12 },
|
||||
|
||||
// Герой Квантик — физ-тело, стартует из (0,0) со скоростью (vx,vy).
|
||||
// glow + кометная трасса (P2).
|
||||
{
|
||||
id: 'ball', type: 'point', r: 7, color: '#06D6E0',
|
||||
x: 0, y: 0,
|
||||
glow: true, glowColor: '#06D6E0', trail: true, trailColor: '#06D6E0',
|
||||
body: {
|
||||
mass: 1,
|
||||
vx: 'v*cos(theta*pi/180)',
|
||||
vy: 'v*sin(theta*pi/180)'
|
||||
}
|
||||
},
|
||||
|
||||
// Живые показания скорости (бейдж-оверлей).
|
||||
{ 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 - ' + PORTAL_X + ', ball.y - ' + (PORTAL_Y + PORTAL_R) + ') < ' + PORTAL_R,
|
||||
// Мягкий проигрыш: улетел далеко за поле (промах) — можно перезапустить.
|
||||
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 - ' + STAR_X + ', ball.y - ' + STAR_Y + ') < ' + STAR_R, label: 'Собрал кристалл' }
|
||||
{ when: 'hypot(ball.x - ' + L1_CX + ', ball.y - ' + L1_CY + ') < ' + L1_CR, label: 'Собрал кристалл' },
|
||||
{ when: 't*1000 <= 1500', label: 'Быстро (≤1.5 с)' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var LEVELS = [artillery1];
|
||||
/* Уровень 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, LEVELS: LEVELS };
|
||||
global.QuantikLevels = {
|
||||
list: list, get: get, chapter: chapter,
|
||||
LEVELS: LEVELS, CHAPTERS: CHAPTERS
|
||||
};
|
||||
|
||||
})(typeof window !== 'undefined' ? window : this);
|
||||
|
||||
Reference in New Issue
Block a user