# Интерфейс и ввод

> Кнопки и карточки выбора на холсте нажимаются и подсвечиваются при наведении; можно регистрировать горячие клавиши, выбирать точку кликом по земле, узнавать, куда указывает мышь и кого выбрал локальный игрок. Клики, горячие клавиши, применение способностей, полный текст чата, уход игроков — всё попадает в поток событий.

Источник: https://war3ai.com/ru/docs/ui-input/

То, что рисует [холст](https://war3ai.com/ru/docs/canvas/), теперь **можно нажимать**. Рантайм перехватывает ввод окна игры, и внешняя программа может:

| Возможность | Коротко | Получает ли это игра |
|---|---|---|
| **Кликабельные элементы холста** | Кнопки, карточки выбора, панели: клик отправляет `ui.click`, при наведении — автоматическая подсветка | Клик по кнопке **до игры не доходит** |
| **Горячие клавиши** | Регистрируете сочетания вроде `F5` или `ctrl+shift+Q`, нажатие отправляет `hotkey` | Можно поглотить (вместе с символом, который порождает нажатие) |
| **Клик по земле** | Клик по миру отправляет `mouse.world` с координатами на земле | Можно поглотить («кликните, где поставить башню») |
| **Позиция мыши** | Обновляется каждый кадр: пиксели экрана, точка на земле под курсором, элемент холста под курсором | — |
| **Выделение** | Кого выбрал локальный игрок; при любом изменении приходит `selection.changed` | — |

Всё это — **локальный ввод + локальная отрисовка**: в поток приказов ничего не попадает, поэтому безопасно и в многопользовательской игре. Но если обработчик меняет мир (создаёт юнитов, меняет характеристики), это по-прежнему только для одиночной игры.

## Python: g.ui

```python
ui = g.ui                                                   # при первом обращении рантайм перехватывает ввод окна
ui.button("shop", "Купить зелье (50 золота)", screen=(40, 300), on_click=lambda g, ev: buy(g))
c = ui.choice("Новый уровень! Выберите награду", [("Сила +5", "крепче"), ("Скорость атаки +20%", "бьёт чаще"), ("Призвать волка", "ещё один помощник")],
              pause=True, on_pick=lambda g, i: give(g, i))  # ряд карточек посреди экрана; pause=True — пока выбирают, игра на паузе
i = c.wait(timeout=30)                                      # можно и ждать с блокировкой (события при этом обрабатываются и не теряются)
ui.hotkey("F5", lambda g, ev: g.say(hero, "Понял!"))        # по умолчанию нажатие поглощается
ui.hotkey("ctrl+shift+Q", on_press=..., swallow=False)
ui.mouse(on_click, capture=True, buttons=("left", "right"))  # ловить клики по земле: сообщать о левой и правой кнопке и поглощать их
xy = ui.pick_point("Кликните по земле: где поставить башню?")  # блокирующая версия: следующий левый клик по земле -> (x, y); Esc или тайм-аут -> None
ui.cursor()                                                 # {'screen': (x, y), 'world': (x, y, z) или None, 'hover': 'shop'}
ui.toast("Идёт 3-я волна!", seconds=3)
ui.close()                                                  # убрать свои элементы и горячие клавиши; ввод окну возвращается, только если им не пользуется другая программа
g.close()                                                   # или отключиться целиком (можно и так: with Game(...) as g:)
```

Обработчики получают `(g, ev)` и срабатывают, когда вы вызываете `g.events()`, — раннеры ботов и [игровых модов](https://war3ai.com/ru/docs/mods/) делают это каждый тик. Клики, для которых обработчик не задан, попадают в `ui.clicks`. Исключение в обработчике только записывается в лог и не мешает другим обработчикам и событиям.

Можно работать и напрямую с холстом: `g.canvas.text(..., clickable=True, hover=цвет)`; клики приходят из потока событий, а `ev.key` — это key, заданный при отрисовке. Если нарисовать кликабельный элемент, перехват ввода включается автоматически — трогать `g.ui` заранее не нужно.

**Запись горячих клавиш**: `F1` ~ `F24`, `A` ~ `Z`, `0` ~ `9`, `numpad0` ~ `numpad9`, `space enter esc tab backspace insert delete home end pageup pagedown left up right down`; впереди можно добавить `ctrl+`, `shift+`, `alt+`.

> **Внимание**
>
> Буквы и цифры без модификаторов конфликтуют с вводом в чат и горячими клавишами самой игры. Лучше берите клавиши, которые игра не использует, вроде F5 ~ F8, или сочетания.

## Новые события

В `g.events()` появились такие события (все поля — в [протоколе W3P](https://war3ai.com/ru/docs/protocol/)):

| kind | Когда | Удобные поля |
|---|---|---|
| `ui.click` | Нажат интерактивный элемент холста | `.key` — key на холсте, `.button` (`'left'` / `'right'`), `.mods` — модификаторы |
| `ui.hover` | Мышь зашла на элемент холста / ушла с него | `.key` (при уходе — `None`) |
| `hotkey` | Нажата зарегистрированная горячая клавиша | `.key` — запись клавиши, `.mods` |
| `mouse.world` | Клик по миру, если включён перехват кликов по земле | `.x .y` — координаты на земле, `.button`, `.value` (1 = клик поглощён) |
| `selection.changed` | Изменилось выделение локального игрока | Юнитов берите через `g.selection()` |
| `spell.cast` | Юнит применил способность (способность ушла на перезарядку) | `.spell` — четырёхсимвольный код, `.b` — уровень, `.value` — перезарядка в секундах, `.x .y` — точка применения |
| `message` | В окне сообщений на экране появилась строка | `.text` — полный текст, `.frame` — в каком окне, `.chat` (если это чат) |
| `player.left` | Игрок вышел или удалён после поражения | `.player` |
| `game.ended` | Выход из матча | — |

## Чат и экранные сообщения

То, что игрок пишет в чат, читается прямо из поля `.chat` события `message`:

```python
for ev in g.events():
    if ev.kind == "message" and ev.chat and ev.chat["text"] == "-follow":
        ...                                    # ev.chat = {'channel': 'Всем', 'sender': 'имя игрока', 'text': '-follow'}
```

У `g.messages()` свой, независимый курсор, и в нём есть ещё и подсказки игры («Нужно больше ферм», «Здесь строить нельзя»). В боте по ним видно, почему команда не выполнилась.

## Из других языков

- **Шлюз**: методы `ui.button`, `ui.choice`, `ui.hotkey`, `ui.mouse`, `ui.cursor` доступны в [шлюзе](https://war3ai.com/ru/docs/gateway/) под теми же именами. Передать функцию-обработчик удалённо нельзя, поэтому клики и горячие клавиши приходят в потоке событий (событие `ui.click` содержит `key`).
- **Прямая запись в общую память**: сначала отправьте семантическую команду `input_enable` (код операции W3P 74), и рантайм начнёт перехватывать ввод; в блоке ввода `Local\War3Input_<pid>` вы записываете таблицу горячих клавиш и переключатели мыши, а рантайм записывает обратно позицию мыши, точку на земле под курсором и элемент под курсором. Флаг `0x40` у элемента холста означает «интерактивный». Раскладка — в [протоколе W3P](https://war3ai.com/ru/docs/protocol/).

## Несколько программ одновременно

Моды, Farsight, MCP и каждый сеанс шлюза могут одновременно ставить кнопки и регистрировать горячие клавиши в одной игре, не мешая друг другу:

- каждая программа регистрирует свои горячие клавиши и свой переключатель кликов по земле, а SDK сводит всё в одну таблицу и передаёт её рантайму. Каждая клавиша попадает в таблицу один раз, событие получают все, и каждый узнаёт свои горячие клавиши по клавише;
- `ui.close()` убирает только своё, а ввод окну возвращается, лишь когда уходит последняя программа;
- если программу завершили принудительно и она не успела прибраться, рантайм раз в 2 секунды проверяет, живы ли зарегистрированные программы; когда все они завершились, он убирает оставленные ими горячие клавиши и перехват кликов по земле, а их кнопки больше не перехватывают клики.

## Замеры

2026-09-25, проверка в реальной игре на тестовом экземпляре, 16/16:

- клик по кнопке → `ui.click` + обработчик, счётчик перехватов в рантайме +1 (игра этот клик не получила); клик мимо кнопки ничего не вызывает;
- F6 → `hotkey`; клик по земле → `mouse.world` (поглощён);
- создать паладина и выбрать его → `selection.changed`, `g.selection()` совпадает; применить «Божественный щит» → `spell.cast('AHds', 1, 35.0)`;
- текст карты → `message`; чат → `message`, в `.chat` разобраны отправитель и текст;
- поражение компьютерного игрока → `player.left`; завершение матча → `game.ended`.

Клики по кнопкам настоящей мышью и подсветку при наведении тоже проверили по одному.

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

- **Позиция берётся от настоящей мыши**: игра сама читает позицию по системному курсору, поэтому наведение и `cursor()` отражают настоящую мышь. Перехват касается только нажатий.
- **Рисуется под указателем мыши**: Warcraft каждый кадр рисует указатель как часть картинки. Холст и облачки над головой рисуются до того шага, на котором игра рисует указатель: они перекрывают интерфейс игры, а указатель перекрывает их. Только если в этом кадре указатель не рисуется (скрыт или идёт заставка), они рисуются последним шагом.
- **Системное масштабирование**: если в своих тестах вы отправляете клики оконными сообщениями, координаты от процесса без поддержки DPI система увеличивает (при масштабе 150% замерено ×1.5). Объявите в тестовой программе поддержку DPI. На клики живого человека это не влияет.
- **В первый раз нужно прогреть шрифты** — около 1 секунды. Пока кнопки ещё не нарисованы, нажать их нельзя.
- **Вне матча клики по земле не сообщаются**: в главном меню и на экране итогов `mouse.world` не отправляется и не поглощается.
- Если нажатие было поглощено, а до отпускания вы переключились на другую программу или увели мышь за пределы окна, состояние всё равно сбрасывается, и следующее отпускание уже не поглощается.
- В 1.27 нет функций для создания новых фреймов интерфейса игры (они появились только в 1.31): кнопки и карточки здесь рисует рантайм, стиль может быть любым, но в собственной иерархии меню игры они не появляются.
