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> @
9.4 KiB
9.4 KiB
Phase 1: Оболочка игры + 1 физ-уровень + прогресс (MVP)
Status: ✅ Done (reviewed — PASS, committed) Parent plan: PLAN.md Domain: fullstack
Objective
Сквозной играбельный срез: страница /quantik грузит уровень-спеку, монтирует движок в
«игровом режиме» (управление = слайдеры закона + кнопка «Запуск»), на победу шлёт результат
на сервер, показывает экран успеха со звёздами/временем. Прогресс сохраняется в БД.
Первый уровень — «Артиллерия Квантика»: угол+скорость, попасть в портал, собрать звезду.
Tasks
- Task 1: Миграция
076_game_progress.sqlgame_progress:id, user_id, level_id TEXT, best_time_ms INTEGER, best_stars INTEGER, attempts INTEGER, completed_at. UNIQUE(user_id, level_id). - 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; валидация входа. - Task 3: Клиент
LS.gameProgressList()/LS.gameProgressSubmit(levelId, {time_ms, stars})в js/api.js. - 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). - Task 5:
frontend/quantik.html+frontend/js/game/quantik-game.js: доступ всем авторизованным (LS.initPage()); подключает_sim_expr.js+_sim_engine.jsтем же путём, что lab/sim-builder. Монтирует уровень,onGoal→ submit + экран успеха. - Task 6: «Игровой режим» — HUD из Ф0 включается сам наличием
goal; управление = слайдеры params движка + кнопки play/reset (встроены вinst.el). Редакторских панелей нет. - Task 7: Экран успеха (DOM-оверлей страницы): звёзды (inline SVG), время, попытки, «Ещё раз» (inst.reset) / «Дальше» (disabled-заглушка для MVP). Без эмодзи.
- Task 8: Пункт сайдбара
js/sidebar.js—/quantikв группе practice (iconrocket), видим всем.isActive('/quantik')подсветка работает на clean URL. - 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:routesbaseline 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
- Все задачи; конвенции (ownership/auth как studentMaterials/customSim); без эмодзи/eval
- Миграция применяется; 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_simscat='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 оверлея — в
<style>quantik.html(.qg-*). Ф2 переиспользуетbuildSuccessOverlay(можно расширить параметром «следующий уровень»).
Гочи для Ф2
inst.onGoalсрабатывает 1 раз и делаетpause(). Перезапуск —inst.reset()(это И физика, И attempts++). Не зватьplay()в onGoal-колбэке.res.timeMs— мировое время (детерминизм), не wallclock.res.stars.got/res.stars.total— счётчики звёзд.- Страница не разрушает
instявно при навигации; Ф2 при смене уровня без перезагрузки должна вызватьinst.destroy()перед монтированием нового (или перезагружать?level=). - Сайдбар-пункт
/quantikвидим ВСЕМ (безhidden), в отличие от teacher-only/sim-builder.