# Feature Context: Квантик — Законы Мира

## Current State
- Ветка `feature/quantik-game` ответвлена от `feature/sim-builder` (движок P1–P3 там, не в master).
- При ответвлении унаследован **чужой uncommitted WIP** sim-builder: `frontend/js/sim-builder.js`,
  `frontend/sim-builder.html`, `.claude/settings.json` + множество untracked `tmp_*`/мусорных файлов.
  ⛔ НЕ трогать и НЕ коммитить этот WIP — стейджить только свои файлы поимённо.
- **Phase 0 реализован** (pending review): слой целей в движке `_sim_engine.js` (блок `goal`,
  компиляция when/fail/stars через SimExpr, состояние результата, HUD-оверлей, API
  `onGoal/getResult/resetResult`) + серверный гейт `validateSpec` пропускает `goal`/`game`.
  Изменены: `frontend/js/labs/_sim_engine.js`, `backend/src/controllers/customSimController.js`.
  Аддитивно: спека без `goal` ведёт себя ровно как раньше (HUD не создаётся, побед не считается).
  Смоук 40/40; `npm test` 238 pass / 8 baseline fail; lint:routes 0.
- **Phase 1 реализован** (pending review): сквозной играбельный срез. Страница `/quantik`
  (`frontend/quantik.html` + `frontend/js/game/quantik-game.js`) монтирует уровень-спеку через
  `SimEngine.mount`; «игровой режим» = HUD из Ф0 (сам по наличию `goal`) + слайдеры params +
  play/reset. Уровень `phys-artillery-1` — данные в `frontend/js/game/levels.js`
  (`window.QuantikLevels`): physics-гравитация + body-запуск под углом θ/скоростью v, портал-цель,
  бонус-звезда. На победу `onGoal` → `LS.gameProgressSubmit` + DOM-оверлей успеха (звёзды/время/попытки).
  Прогресс: таблица `game_progress` (мигр.**076**), API `/api/game/progress` (GET/POST,
  `gameController.js`+`routes/game.js`, смонтировано в `server.js` после `/api/custom-sims`),
  клиент `LS.gameProgressList/Submit`. Сайдбар: `/quantik` (icon `rocket`) виден всем.
  Новые: `076_game_progress.sql`, `gameController.js`, `routes/game.js`, `quantik.html`,
  `js/game/levels.js`, `js/game/quantik-game.js`, `tests/game.test.js`. Изменены: `server.js`,
  `js/api.js`, `js/sidebar.js`. `npm test` 251 pass / 8 baseline fail (game.test.js 13/13);
  lint:routes 0; миграция применяется чисто.
- **Phase 2 реализован** (pending review): одиночный уровень превращён в **играбельный мир**.
  Карта-созвездие (`frontend/js/game/map.js`, `window.QuantikMap`) на звёздном фоне: 6 физ-уровней
  в 2 главах (Кинематика 1–3, Динамика 4–6), узлы-«звёзды» со статусом (locked/available/completed+
  звёзды), линии-связи, поэтапное появление. Шапка: нарратор-Квантик (`PetSprite`), XP-бар + «уровень
  Квантика», всего звёзд, скин-пикер (8 скинов, часть за XP/звёзды). Контент уровней расширен в
  `levels.js` (метаданные `chapter/order/par_ms/unlockStars`, по 2 звезды: кристалл + норматив времени).
  Разблокировка/XP/группировка — ЧИСТЫЕ функции в новом `frontend/js/game/progress-logic.js`
  (`window.QuantikProgress`), покрыты тестом. Навигация: карта→интро(нарратор)→уровень→успех
  (нарратор по звёздам)→карта; «Дальше» активирована (`nextPlayable`); скин тинтует героя+нарратора
  (localStorage `quantik-skin`). **Backend НЕ тронут** — XP клиентская агрегация из `game_progress`.
  Новые: `js/game/map.js`, `js/game/progress-logic.js`. Изменены: `quantik.html`, `js/game/levels.js`,
  `js/game/quantik-game.js`. `node --check` все OK; смоуки (логика 16/16, рендер 7/7, winnability 6/6
  на реальном движке) зелёные и удалены; `npm test` 259/251 pass / 8 baseline fail (без изменений);
  lint:routes 0.
