@
feat(quantik-game): фаза 4 — квантовые способности + SR-комнаты Глава-созвездие quantum (L12–L16) и фирменные механики — всё через безопасную модель спеки, движок и бэкенд НЕ тронуты (engine touch = 0): - Суперпозиция: два тела ball+ball2, goal.when требует ОБА (зеркальный закон). Туннелирование: forbidden-зона wall + fail wall.hit && tunnel<1; способность тратит энергию → setParam(tunnel,1). Коллапс/прицел: пунктир- plot предсказанной траектории на паузе. - Энергия — клиентский ресурс (localStorage quantik-energy, QuantikEnergy). - SR-комната: мини-сессия повторения флешкарт в модалке (НЕ iframe), LS.fcStudySession/fcReview; «Знаю/Легко» дают энергию; текст карт экранируется, картинки — по regex-вайтлисту. Все 5 уровней проверены на реальном движке (2★ достижимы; суперпозиция требует оба тела; туннель-гейт блокирует без заряда). npm test 253/8 baseline; lint:routes 0; цепочка разблокировки проходима. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
+293
-2
@@ -661,16 +661,307 @@
|
||||
}
|
||||
};
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
Глава 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
|
||||
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' }
|
||||
functions: { key: 'functions', title: 'Функции', subtitle: 'Едем по кривой y = f(x)', accent: '#67E8F9' },
|
||||
quantum: { key: 'quantum', title: 'Квантовые законы', subtitle: 'Суперпозиция · прицел · туннель', accent: '#C4B5FD' }
|
||||
};
|
||||
|
||||
function list() { return LEVELS.slice(); }
|
||||
|
||||
Reference in New Issue
Block a user