# Phase 3: Граф-уровни (движение по f(x)) + зоны-препятствия **Status:** ✅ Done (reviewed — PASS, committed) **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** fullstack ## Objective Новый тип уровня: Квантик движется по кривой `y=f(x)`, которую **собирает игрок** (настраивает параметры/выбирает выражение). Препятствия — «запретные зоны»; цель/звёзды/проигрыш — выражения. Реюз `plot` + `SimExpr`. Сид граф-главы. ## Tasks - [x] Task 1: «Бегунок по кривой»: герой-точка с `x` = функция t (напр. линейный проход xmin→xmax), `y = f(x)` через ту же скомпилированную функцию, что у `plot`. Кривая рисуется (P3 plot), герой едет по ней с glow/trail. Без физики (кинематический проход), либо мягкая физика — на выбор уровня. → `plot.runner:{duration,hold}` кладёт в env `.runX/.runY/.runDone`; герой = обычный point с `x:'curve.runX', y:'curve.runY'`, glow+trail. f компилируется 1 раз и питает И кривую, И бегунок. - [x] Task 2: Тип объекта/поле «зона» (forbidden/target): прямоугольник/круг в мире + удобные env-предикаты (или документированный паттерн: `fail:'inzone(...)'`). Реализовать helper-предикаты БЕЗ расширения небезопасного синтаксиса — предпочесть готовить булевы поля зон в env (напр. `zone1.hit`) на основе позиции героя, чтобы `goal`/`fail` ссылались на них. → `type:'zone'` (shape rect/circle, kind forbidden/target/collect, track). Движок кладёт `.hit` (1/0) в env. ⛔ Никаких inzone()-предикатов в грамматике — только именованные булевы env-поля. - [x] Task 3: Цель = добраться до конца/в целевую зону, не задев запретные (`fail`). Звёзды: пройти под нормативом, собрать бонус-точки (зоны-сборы). → goal.when=`'gate.hit'`, fail=`'pit.hit'`, stars=[collect-zone hit, доп. условие формы кривой]. - [x] Task 4: Управление: слайдеры коэффициентов `f(x)` (a·sin(b·x+c)+d и т.п.) ИЛИ выбор/набор выражения с inline-проверкой `SimExpr.compile(...).error` (как в sim-builder). Безопасно. → коэффициенты = обычные `params`-слайдеры движка; крутишь → кривая+путь героя перестраиваются. Свободный ввод выражения не понадобился (слайдеры коэффициентов достаточны для MVP-главы). - [x] Task 5: Контент: сид граф-главы (~4–5 уровней): синус под мостом, парабола над ямой, кусочная подгонка, экспонента/логарифм — растущая сложность, привязка к темам алгебры. → 5 уровней в `functions`: луч (a·x+b), синус (A·sin(k·x)), парабола (a·(x−5)²+k), модуль (a·|x−m|+1), экспонента (c·e^(r·x)). Все solvable (см. Concerns). - [x] Task 6: Интеграция в карту (Ф2): новая глава-созвездие; общий конвейер результата/XP. → глава `functions` в `CHAPTERS`; map.js НЕ тронут (рисует по метаданным). Бейдж темы в quantik.html стал per-level (`subject` → Физика/Алгебра) — аддитивно. - [x] Task 7: Тесты: проход по кривой достигает цели; задевание зоны → fail; смоук рендера кривой+героя. → headless vm-смоук (логика+per-level solvability, 29/29, удалён); серверный тест приёма zone+runner спеки (custom-sims.test.js, +2 теста, остаётся). ## Files to Modify/Create - `frontend/js/labs/_sim_engine.js` — поддержка «бегунка по кривой» (если не выразимо текущими полями) и подготовка булевых полей зон в env. Аддитивно, документировать в шапке. - `frontend/js/game/levels.js` — граф-глава. - `frontend/js/game/quantik-game.js` / `map.js` — новая глава, управление коэффициентами. - тест(ы). ## Acceptance Criteria - Квантик едет по собранной игроком кривой; правильная `f(x)` проводит между препятствиями к цели. - Задевание запретной зоны → проигрыш; норматив/сборы дают звёзды. - Кривая безопасна (SimExpr, без eval); existing симуляции/уровни не затронуты; тесты зелёные. ## Notes - НЕ вводить произвольные функции-предикаты в синтаксис выражений (безопасность). Зоны → булевы env-поля. - Переиспользовать P3 plot (несколько кривых, заливка, маркеры) для визуала «земли»/препятствий. ## Review Checklist - [x] Все задачи; аддитивность движка; без эмодзи/eval; тесты зелёные; lint baseline 0 ## Handoff to Next Phase ### Контракт «бегунка по кривой» (движок, `_sim_engine.js`) - На объекте `plot`: `runner:{ duration?:8, hold?:true }`. Делает из ПЕРВОЙ кривой plot дорожку. - Движок кладёт в env (в `_buildEnv`, ДО формульных центров): `.runX` (= `a + (b−a)·clamp(t/duration,0,1)`), `.runY` (= f(runX) ТОЙ ЖЕ скомпил. функции, что рисует кривую), `.runDone` (1 при t≥duration). - Герой = обычный `point` с `x:'curve.runX', y:'curve.runY'` + glow + trail. НЕ тело → нет само-ссылки (f компилируется один раз, питает И кривую, И бегунок). `hold:true` — остаётся на конце; иначе зацикливание по `time.loop`. - ⛔ Никакого eval: f — обычное SimExpr-выражение кривой. ### Контракт зон (движок) - `type:'zone'`, `id`, `shape:'rect'|'circle'`, `kind:'forbidden'|'target'|'collect'` (цвет/семантика), геометрия (rect: x,y центр + w,h; circle: x,y + r — числа ИЛИ выражения), `track?:'ball'` (чью позицию тестить), `label?`, `color?`. - Движок кладёт `.hit` (1/0) в env (последним — нужна актуальная позиция героя). `goal.when/fail/stars[].when` ссылаются на него. - ⛔ Предикаты в синтаксис выражений НЕ добавлялись — только именованные булевы env-поля (модель безопасности `t`/`tries` из Ф0). - Рисуется в `_drawObject`/`_drawZone`: forbidden=красный пунктир, target=зелёный, collect=золотой пунктир. Цвета — только canvas-стоки. - Зона НЕ кладёт `.x/.y` как центр объекта (`hasCenter` пропущен для type==='zone'). ### Как определяется граф-уровень (данные, `levels.js`) - Хелперы: `road(exprStr,a,b,dur)` (plot+runner, id 'curve'), `graphHero()` (point ball на curve.runX/runY), `rectZone/circZone(id,kind,...)`, `startMarker`. Уровень = спека с этими объектами + `goal{when:'gate.hit',fail:'.hit',stars}`. - ⚠️ ГОЧА: имена param `t/w/h/pi/e/E/PI/tau` зарезервированы движком (`h`=высота вьюпорта!). abs-уровень использует `m` (вершина), НЕ `h`. При добавлении уровней проверять имена коэффициентов. - `time:{duration,loop:false}` синхронизирован с `runner.duration` — герой доезжает до конца за один проход. ### Карта / запуск - Глава `functions` добавлена в `CHAPTERS` (key/title/subtitle/accent). map.js НЕ тронут — узлы рисуются по метаданным, тип спеки карте безразличен. Разблокировка: `unlockStars` 9/11/13/15/17 (≤ 18 макс. звёзд физ-глав → нет дедлока). - Запуск тот же (`QuantikGame.start` → `SimEngine.mount`); граф-уровни используют те же слайдеры params, спец-вайринг НЕ нужен. Бейдж темы в quantik.html — per-level по `level.subject` (аддитивно). ### Для Ф4 (квантовые способности) - `runDone`/`runX`/`.hit` — готовые env-поля для условий способностей (напр. «туннель» = временно игнорить forbidden.hit в `fail`). Способность может менять `params` (коэффициенты) или подменять выражение кривой — всё через тот же SimExpr-конвейер. - Зоны kind:'collect' уже «залипают» через механизм stars (Ф0). Новая способность = новый env-флаг + условие, БЕЗ eval. - Сервер уже принимает `zone`+`runner` (validateSpec, OBJECT_TYPES) — авторённые граф-уровни (Ф5) пройдут гейт.