# Катсцены — `$.cutscene`

Дирижёр сценария **внутри текущей сцены**: забирает управление у игрока, ведёт
любые узлы мира, двигает камеру и возвращает всё как было. Мир при этом не
перезагружается — этим `$.cutscene` и отличается от `$.timeline`, которая
владеет полноэкранной «новеллой» (свои локации, состав, фон) и посреди уровня
запустить её нельзя.

```js
$.cutscene.define('bridge', [
    { take: 'input' },                                  // ввод забран
    { letterbox: 0.12 },                                // полосы сверху и снизу
    { camera: { at: [1200, 300], zoom: 1.5, ms: 600 } },
    { walk: '#npc', to: [1000, 300], speed: 200, ms: 400 },
    { say: 'Мост не выдержит!', who: 'npc' },           // панель из $.dialog
    { face: ['#npc', '#hero'] },
    { sfx: 'crash.ogg' }, { shake: 12, ms: 400 },
    { do: ($) => $('#bridge').shader('dissolve') },
    { wait: 300 },
    { give: 'input' },                                  // управление вернулось
    { letterbox: 0 },
    { camera: 'restore', ms: 400 },                     // камера как была
]);

$.cutscene.play('bridge');   // играется в текущей сцене
$.cutscene.skip();           // пропускает шаги с skip: true
$.cutscene.blocking();       // true, пока ввод забран — для своего ИИ
```

---

## 1. Шаги сценария

| Шаг | Смысл |
|---|---|
| `take: 'input'` / `give: 'input'` | забрать и вернуть управление игроком |
| `letterbox: 0.12` | полосы кадра (0 — убрать, максимум 0.5) |
| `camera: { at, zoom, ms }` | переезд и зум; `camera: 'restore'` — вернуть как было |
| `walk: '#npc', to: [x, y], speed, ms` | провести узел к точке (работает и у узлов с телом) |
| `face: ['#a', '#b']` | развернуть узлы друг к другу |
| `say: 'текст', who: 'npc'` | реплика через `$.dialog` |
| `sfx: 'файл', volume` | звук |
| `shake: 12, ms: 400` | тряска камеры |
| `fade: 1, ms: 300` / `flash: '#fff', ms: 200` | затемнение и вспышка |
| `do: ($) => { … }` | свой код (шейдер, анимация, что угодно) |
| `wait: 300` / `ms: 300` | длительность шага |
| `skip: true` | этот шаг пропускается по `$.cutscene.skip()` |

Мгновенные шаги (`take`, `give`, `letterbox`, `face`, `do`, `sfx`) выполняются и
сразу передают ход следующему — им время не нужно.

## 2. Методы

| Вызов | Смысл |
|---|---|
| `define(name, steps)` / `has` / `names` / `remove` | реестр сценариев |
| `duration(name)` | сколько миллисекунд займёт сценарий |
| `play(name, opts?)` | играть в текущей сцене; `opts.take: false` — не забирать ввод |
| `stop()` | остановить и вернуть управление и камеру |
| `skip()` | пропустить шаги с `skip: true` |
| `running()` / `runningName(name)` / `progress()` | что играется сейчас |
| `blocking()` | ввод забран — своё ИИ и ввод должны молчать |
| `letterbox()` | текущие полосы |
| `state()` / `describe()` | снимок состояния (для отладки и агента) |
| `on('end', fn)` / `off` | конец сценария (получает имя) |

Чистые функции: `normalizeSteps(steps)`, `stepsDuration(steps)`, `pointOf(value)`.

## 3. Что происходит с вводом

Пока идёт шаг `take`, катсцена:

1. снимает признак `attrs.controls` у всех управляемых узлов и **помнит прежние
   значения** — `give` возвращает ровно их;
2. гасит скорость управляемых тел: без этого герой «доползёт» по инерции;
3. не даёт читать ввод в обход гейта: `$.input.taken()` возвращает `true`.

`$.cutscene.blocking()` нужен своему ИИ: движок не знает про «NPC-скрипт» игры,
поэтому врагов останавливает игра — по этому флагу.

Тик катсцены идёт **до** применения управления игроком (`api.js`), иначе гейт
опаздывал бы на кадр.

## 4. Камера

`play()` снимает состояние камеры (цель слежения, сглаживание, границы, мёртвую
зону, зум и смещение) и `camera: 'restore'` возвращает **всё** это. Шаг `camera`
снимает слежение на время переезда: иначе кадровый тик камеры каждый кадр тянет
её к цели слежения и переезд откатывается.

## 5. Ограничения (честно)

* **катсцены не вкладываются**: `play` во время другой катсцены останавливает
  первую (с возвратом управления и камеры);
* **скип грубый**: шаг с `skip: true` пропускается целиком, шаги с игрой
  доигрываются; мгновенного «промотать всю катсцену сразу» нет;
* **`walk` ведёт по прямой** без обхода препятствий: для сложных дорог —
  `$.nav` и свой шаг `do`;
* **реплики — через `$.dialog`**: если диалог недоступен, шаг молча ничего не
  делает (в журнал ничего не пишется);
* **серверная в мультиплеере**: катсцену играет хост и рассылает как
  авторитетное состояние; клиент не решает сам, когда она началась (`net.md`).

## 6. Проверка

```bash
# ядро без движка: разбор шагов, длительность, точки
build/_deps/quickjs-build/qjs tests/js/cutscene_test.mjs
# в движке: ввод забран, NPC идёт, камера едет и возвращается
python3 tests/agent/highlevel_cutscene_test.py
```
