# `$.tween` — Tween-объекты в стиле Godot 4

Подсистема добавляет к `$` **Tween-объект** — сценарий анимации: свойства,
шаги, цепочки, циклы, скорость, пауза и события. Это аналог
`create_tween()` / `tween_property()` / `chain()` из Godot 4.

```js
$.ready(() => {
    $('<rect>', { id: 'hero' }).at(0, 200).size(32, 32).appendTo($.world);

    const t = $.tween($('#hero'));
    t.property('x', 400, 0.6).trans('quad').ease('out');
    t.property('alpha', 0.2, 0.6);          // параллельно первому свойству
    t.chain().property('y', 120, 0.4);      // следующий шаг
    t.loops(2).on('finished', () => $.log('готово'));
    t.finished().then(() => $.log('Promise тоже пришёл'));
});
```

Старый Promise-API (`.tween()`, `.tweenTo()`, `.moveTo()`, `.fadeTo()`,
`.scaleTo()`, `.rotateTo()`, `$.sequence`, `$.wait`, `shake/flash`) работает
как раньше и никуда не делся — это отдельный, независимый движок.

---

## Создание

| Вызов | Назначение |
|---|---|
| `$.tween(target?)` | создать Tween. `target` — узел, обёртка `$('#id')`, CSS-селектор или обычный объект |
| `$.tweenOf(object)` | то же, но явно для произвольного объекта (в т.ч. `Node`) |
| `$.tweens()` | массив живых (не убитых и не завершённых) Tween'ов |
| `$.tween.active()` | число живых Tween'ов |
| `$.tween.transition(trans, ease)` | та же чистая функция `transitionFunction` |

Если цель не найдена (нет узла по селектору), Tween создаётся, но `property()`
ничего не запишет — сначала проверьте селектор.

## Свойства и модификаторы

| Метод Tween | Возвращает | Смысл |
|---|---|---|
| `property(prop, to, seconds)` | PropertyTweener | анимировать свойство от текущего значения к `to` |
| `interval(seconds)` | IntervalTweener | пауза внутри сценария |
| `callback(fn)` | CallbackTweener | вызвать `fn()` в нужный момент |
| `method(fn, from, to, seconds)` | MethodTweener | звать `fn(value, t)`, где `value` интерполируется, `t` — прогресс с плавностью |
| `chain()` | Tween | начать новый шаг (следующий твинер — после завершения текущего) |
| `parallel()` | Tween | вернуться к «всё параллельно»; отменяет пустой шаг после `chain()` |
| `loops(n)` | Tween / число | число проходов; `n < 0` — бесконечно |
| `speed(scale)` | Tween / число | множитель скорости |
| `time()` | секунды | сколько времени сценария прожито (с учётом `speed`) |
| `progress()` | `0..1` | прогресс текущего прохода |
| `trans(name)` / `ease(kind)` | Tween | переход/плавность по умолчанию для последующих твинеров |
| `pause()` / `play()` | Tween | пауза и продолжение самого Tween'а |
| `stop()` | Tween | остановить, но оставить живым: `play()` продолжит с того же места |
| `kill()` | Tween | убить безвозвратно |
| `isRunning()` / `isValid()` / `isPaused()` | `bool` | состояние |
| `bind(node)` | Tween | убить Tween вместе с узлом |
| `ignoreTimeScale(flag)` | Tween | идти по реальному кадру, игнорируя паузу и `$.time.scale()` |
| `on('finished'\|'loop'\|'step', fn)` / `off(...)` | Tween | подписка на события |
| `finished()` | `Promise` | разрешается по завершении |

Модификаторы твинера (цепочкой на нём самом):

| Метод | Смысл |
|---|---|
| `.from(value)` | явное начальное значение |
| `.fromCurrent()` | начать от текущего значения (по умолчанию) |
| `.asRelative()` | `to` — приращение к начальному значению |
| `.delay(seconds)` | задержка перед стартом (алиас `.delaySeconds`) |
| `.trans(name)` / `.ease(kind)` | плавность только этого твинера |
| `.interpolator(fn(from, to, t) => value)` | своя интерполяция; `t` — уже с плавностью |

```js
const t = $.tween(node);
t.property('x', '+120', 0.4).asRelative().trans('back').ease('out');
t.property('scale', 1.4, 0.2).delay(0.4);
// interval() возвращает Tweener, а callback() живёт на твине: двумя строками.
t.chain().interval(0.1);
t.callback(() => $.log('пауза кончилась'));
t.method((v) => bar.value = v, 0, 1, 0.5).trans('sine').ease('in_out');
```

## Свойства цели

* **Узел** — те же имена, что понимает движок: `x`, `y`, `alpha`/`opacity`,
  `angle`/`rotation`, `scale`, `scaleX`, `scaleY`, `w`/`width`, `h`/`height`,
  `value`, а также любой числовой атрибут (`node.attrs[...]`).
* **Обычный объект** — любое числовое поле. Поддерживается вложенный путь
  `'a.b.c'`; недостающие промежуточные объекты создаются при записи.

## Порядок шагов

