# Пример «Melee/contact» (§31-2)

Законченный пример на одном `$`: удар по **окну анимации**, цель с несколькими
**hurtbox**, **отражение** снаряда щитом, физическая **отдача** и hit-фидбек.
Ни `engine.*`, ни импорта внутренностей движка — только публичное API.

```bash
./build/russiano2d --game demos/melee
python3 tests/agent/melee_test.py                      # автоматическая проверка
R2D_BINARY=build-2d/russiano2d python3 tests/agent/melee_test.py   # сборка без RE2D
```

## Управление

| Клавиша | Действие |
|---|---|
| `A` / `D` | шаг (встроенное `.controls()`) |
| `Space` | удар (клип `slash`) |
| `Shift` (удерживать) | щит: попадание снаряда отражается |
| `J` | выстрел в героя (стрелок слева) |

## Что именно показано

**1. Окно анимации, а не «пока играет клип».** Клип `slash` (420 мс) объявляет
маркеры `window: 'attack'` с `windowPhase: 'open' | 'close'`:
открытие на 120 мс, закрытие на 300 мс. Игра открывает hitbox и создаёт
`$.combat.attack({ window: 180 })` **по маркеру**, а по закрытию зовёт
`$.combat.endAttack()`. Поэтому первые 120 мс клип играет, но урона нет.

**2. Несколько hurtbox — один gameplay-hit.** У `#grunt` три hurtbox
(`torso`, `head`, `legs`). Пока окно открыто, игра каждый кадр «щупает» hitbox
(`.areaOverlap('blade')`), но дедупликация §9 считает попадание по **владельцу**
цели, поэтому удар ровно один. Новая атака бьёт снова.

**3. Отражение снаряда.** Снаряд `#shot` — тело с областью `body`. Пока
удерживается `Shift`, включена область `guard`; попадание в неё вызывает
`$.combat.reflect()`: у атаки появляется **новая identity**, новый владелец
(герой) и команда, снаряд разворачивается и бьёт стрелка (`×1.5` урона —
правило игры).

**4. Отдача — физика, а не косметика.** Удар несёт `impulse`; §9 применяет его
телу и кладёт в канал внешней скорости (§8), поэтому `.controls()` не стирает
отдачу следующим тиком. Отброшенный враг упирается в колонну и **не проходит
сквозь неё**.

**5. Feedback — presentation.** По событию `damage` игра зовёт `$.hitFx`
(hit-stop, тряска камеры, вспышка, частицы). Тест доказывает независимость:
с `$.store.set('feedback', false)` урон и счётчик hits те же, а `$.hitFx` не
вызывается вовсе.

## Файлы

| Файл | Что внутри |
|---|---|
| `project.json` | окно 1280×720 |
| `main.js` | `$.ready` → сцена `melee` |
| `index.js` | арена, герой, цели с hurtbox, клип удара, щит, снаряд, HUD |

## Ограничения и заметки

* **Снаряд — sensor-тело.** Попадание считает gameplay (`areaOverlap` + §9), а
  не контакт тел; поэтому урон и отражение детерминированы, а стены снаряд не
  останавливают. Физический CCD-снаряд, который останавливают стены, показан
  `tests/agent/highlevel_ccd_test.py` (§7).
* **Звук в пример не входит** (в `demos/melee` нет ассетов). Точка подключения —
  одна строка: `$.hits.preset('crit', { sound: 'assets/audio/sfx/hit.ogg' })`.
* **Препятствие для отдачи низкое.** Колонна (верх 545) задевает тела на полу,
  но не перекрывает траекторию снаряда (526) — иначе снаряд разбивался бы о неё
  раньше, чем встречал героя.
* **Presentation играет прямо по ответу §9.** `$.hitFx(res)` берёт точку и цель
  из самого damage-ответа (`x`/`y`, `targetNodeId`): собирать `{ x, y, self }`
  руками не нужно, а цель в ответе адресована устойчивым `nid`, а не ссылкой на
  узел (ответ уходит в JSON без циклов).

Короткий гайд по этому примеру — [docs/tutorial-melee.md](tutorial-melee).
