c780b6fd96
feat(quantik-game): фаза 5 — авторинг игровых уровней в sim-builder + раздача Учитель собирает игровой уровень без кода: новая (аддитивная, сворачиваемая) панель в sim-builder задаёт блок goal (when/title/hint/hold/fail) + до 3 звёзд + game-мету (chapter/order/par_ms); выражения проверяются inline через SimExpr.compile (без eval). buildSpec/loadFromSim — round-trip без потерь (goal/game пишутся только при включённом слое; обычная sim не меняется). Кнопка «Играть» монтирует черновик в SimEngine-модалке (HUD цели из Ф0). QuantikLevels стал async: подмешивает custom_sims cat=game (свои+ published) в реестр (custom:<dbid>), offline-safe, строки без goal отбрасываются; deep-link /quantik?level=custom:<id> с серверной проверкой доступа (own|published → иначе 403/404), мимо геймплейного гейта unlockStars. Раздача классу — реюз share Ф6 (game-aware ссылка + durable pushNotif). Правки sim-builder строго аддитивны (параллельная сессия). npm test 259/8 baseline; quantik-authoring 6/6; lint:routes 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
7.9 KiB
7.9 KiB
Phase 5: Авторинг уровней в sim-builder + раздача классу
Status: ✅ Done (reviewed — PASS, committed) Parent plan: PLAN.md Domain: fullstack
Objective
Дать учителю собирать игровые уровни без кода в существующем sim-builder: задать цель/звёзды/
подсказку/главу/норматив, сохранить как custom_sims с cat='game', опубликовать и раздать
классу. Игра начинает грузить уровни из БД (а не только встроенные).
⚠️ ПЕРЕД СТАРТОМ: свериться с base-веткой feature/sim-builder — чужой P4-WIP билдера должен
быть смержен. При необходимости влить base в feature/quantik-game и разрешить конфликты.
Tasks
- Task 1: Режим «Игровой уровень» в
sim-builder.js/.html: панель цели (goal.when,title,hint,hold,fail), список звёзд (add/del:when+label), глава/порядок/par_ms. Inline-проверка выражений черезSimExpr.compile().error(как остальные поля билдера). - Task 2:
buildSpec()материализует блокgoal/game;loadFromSim()раскладывает обратно (round-trip), как сделано с plot-range в Ф4 билдера. - Task 3: Кнопка «Играть» в билдере — открыть текущую спеку в игровом режиме (тест уровня автором).
- Task 4: Каталог уровней: игра грузит
custom_simsccat='game'(свои+published) — реюзLS.customSimsList/Get. Категорияgameв спискеCATS(customSimController) + фильтр. - Task 5: Раздача классу: реюз паттерна Ф6 sim-builder (авто-публикация +
pushNotifученикам, ссылка/quantik?level=custom:<id>); привязка к программе черезlab_sim_links(sim_id='custom:<id>'). - Task 6: Deep-link
/quantik?level=custom:<id>(паттерн Ф5/Ф7 sim-builder, доступ own|published|admin). - Task 7: Тесты: round-trip goal в билдере (headless как Ф4 sim-builder); доступ к чужому draft запрещён; published-уровень виден; раздача шлёт уведомление.
Files to Modify/Create
frontend/sim-builder.html,frontend/js/sim-builder.js— режим игрового уровня (аддитивно).backend/src/controllers/customSimController.js—CATS+= 'game'; (goal уже в validateSpec из Ф0).frontend/js/game/quantik-game.js— загрузка уровней из custom_sims + deep-link.- тест(ы).
Acceptance Criteria
- Учитель собирает уровень с целью/звёздами, тестирует «Играть», сохраняет/публикует.
- Игра грузит уровни из БД; deep-link открывает конкретный уровень с проверкой доступа.
- Раздача классу публикует + уведомляет; round-trip спеки без потерь; тесты зелёные; lint baseline 0.
Notes
- Билдер — зона, где мог идти параллельный P4-WIP; правки строго аддитивны, свериться с base.
- Санитизация goal-полей — уже на сервере (Ф0). Клиентская валидация зеркалит её (как в Ф4 билдера).
Review Checklist
- Все задачи; аддитивность билдера; ownership/доступ корректны; без эмодзи/eval; тесты зелёные
Handoff to Next Phase
Как авторённый уровень попадает в реестр игры
- Хранилище:
custom_simsсcat='game'. Спека = обычная SimForge-спека + блокgoal{when,title,hint,hold,fail,stars[]}+ блокgame{chapter,order,par_ms,unlockStars?}. window.QuantikLevelsстал «асинхронным»: встроенныеLEVELSдоступны сразу (offline), а опубликованные/свои игровые спеки подмешиваются черезQuantikLevels.ensureCustom()(Promise, кэш):LS.customSimsList()→ фильтрcat==='game'→LS.customSimGet(id)каждой →customToLevel(row)→ запись реестра.list()=LEVELS.concat(CUSTOM);get(id)ищет в обоих.- Форма записи авторённого уровня (
customToLevel):{ id:'custom:<dbid>', dbid, title, chapter:(game.chapter||'custom'), order:(game.order||1000+dbid), unlockStars:(game.unlockStars||0), par_ms, subject, hint:(goal.hint), spec, _custom:true }. Запись БЕЗgoalотбрасывается (не уровень). - Новая глава-созвездие
customвCHAPTERS(levels.js) — авторённые уровни без явной главы группируются в неё; map.js рисует автоматически (по метаданным, не тронут). Если автор задалgame.chapter='kinematics'и т.п. — уровень встанет в соответствующее созвездие.
Deep-link контракт
/quantik?level=custom:<dbid>→QuantikLevels.getAsync('custom:<dbid>'): если уже в кэше — синхронно; иначеLS.customSimGet(dbid)(сервер: доступ own|published|admin → иначе 404/403 → карта). Авторённый уровень по deep-link открывается БЕЗ гейтаunlockStars(получатель ссылки заходит прямо). Встроенный?level=<id>— как раньше (черезisUnlocked).- Прогресс игрока по авторённым уровням пишется так же:
LS.gameProgressSubmit('custom:<dbid>', ...)(game_progress.level_id— TEXT ≤120, двоеточие проходит; бэкенд НЕ менялся).
Share-flow
- Реюз контроллера
customSimController.share(Ф6). Дляcat==='game'ссылка/тип уведомления переключены: link/quantik?level=custom:<id>, типgame_level_shared(обычная sim —/lab?sim=…,sim_shared). Авто-публикация + durablepushNotifученикам класса. Ответ теперь содержитlink. - Раздача игрового уровня из билдера — той же кнопкой «Раздать» (
openShareModal→LS.customSimShare), отдельный UI не нужен. Курикулумная привязка —lab_sim_linkssim_id='custom:<id>'(Ф6, не трогалось).
Для Phase 6 (лидерборд / живая гонка)
- Лидерборд может агрегировать
game_progressпоlevel_id(включаяcustom:<dbid>). Уровень-метаданные (title/chapter) для custom доступны черезQuantikLevels.getAsyncили прямойLS.customSimGet. - Живая гонка (мост
sim_state) — он на base-ветке sim-builder Ф7; авторённый игровой уровень уже монтируется тем жеSimEngine, что и встроенные, поэтому мост применим без изменений в этой фазе. - Авторинг-панель пишет
goal/gameтолько приst.game.enabled— обычные симуляции не затронуты.