Твинеры, добавленные подряд, идут **параллельно** и образуют один шаг.
`chain()` начинает новый шаг, который стартует после завершения предыдущего
(с учётом `.delay()` всех его твинеров). `loops(n)` повторяет весь сценарий
целиком.

```js
const t = $.tween(node);
t.property('x', 100, 1.0);
t.property('y', 100, 1.0);      // идёт одновременно с x
t.chain().property('alpha', 0, 0.5);   // начнётся после x и y
```

## Переходы и плавности

Две независимые оси, как в Godot:

* `trans`: `linear`, `sine`, `quad`, `cubic`, `quart`, `quint`, `expo`,
  `circ`, `elastic`, `back`, `bounce`, `spring`;
* `ease`: `in`, `out`, `in_out`, `out_in` (короткие `inOut` / `outIn`).

```js
import { transitionFunction } from './src/highlevel/tween.js';
const f = transitionFunction('quad', 'out');   // чистая функция t → [0..1]
```

Границы жёсткие: `f(0) === 0`, `f(1) === 1`. Внутри `elastic`, `back` и
`spring` перелетают цель — это их смысл. Неизвестное имя перехода или
плавности не роняет игру: пишется строка в журнал и берётся `linear`/`in_out`.

## События и `finished()`

```js
const t = $.tween(node);
t.property('x', 100, 1);
t.loops(3);
t.on('step', (e) => $.log('шаг', e.step, 'проход', e.loop));
t.on('loop', (e) => $.log('проход', e.loop));
t.on('finished', () => $.log('всё'));
await t.finished();
```

* `step` — шаг завершён (поле `step` — индекс шага, `loop` — номер прохода);
* `loop` — проход завершён и начинается следующий (в конце последнего прохода
  `loop` уже не приходит);
* `finished` — сценарий закончился целиком; при `loops(-1)` не приходит никогда.

## Время, пауза и kill

* Tween живёт в игровом времени: `$.time.pause()` останавливает его,
  `$.time.scale()` ускоряет/замедляет. `ignoreTimeScale(true)` переключает
  Tween на реальный кадр — тогда на него не действуют ни пауза, ни масштаб.
* Свойство с `seconds <= 0` в первом шаге применяется сразу, без кадра.
* `kill()` делает Tween недействительным: `finished()` **не** разрешается, а
  `on('finished')` не вызывается (как в Godot). `stop()` — мягкая остановка,
  после неё `play()` продолжает с того же места.
* `bind(node)` убивает Tween, когда узел удалён (`removed`) или убит
  (`max_hp > 0` и `cur_hp <= 0`).

## Отличия от старого Promise-API

| | старый API | `$.tween` |
|---|---|---|
| Единица | один переход | сценарий из шагов |
| Результат | `Promise` | объект с `finished()` и событиями |
| Параллельность | только через `Promise.all` | по умолчанию |
| Циклы | вручную | `loops(n)` |
| Пауза | `.pauseTweens()` на узле | `t.pause()` + общая `$.time.pause()` |
| Плавность | `easeInOutQuad` и т. п. (`EASES`) | `trans` × `ease` (`transitionFunction`) |
| Задержка | `await $.wait(ms)` | `.delay(seconds)` |

Старые имена плавностей сохранены без изменений — на них стоят демо и `$.anim`:
`easeFunction` и `easeNames` (обе экспортируются из `src/highlevel/tween.js`).
Таблица `EASES` — внутренняя, наружу не отдаётся: тянуть её из модуля не нужно,
список имён даёт `easeNames()`.

## Производительность: нативная лента простых твинов

Простой твин узла — `.tween({ x, alpha }, ms, ease)`, `.moveTo`, `.fadeTo`,
`.scaleTo`, `.rotateTo`, `.fadeIn/.fadeOut` — со встроенной плавностью и
свойствами `x, y, alpha/opacity, angle, scale, scaleX, scaleY, w/width,
h/height, value` целиком считается в C (`src/nodes.c`): состояние, плавность
и запись в узел (`x`/`y` — вместе с телом). JS только разрешает Promise.
2000 таких твинов стоят ≈0,04 мс за кадр против ≈1,2 мс в JS, значения
совпадают побитово (`tests/agent/native_passes_test.py`).

Своя функция плавности или запись в `attrs` идут прежней JS-лентой. Если
одно свойство одного узла одновременно ведут твины из обеих лент, последней
пишет JS-лента. Сценарии `$.tween(target)` (этот документ) остаются в JS:
шаги, петли и события — оркестрация, а не горячий путь.

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

* `trans`/`ease`, заданные на Tween, влияют только на **последующие**
  твинеры; для уже добавленного используйте модификатор на нём самом.
* Мгновенный твинер (`seconds = 0`) первого шага записывает значение сразу,
  поэтому `.delay()` на нём влияет только на момент завершения шага, но не на
  момент записи значения.
* `bind()` реагирует на `removed` и на смерть по здоровью (`max_hp > 0`); у
  узлов без здоровья (`max_hp === 0`) смерть по `cur_hp` не отслеживается.
* `loops(-1)` со сценарием из одних мгновенных шагов прокручивает один проход
  за кадр — иначе кадр завис бы в бесконечном цикле.
