# Фазы кадра

Документ фиксирует **фактический** порядок фаз кадра (§4.3 Спецификации
производственной готовности) и обязательства, которые из него следуют. Порядок
не является «правильным по замыслу»: он описан по коду `src/main.c`
(нативная часть) и `installFrameHooks` в `src/highlevel/api.js` (скриптовая), а
проверяется тестом `tests/agent/frame_phases_test.py` (фикстура
`tests/fixtures/frame_phases`).

Изменение порядка — изменение поведения API: оно требует обоснования,
обновления этого файла и теста, а не «мелкой правки цикла».

## Нативная часть (один кадр `src/main.c`)

| Шаг | Что происходит |
|---|---|
| 1 | ввод: SDL-события + виртуальный ввод агента накладывается на реальный |
| 2 | шаг физики фиксированным `R2D_FIXED_DT`; аккумулятор получает `app->dt * app->time_scale` (`engine.setTimeScale`: 0 — hit-stop) |
| 3 | контакты Box2D за прошедшие подшаги копятся в буфер кадра (`r2d_physics_begin_contacts` обнуляет его строго здесь) |
| 4 | вызывается `setUpdate` (скриптовый кадр ниже) |
| 5 | вызывается `setRender` (фаза отрисовки) |

Важно: за один кадр физика делает **один или несколько** фиксированных шагов;
контакты, которые видит игра, относятся ко всем подшагам этого кадра.

## Скриптовая часть (`installFrameHooks`)

Фазы идут ровно в этом порядке; в скобках — что их обслуживает.

1. **Синхронизация тел** — перенос позиций и углов из Box2D в узлы
   (`ctx.world.sync`).
2. **Контакты** — `collide` / `separate` / `hit` рассылаются узлам
   (`dispatchContacts`). Событие несёт `nid`, `source_id`, `target_id`, точку,
   нормаль и скорость (§13).
3. **Смена сцены** — отложенный `$.scene.load` применяется здесь, чтобы новая
   сцена прожила кадр целиком (`ctx.scene._tick`).
4. **Время** — твины, таймеры, тряска/вспышки, события ввода; окно (`tickTime`,
   `tickWindow`).
5. **`$.ready`** — один раз на первом кадре, затем код игры.
6. **Сцена** — `scene.update(dt, $)` текущей сцены.
7. **Хуки игры** — `$.update(dt, $)` в порядке регистрации.
8. **Катсцена** — `tickCutscene` (до управления: иначе гейт ввода опаздывает).
9. **Анимация спрайтов, наведение, управление** — `animateSprites`,
   `tickWorldHover`, `applyControls`.
10. **Подсистемы** — `$.anim`, плеер анимации, состояния, flow, экраны, диалоги,
    таймлайн, tilemap, VFX, частицы, навигация, префабы, слои, виджеты,
    триггеры, наблюдения, DevTools, i18n, пулы, вьюпорты, HTTP, SDK, шины
    звука, акустика, интерфейс.
11. **Отрисовка** (`setRender`) — обрезки кадра, `scene.render`, `$.render`,
    сборка батча, отладочный слой.

### Разделение часов внутри кадра

* **Simulation** (игровое время): `ctx.time.delta()` — анимация, состояния,
  таймлайны, частицы-логика. Стоит на паузе и hit-stop.
* **Presentation** (реальное время): тряска камеры, вспышки, VFX-частицы —
  продолжают идти на hit-stop, поэтому искры видно в замороженном кадре.
* **UI** (реальное время): `ctx.ui._tick()` — кнопки обязаны работать и на
  паузе.

Контракт часов и их домены — `docs/highlevel/time.md`.

## Обязательства (проверяются тестом)

| Обязательство | Как проверяется |
|---|---|
| `ready` вызывается ровно один раз и раньше кода игры | `frame_phases_test` |
| обновление сцены идёт до хуков `$.update` | индекс меток одного кадра |
| удаление узла внутри обработчика контакта безопасно; удалённый узел больше не получает кадровых вызовов | счётчик вызовов у удалённого узла |
| контакт доходит до обработчика узла и несёт устойчивые ID (`source_id > target_id`), а не `[object Object]` | журнал фикстуры |
| у каждого узла есть стабильный логический `nid` (§4.2, §14), он попадает в снимок узла (`$.prefab.save`) | `$('#x').nid()` и данные снимка |
| журнал `$.trace` показывает цепочку событий, ограничен по размеру и не пишет при выключении | `$.trace.list()`, `$.trace.limit`, `$.trace.on(false)` |

## Что здесь ещё не закрыто

* Разделение update/fixed-update по узлам (§44) — обработка узлов пока общая,
  opt-in политика в работе.
* Порядок «контакт → применение урона» отдельным контрактом ещё не закреплён
  (§9): сейчас урон применяет игра по событию, движок дедупликацию не ведёт.
* Асинхронная загрузка (§45) идёт вне этого порядка: она завершается в
  безопасной фазе кадра, но пока синхронным чтением.