- **Phase 3 реализован** (pending review): новый ТИП уровня — Квантик едет по кривой `y=f(x)`,
  которую СОБИРАЕТ игрок (слайдеры коэффициентов). Движок (`_sim_engine.js`, аддитивно):
  (1) «бегунок по кривой» — на `plot` поле `runner:{duration,hold}` кладёт в env `<id>.runX/.runY/.runDone`;
  герой = обычный point на `curve.runX/runY` (f компилируется 1 раз, питает И кривую, И бегунок — нет само-ссылки);
  (2) `type:'zone'` (rect/circle, kind forbidden/target/collect, track) → булево env-поле `<zoneId>.hit` (1/0);
  goal/fail/stars ссылаются на него. ⛔ Предикаты в грамматику SimExpr НЕ добавлялись. Новая глава-созвездие
  `functions` в `levels.js` (5 уровней: луч/синус/парабола/модуль/экспонента, `unlockStars` 9..17 ≤ 18 макс
  физ-звёзд → нет дедлока); map.js НЕ тронут (рисует по метаданным). Сервер `validateSpec` принимает
  `zone`+`runner` (OBJECT_TYPES + поля). Изменены: `_sim_engine.js`, `levels.js`, `customSimController.js`,
  `quantik.html` (per-level бейдж темы). Новые тесты: custom-sims.test.js +2 (приём zone+runner, отказ
  unknown type) — 26/26. Headless vm-смоук (per-level solvability + logic 29/29) зелёный и удалён.
  `npm test` 261 / 253 pass / 8 baseline fail (без новых); lint:routes 0; все `node --check` OK.
- **Phase 4 реализован** (pending review): фирменные квантовые способности + SR-связка, ВСЁ через
  безопасную модель (движок `_sim_engine.js` НЕ тронут). Новый `frontend/js/game/quantik-abilities.js`:
  `window.QuantikEnergy` (клиентский ресурс энергии, localStorage `quantik-energy`, 0..99;
  grant/spend/canSpend/rewardForQuality; TUNNEL_COST=3, GOOD=1/EASY=2) + `window.QuantikAbilities`
  (`mountBar` — HUD энергии + кнопки «Повторение/Туннель/Прицел» оверлеем на сцене; `openRestRoom` —
  мини-сессия повторения флешкарт в модалке, реюз `LS.fcListDecks/fcStudySession/fcReview`, НЕ iframe).
  **Туннель** = тратит энергию → `inst.setParam('tunnel',1)`; барьер = `forbidden`-зона `wall`,
  `fail:'wall.hit && tunnel<1'` (tunnel — не слайдер, отсутствует в env → 0 → стена сплошная).
  **Прицел** = пауза-тоггл над пунктир-plot предсказанной траектории. **Суперпозиция** = чистый
  контент: 2 тела `ball`+`ball2`, `goal.when` с обоими. Глава `quantum` (L12–L16) + `CHAPTERS.quantum`
  в `levels.js`; карта рисует автоматически (map.js не тронут). `js/api.js` +2 врапера
  (`fcStudySession`, `fcReview`). `quantik.html` +script-тег +CSS `.qa-*`. **Backend НЕ тронут.**
  Все `node --check` OK (вкл. инлайн quantik.html); headless vm-смоук (РЕАЛЬНЫЕ движки):
  энергия + суперпозиция-оба-тела + tunnel-flips-fail + per-level solvability sweep (5/5 выигрываемы,
  full-star достижим, L15/L16 без tunnel = 0 win) + регресс 11 существующих уровней — 48/48, удалён.
  Контент-фикс: монета L16 (5,6)r0.7 → (5,6.9)r0.85 (была несовместима со 2-й звездой k≥6.8).
  `npm test` 261 / 253 pass / 8 baseline fail (без новых); lint:routes 0.
- **Phase 5 реализован** (pending review): авторинг игровых уровней в sim-builder + раздача классу.
  ⚠️ **ПАРАЛЛЕЛЬНАЯ СЕССИЯ активна** на ветке (правит sim-builder + admin «games»), поэтому все правки
  sim-builder.js/.html — строго АДДИТИВНЫЕ (новые методы/панель/CSS-блок, существующие строки почти не
  тронуты). sim-builder: панель «Игровой уровень (цель/звёзды)» (`sectionGame` + wiring + `playGame` +
  helpers `loadGame`/`buildGoal`/`buildGameMeta`) — тумблер «Это игровой уровень» включает слой goal
  (`when/title/hint/hold/fail`) + до 3 звёзд (`when`+`label`) + метаданные (`chapter/order/par_ms`);
  выражения проверяются inline через `SimExpr.compile`. `blankState`/`loadFromSim`/`buildSpec`/`validate`
  расширены аддитивно (по 1 врезке каждый). Кнопка «Играть» монтирует SimEngine в модалке (HUD/победа
  активируются сами наличием `goal` — Ф0). Round-trip goal/game без потерь.
  Игра: `QuantikLevels` стал асинхронным — `ensureCustom()` грузит `custom_sims` cat='game' (свои+
  published) и мёржит как записи `custom:<dbid>`; `getAsync(id)` резолвит deep-link (own draft через
  `LS.customSimGet`). Новая глава `custom` в `CHAPTERS`. quantik.html: `Promise.all([loadProgress,
  ensureCustom])` до карты + deep-link `?level=custom:<id>` (без гейта unlockStars). Backend:
  `share()` для cat='game' шлёт `game_level_shared` со ссылкой `/quantik?level=custom:<id>` (иначе
  `/lab?sim=…`), ответ +`link`. `CATS` уже содержал 'game' (Ф0/Ф3); goal/game уже в validateSpec.
  Изменены: `frontend/js/sim-builder.js`, `frontend/sim-builder.html`, `frontend/js/game/levels.js`,
  `frontend/quantik.html`, `backend/src/controllers/customSimController.js`. Новый тест:
  `tests/quantik-authoring.test.js` (6/6). Headless round-trip-смоук (vm + реальные _sim_expr+sim-builder
  +levels) 7/7 — удалён. Все `node --check` OK (вкл. инлайн обоих HTML). `npm test` 267 / 259 pass /
  8 baseline fail (без новых); lint:routes 0.

