# Phase 1: Оболочка игры + 1 физ-уровень + прогресс (MVP) **Status:** ✅ Done (reviewed — PASS, committed) **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** fullstack ## Objective Сквозной играбельный срез: страница `/quantik` грузит уровень-спеку, монтирует движок в «игровом режиме» (управление = слайдеры закона + кнопка «Запуск»), на победу шлёт результат на сервер, показывает экран успеха со звёздами/временем. Прогресс сохраняется в БД. Первый уровень — «Артиллерия Квантика»: угол+скорость, попасть в портал, собрать звезду. ## Tasks - [x] Task 1: Миграция `076_game_progress.sql` `game_progress`: `id, user_id, level_id TEXT, best_time_ms INTEGER, best_stars INTEGER, attempts INTEGER, completed_at`. UNIQUE(user_id, level_id). - [x] Task 2: Контроллер `gameController.js` + роутер `game.js`, смонтирован в `server.js` (после `/api/custom-sims`). `GET /api/game/progress` (свой прогресс), `POST /api/game/progress` `{level_id, time_ms, stars}` (upsert: min time / max stars; attempts++). auth-only; валидация входа. - [x] Task 3: Клиент `LS.gameProgressList()` / `LS.gameProgressSubmit(levelId, {time_ms, stars})` в js/api.js. - [x] Task 4: Уровень как ДАННЫЕ: `frontend/js/game/levels.js` (`window.QuantikLevels`), встроенная спека `phys-artillery-1` (physics gravity + body launch + goal + 1 star + portal). Источник уровней зафиксирован в CONTEXT.md (встроенные данные сейчас; custom_sims в Ф5). - [x] Task 5: `frontend/quantik.html` + `frontend/js/game/quantik-game.js`: доступ всем авторизованным (LS.initPage()); подключает `_sim_expr.js`+`_sim_engine.js` тем же путём, что lab/sim-builder. Монтирует уровень, `onGoal` → submit + экран успеха. - [x] Task 6: «Игровой режим» — HUD из Ф0 включается сам наличием `goal`; управление = слайдеры params движка + кнопки play/reset (встроены в `inst.el`). Редакторских панелей нет. - [x] Task 7: Экран успеха (DOM-оверлей страницы): звёзды (inline SVG), время, попытки, «Ещё раз» (inst.reset) / «Дальше» (disabled-заглушка для MVP). Без эмодзи. - [x] Task 8: Пункт сайдбара `js/sidebar.js` — `/quantik` в группе practice (icon `rocket`), видим всем. `isActive('/quantik')` подсветка работает на clean URL. - [x] Task 9: Тест `backend/tests/game.test.js` (паттерн lab-links.test.js): submit создаёт строку, лучший перезаписывает / худший нет, attempts++, per-user, требует auth (401), валидирует вход (400). 13/13 PASS. ## Files to Modify/Create - `backend/src/db/migrations/0NN_game_progress.sql` — таблица прогресса. - `backend/src/controllers/gameController.js`, `backend/src/routes/game.js` — API. - `backend/src/server.js` — монтаж роутера. - `frontend/quantik.html`, `frontend/js/game/quantik-game.js`, `frontend/js/game/levels.js` — клиент+уровень. - `frontend/js/api.js` — `LS.gameProgress*`. - `frontend/js/sidebar.js` — пункт меню. - `backend/tests/game.test.js` — тест. ## Acceptance Criteria - `/quantik` грузится, монтирует уровень, цель видна; «Запуск» проигрывает физику. - Попадание в портал (+звезда) → экран успеха с временем/звёздами; результат записан в `game_progress`. - Повторный худший результат не перезаписывает лучший; attempts растёт. - `npm run migrate` применяет миграцию; `npm test` зелёный (+ новый тест); `lint:routes` baseline 0. ## Notes - Маршрутизация `/js/game/*`: помнить гочу sim-builder — `/js` мапится на корневой `js/`, а файлы лежат во `frontend/js/game/` → отдаются через `express.static(frontendDir)`. Не трогать server.js static. - Роуты `:id` прикрыть `authMiddleware` на уровне роутера (lint:routes baseline 0). - Время — из `getResult().timeMs` (Ф0). ## Review Checklist - [x] Все задачи; конвенции (ownership/auth как studentMaterials/customSim); без эмодзи/eval - [x] Миграция применяется; API безопасен; тест зелёный; lint baseline 0; existing тесты не сломаны ## Handoff to Next Phase ### Реестр уровней (форма данных) `frontend/js/game/levels.js` → `window.QuantikLevels`: - `QuantikLevels.list()` → массив записей уровней (копия); `QuantikLevels.get(id)` → одна запись или null; `QuantikLevels.LEVELS` — сырой массив. - **Запись уровня**: `{ id, title, subject?, hint?, spec }`. `id` == `level_id` для API прогресса. `spec` — обычная спека SimForge с верхнеуровневым блоком `goal` (Ф0). Сейчас один уровень `phys-artillery-1`. - **Добавить уровень** = добавить запись в `LEVELS` (или, в Ф5, подгрузить опубликованные `custom_sims` cat='game' и смержить в реестр — той же формы записи). Источник = данные, не код. - Уровень мог бы прийти и из `custom_sims` (cat='game'): `spec` уже валидируется сервером (validateSpec пропускает goal/game). Реестр в Ф2/Ф5 может стать асинхронным (загрузка + слияние со встроенными). ### Контракт API прогресса - `GET /api/game/progress` (auth) → `{ progress: [ { level_id, best_time_ms, best_stars, attempts, completed_at } ] }` — все уровни текущего игрока. - `POST /api/game/progress` (auth) body `{ level_id, time_ms, stars }` → `{ ok:true, progress:{...одна строка...} }`. Upsert: best_time_ms=min, best_stars=max, attempts++. Валидация: level_id строка ≤120; time_ms/stars неотрицательные целые; stars 0..3 (иначе 400). - Клиент: `LS.gameProgressList()`, `LS.gameProgressSubmit(levelId, { time_ms, stars })`. - Таблица `game_progress` — миграция **076**, UNIQUE(user_id, level_id), user_id ON DELETE CASCADE. - На Ф6 (лидерборд) — этой таблицы достаточно для «лучшее время по уровню»; агрегаты по классу — JOIN на class_members. ### Где живёт экран успеха / как монтируется уровень - Монтаж: `QuantikGame.start({ host, level })` → `SimEngine.mount(host, level.spec)` → возвращает `inst`. «Игровой режим» включается САМ (HUD появляется, т.к. в спеке есть `goal`). Управление — слайдеры params + play/reset движка (внутри `inst.el`). - Победа: `inst.onGoal(res => …)` (Ф0; срабатывает 1 раз). В колбэке: `LS.gameProgressSubmit(level.id, { time_ms: res.timeMs, stars: res.stars.got })` (best-effort, .catch офлайн) + экран успеха. - **Экран успеха** = DOM-оверлей `.qg-overlay`, добавляется в `host` (=`#qg-stage`), `QuantikGame.buildSuccessOverlay(state)` строит карточку (звёзды inline SVG, время/звёзды/попытки, кнопки). «Ещё раз» → убрать оверлей + `inst.reset()`. «Дальше» — disabled-заглушка (нет следующего уровня в MVP); Ф2 (карта/мир) активирует её переходом к следующему узлу. - CSS оверлея — в `