351251d652
feat(quantik-game): фаза 1 — оболочка игры + физ-уровень + прогресс (MVP) Страница /quantik монтирует уровень-спеку в SimEngine (игровой режим: HUD из Ф0 + слайдеры закона + play/reset), на победу шлёт результат и показывает экран успеха (звёзды/время/попытки, inline SVG). Уровень phys-artillery-1 как данные (levels.js): гравитация + запуск тела из угла/скорости, портал, бонус-звезда. Бэкенд: миграция 076 game_progress (UNIQUE user+level), /api/game/progress (GET свой / POST upsert best time/stars, attempts++, auth-only, валидация входа), клиент LS.gameProgress*, пункт сайдбара. game.test.js 13/13; npm test 251 pass/8 baseline; lint:routes 0. Уровень проверен на реальном интеграторе (311 выигрышных комбо, 31 на 3★). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
108 lines
6.4 KiB
JavaScript
108 lines
6.4 KiB
JavaScript
'use strict';
|
||
/* ════════════════════════════════════════════════════════════════════════
|
||
Квантик — Законы Мира · Реестр уровней (Фаза 1, MVP).
|
||
|
||
Уровень = СПЕКА SimForge (данные, не код) + блок `goal` (победа), который
|
||
движок (_sim_engine.js) умеет с Фазы 0. Игрок не управляет героем напрямую —
|
||
он «чинит закон мира»: крутит слайдеры params (угол/скорость), затем «Запуск»,
|
||
и симуляция проигрывается к цели.
|
||
|
||
ИСТОЧНИК УРОВНЕЙ (решение зафиксировано в CONTEXT.md):
|
||
— СЕЙЧАС (Фаза 1): встроенные данные здесь, window.QuantikLevels.
|
||
— ПОЗЖЕ (Фаза 5): уровни авторятся в sim-builder и хранятся в custom_sims
|
||
(cat='game'); реестр пополнится загрузкой опубликованных спек с сервера.
|
||
|
||
Форма записи уровня:
|
||
{ id, title, subject?, hint?, spec }
|
||
где spec — обычная спека SimForge с блоком goal. id == level_id для
|
||
/api/game/progress (LS.gameProgressSubmit(id, ...)).
|
||
|
||
⛔ Без 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 artillery1 = {
|
||
id: 'phys-artillery-1',
|
||
title: 'Артиллерия Квантика',
|
||
subject: 'physics',
|
||
hint: 'Подберите угол и скорость, чтобы Квантик долетел до портала. Соберите кристалл по дороге — это бонусная звезда.',
|
||
spec: {
|
||
specVersion: 1,
|
||
meta: { title: 'Артиллерия Квантика', desc: 'Закон движения: бросок под углом к горизонту.' },
|
||
viewport: { xmin: -1, xmax: 12, ymin: -1.2, ymax: 7, grid: true, axes: true, bg: '#0D0D1A' },
|
||
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: [
|
||
// «Земля» — линия 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: '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,
|
||
// Мягкий проигрыш: улетел далеко за поле (промах) — можно перезапустить.
|
||
fail: 'ball.x > 11.5 || ball.y < -1.0',
|
||
stars: [
|
||
{ when: 'hypot(ball.x - ' + STAR_X + ', ball.y - ' + STAR_Y + ') < ' + STAR_R, label: 'Собрал кристалл' }
|
||
]
|
||
}
|
||
}
|
||
};
|
||
|
||
var LEVELS = [artillery1];
|
||
|
||
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;
|
||
}
|
||
|
||
global.QuantikLevels = { list: list, get: get, LEVELS: LEVELS };
|
||
|
||
})(typeof window !== 'undefined' ? window : this);
|