## Key Architecture Decisions
- **«Атом» = блок `goal` в спеке** (булево SimExpr). Любой уровень = спека SimForge + `goal`.
  Движок вычисляет `goal.when` каждый кадр; победа → result + callback. Нет `goal` → no-op.
- **Уровни хранятся в `custom_sims`** (cat='game'), а не в новой таблице. Реюз авторинга/шаринга/embed.
  Новые таблицы — только под ПРОГРЕСС игрока и лидерборд (мигр.).
  - **Уточнение Ф1**: для MVP уровни — ВСТРОЕННЫЕ ДАННЫЕ в `frontend/js/game/levels.js`
    (`window.QuantikLevels`, форма `{ id, title, subject?, hint?, spec }`), а не записи `custom_sims`.
    `custom_sims` cat='game' остаётся целевым хранилищем для авторённых уровней (Ф5); реестр тогда
    станет асинхронным (загрузка опубликованных + слияние со встроенными той же формы записи).
- **Герой Квантик**: в уровне = engine point с `body` + glow + trail (визуал P2). На карте/в
  диалогах = `PetSprite.render(level, mood, accessories, colorKey, streak, pattern)` (DOM SVG).
- **Управление = чинить закон**, а не WASD: игрок крутит `params`-слайдеры движка (угол/скорость/
  гравитация) или собирает `f(x)`; затем «Запуск» — симуляция проигрывается к цели.
- **Безопасность**: цвета — только в canvas-стоки; текст спеки — escape (`& < >`); выражения —
  только длина на сервере, исполняет безопасный SimExpr на клиенте.

## Engine touch-points (Phase 0)
- Спека v1 формат — в шапке `_sim_engine.js`. Добавить `goal`/`game` СЮДА (документировать).
- rAF-цикл `_renderFrame` (вычисляет env). Добавить `_evalGoal()` после построения env.
- `mount()` возвращает инстанс — добавить `onGoal`, `getResult`, `resetResult`.
- HUD — DOM-оверлей `_labelLayer`/новый слой (как readout-бейджи). Без эмодзи, inline SVG.
- Серверный гейт `customSimController.validateSpec` (:93) — разрешить `goal`/`stars`/`hint`/`game`.

## Cross-Phase Dependencies
- Phase 1+ зависят от `goal`/`getResult`/`onGoal` из Phase 0.
- Phase 2 (XP/скины) зависит от прогресса Phase 1.
- Phase 4 (туннелирование) зависит от флешкарт-SR API.
- Phase 5 (авторинг) трогает sim-builder — к этому моменту чужой P4-WIP должен быть смержен в
  sim-builder; свериться перед стартом фазы (возможен мерж base-ветки).
- Phase 6 (живая гонка) зависит от моста `sim_state` (Ф7 sim-builder) — он на base-ветке.

## Temporary Workarounds
(пока нет)

## Phase 0 — API/гочи (для следующих фаз)
- Движковое API цели: `inst.onGoal(cb)` (1 раз при победе, cb получает `getResult()`),
  `inst.getResult()` → `{won,failed,timeMs,attempts,stars:{got,total}}` (без goal → `null`),
  `inst.resetResult()` (сброс результата, НЕ считается попыткой). `inst.reset()` = полный
  перезапуск уровня + `attempts++` (пользовательская попытка; первый авто-reset при mount НЕ считается).
- HUD появляется **автоматически** при наличии `goal` в спеке (отдельного флага «game mode» нет).
- `timeMs` = **мировое время** `t` от старта (`max(1, round(t*1000))`), детерминизм; не wallclock.
- Env цели = весь env кадра + единственный доп.идентификатор **`tries`** (= attempts). Других не вводить.
- Серверный `validateSpec` принимает `goal{when,title,hint,hold,fail,stars≤3}` и `game{...}` (резерв Ф1/5);
  выражения не исполняются (только длина ≤500), текст escape+обрезка.
- Победа делает `pause()` в кадре; следующий queued-rAF выходит рано → `onGoal` не задвоится.

## Open Questions / Notes
- Категория `cat='game'` — проверить список `CATS` в customSimController.js, расширить при необходимости.
- Ассеты: разрешены CC0/открытые из интернета (выбор пользователя) — фиксировать источник+лицензию
  в коммите/доке; визуальная база остаётся in-house (PetSprite/canvas/FLUX).
- Маршрут страницы игры: clean URL `/quantik` (паттерн `/sim-builder`, `/lab`).
