# Граф кадров — `$.cels`

Порт из [aarthificial/reanimation](https://github.com/aarthificial/reanimation):
персонаж — это **граф**, который по «водителям» (драйверам) решает, какой кадр
показать. Граф не хранит ни спрайтов, ни таймингов: узел выбирает узел, лист
отдаёт имя кадра, а игра рисует его как хочет.

```js
$.ready(() => {
    $.cels.define('hero', {
        // Начальные водители: 0 — «нет событий».
        drivers: { x: 0, grounded: 0, hurt: 0, time: 0 },
        root: {
            type: 'switch', driver: 'hurt',
            nodes: [
                { type: 'switch', driver: 'grounded', nodes: [
                    // Доля x выбирает ветку: 0 — стоять, 0.5 — бежать вправо, 1 — влево.
                    { type: 'switch', driver: { name: 'x', percentage: true }, nodes: [
                        { type: 'anim', cels: ['idle_0', 'idle_1'], driver: { name: 'time', auto: true } },
                        { type: 'anim', cels: ['run_0', 'run_1'], driver: { name: 'time', auto: true } },
                        { type: 'anim', cels: ['run_0', 'run_1'], driver: { name: 'time', auto: true }, mirror: true },
                    ]},
                    { type: 'anim', cels: ['jump_0'] },
                ]},
                { type: 'cel', cel: 'hurt_0' },
            ],
        },
    });

    const cels = $.cels.create('hero');
    cels.state({ x: 0.5, grounded: 0 });   // водители
    cels.tick(dt);                         // шаг графа
    $('#hero').sprite(cels.cel());         // имя кадра
});
```

---

## 1. Водители

Водитель — число в состоянии графа. Им игра говорит «иду вправо», «в воздухе»,
«ранен», а граф решает, что показать.

| Поле водителя | Смысл |
|---|---|
| `name` | имя значения в состоянии |
| `percentage: true` | число 0..1 превращается в индекс (доля) |
| `auto: true` | после выбора значение увеличивается на 1 (перебор кадров) |

Краткая форма: `driver: 'x'` или `driver: { name: 'x', percentage: true }`;
`percentage: true` рядом с `driver` тоже читается.

## 2. Узлы

| Вид | Что делает |
|---|---|
| `switch` | выбирает один из `nodes` по водителю |
| `anim` | лист: `cels` — кадры; водитель выбирает кадр |
| `cel` | один кадр (`mirror: true` — зеркалить) |
| `override` | всегда этот кадр |
| `termination` | граф ничего не показывает |

Массив узлов — краткая запись `switch` без водителя (выбирается первый).
Строка вместо объекта — краткая запись `cel`.

`setDrivers: { имя: число }` у узла **вливает водители** в состояние до выбора
ветки: так узел может переключить вложенное состояние (например, «в укрытии»).
Так же ведёт себя оригинал (`nextState.Merge(drivers)`).

## 3. Состояние персонажа

| Вызов | Возвращает |
|---|---|
| `$.cels.create(name, drivers?)` | состояние графа (независимое у каждого бойца) |
| `cels.tick(dt)` | шаг: с учётом `fps` может быть пропущен |
| `cels.resolve()` | решить немедленно (без `fps`) |
| `cels.cel()` / `flip()` | имя кадра / зеркалить ли |
| `cels.state({...})` / `set(name, value)` | задать водители |
| `cels.get(name)` / `values()` | прочитать водитель / все |
| `cels.trace()` / `node()` | путь решения (для отладки) |
| `cels.reset(values?)` | вернуть начальные водители |
| `cels.save()` / `load(data)` | снимок состояния |

`fps` у графа ограничивает частоту решения: кадр меняется не чаще, чем раз в
`1/fps` секунд, а не каждый кадр движка.

## 4. Связь с узлами

```js
$.cels.attach('#hero', 'hero', { sprites: { idle_0: spriteId, run_0: … } });
$.cels.state('#hero', { x: 1, grounded: 1 });   // управление
$.cels.tick(dt);                                 // один тик на все привязанные
```

`attach` создаёт состояние и запоминает его за узлом; `tick` решает все
привязанные графы и ставит кадр узлу (`sprite`, `flip_x`). Если у игры спрайты
лежат в атласе, передайте их картой в `sprites`.

## 5. Циклы и ошибки

Граф может «зациклиться», если игра собрала замыкание (узел ссылается сам на
себя). Разбор и решение это переживают: при разборе повторный узел становится
`termination`, при решении повторно посещённый узел пропускается. Без этого
стек QuickJS кончался с `Maximum call stack size exceeded`.

## 6. Ограничения

* **граф показывает имена кадров, а не рисует**: связка с атласами — на игре
  (`$.atlas`, `$.anim`), `attach` умеет только подставить кадр;
* **нет переходов и задержек**: в оригинале граф решался раз в кадр анимации;
  переходы между клипами — `$.anim`;
* **нет `MirroredAnimationNode` целиком**: поддержан флаг `mirror` у кадра, но
  не отдельные зеркальные поддеревья;
* **нет редактора**: в оригинале граф собирался в Unity-ассетах, здесь — кодом
  или JSON;
* **`trace` только читается**: редактор следов не портирован.

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

```bash
# водители, ветки, доли, автоинкремент, зеркало, fps, циклы, сейв
build/_deps/quickjs-build/qjs tests/js/cels_test.mjs
```
