# Пример: Pattern arena (§31-3)

Готовый вертикальный срез «паттерн-арены» на чистом `$`: фазы по явным правилам,
**отменяемая** фаза без отложенных выстрелов, пул врагов и снарядов, пауза по
выбранным часам, смерть и рестарт без утечек. Относится к разделу §31
«Документация и вертикальные примеры» и последовательности §49
[спецификации производственной готовности](https://github.com/Nikide/russiano2d/blob/main/docs/PRODUCTION_READINESS.md).

Пошаговый разбор — [docs/tutorial-arena.md](tutorial-arena).

## Запуск

```bash
./build/russiano2d --game demos/arena
python3 tools/run_tests.py arena_test                                # автоматическая приёмка (62 проверки)
R2D_BINARY=build-2d/russiano2d python3 tools/run_tests.py arena_test
```

Управление: `Space` — урон боссу, `I` — отменить фазу, `P` — пауза,
`R` — рестарт, `K` — смерть игрока, `A`/`D` — движение (`.controls()`).

## Что показывает

| Примитив | Как | Проверка |
|---|---|---|
| Фазы по явным правилам | таблица `PHASES` (HP/длительность/интервал) + `$.state` | таймер двигает фазу, HP 70→40→20 переводит volley→summon→burst, маркер переводит явно |
| Отменяемая фаза | фаза живёт в области `$.scene.scope`, действия — её таймеры | `interrupt` → `idle`; за 90 кадров **ни одного** нового снаряда/спавна |
| Spawn pool | `$.pool.create({max})` + `spawn`/`release`/`releaseAll` | 3×60 спавнов: `created` ровно 48, `spawned` 163, узлы не растут (13 → 61 = база+потолок) |
| Пауза | `$.time.pause()` + §44 `.processing({update,physics,timers:false})` | игровое время и снаряд стоят, фаза не меняется, таймеры не теряются; после resume всё продолжается |
| Смерть | `$.state` переход `killed → dead`, `onDeath` один раз | `deaths` = 1, второй смертельный удар отклонён, фазы молчат 90 кадров |
| Рестарт | `$.scene.load('arena', {scope:true})` + `releaseAll` | узлы/тела/подписки/таймеры/области равны исходным числам, прогресс начинается заново |

Числа из приёмки (`build/test_arena.log`):

```
числа после старта:  {nodes:15, bodies:10, subs:1, waits:0, timers:2, tasks:0, flows:0, scopes:2}
после 3×60 спавнов:  пул created=48, spawned=163, skipped=3; узлы ≤ база+48
отмена фазы:         shots 6 → 6 и spawned 19 → 19 за 90 кадров
смерть:              deaths=1; повторный удар → false; shots/spawned не растут
после рестарта:      {nodes:15, bodies:10, subs:1, waits:0, timers:2, tasks:0, flows:0, scopes:2}
```

## Файлы

| Файл | Что |
|---|---|
| `main.js` | точка входа: старт арены в области владения (§5) |
| `index.js` | фазы, пул, пауза, смерть/рестарт, HUD, публичный фасад `$.arena` |
| `project.json` | имя окна и размер |

## Правила, зафиксированные явно

* **Рестарт начинает арену заново** (hp 100, волна 0, счётчики жизни 0), а не
  продолжает с последней фазы: прогресс живёт в состоянии машины, а машина
  сбрасывается в `prepare`.
* **Отмена фазы = закрытие её области владения**: все scoped-таймеры и потоки
  снимаются, поэтому «отложенных выстрелов» не остаётся. Уже выпущенные снаряды
  не телепортируются — они доживают и возвращаются в пул по выходу за экран.
* **Смерть считается один раз**: переход `killed → dead` есть только из живых
  фаз, повторный `killed` в `dead` не находит перехода.
* **Пауза — по игровым часам**: `$.time.pause()` обнуляет `$.time.delta()`, а
  фаза и снаряды тикают именно им; §44-политика дополнительно выключает
  обработку узлов и не даёт им «просыпаться» в обход паузы.

## Чего не хватило в публичном API

1. **Авто-возврат в пул по времени жизни**: `$.pool.spawn` не принимает TTL, а
   `.release()` нужно звать самому (в примере — по выходу за экран в кадре).
   Удобного `$.pool.spawn(name, {...}, {ttl})`/`autoRelease(node, ms)` нет.
2. **У FSM нет отменяемого перехода/таймаута**: отмена фазы — это закрытие
   scope плюс переход в `idle`; остаток таймера фазы из `$` не читается
   (проверка «продолжается без потерь» делается по факту возобновления).
3. **`$.time.pause()` не сообщает «съеденное» время**: сколько игрового времени
   заняла пауза, из публичного API не видно.
4. **У пула нельзя изменить `max` на ходу**: `$.pool.create` с тем же именем
   возвращает существующий пул с предупреждением.
5. Попадания по игроку из пула снарядов здесь не считаются окном §9: это
   отдельный §7/§9-сценарий (contact/window), поэтому урон боссу в примере —
   явный `$.arena.damage()`, а не физическая атака.
