# RPG-компаньон

> ИИ-компаньон для игрока в RPG и пользовательских картах: ходит за вами, помогает бить крипов, лечит, когда у вас мало здоровья, и разговаривает с вами. Четыре режима; унаследуйте один класс, поменяйте пару атрибутов — и компаньон уже ваш.

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

Не только для обычных матчей. В RPG и пользовательских картах можно завести себе **ИИ-компаньона**: он ходит за вами, помогает бить крипов, лечит, когда у вас мало здоровья, а когда делать нечего — перекидывается с вами парой фраз. Реплики можно брать даже из локальной LLM.

**Как этим пользоваться — решаете вы.** Всё разбито на три слоя API, снизу вверх, и любой из них можно использовать напрямую:

| Слой | Что это | Для чего |
|---|---|---|
| **JASS-канал** `g.jass` | Все функции JASS, доступные авторам карт (их 1291), вызываются напрямую по имени (создать юнита, настроить союз, выдать предмет, переименовать, показать текст, воскресить героя…) | Придумать свой геймплей |
| **Удобные методы** | `g.spawn`, `g.set_alliance`, `g.player_slots`, `g.show_text`, `g.map_data`: частые задачи уже обёрнуты | Писать свои вспомогательные скрипты |
| **Фреймворк компаньона** | `openwar3.companion.Companion` + `openwar3.talk.Talk`: унаследуйте класс, поменяйте пару атрибутов — и у вас компаньон, который ходит следом, помогает в бою, лечит и болтает | Просто нужен компаньон |

> **Внимание**
>
> Только для **одиночных игр и игр по локальной сети, созданных вами**. Создание юнитов, настройка союзов и тому подобное — это одностороннее изменение мира на вашем компьютере: в одиночной игре (против компьютера) это нормально, а в многопользовательской у остальных игроков начнётся рассинхронизация. Поэтому в многопользовательской игре JASS-канал пропускает только функции для чтения, а компаньон автоматически переходит в режим «только разговор».

## Быстрый старт: в Farsight в один клик

1. **Выберите карту**: в Farsight откройте страницу «Экземпляры и запуск» → «Следующий матч» → карта и выберите RPG-карту (в списке всё, что лежит в `Scenario` и `Download` папки `Maps` в каталоге игры, например `(4)WarChasers`).
2. **Выберите схему**: в выпадающем списке «ИИ-схема» на карточке экземпляра выберите **Пример компаньона (buddy)** → «Выбрать».
3. **«Начать тест»**: когда игра запустится, **играйте сами в окне игры**. Компаньон — паладин по имени «Светик» — появится рядом с вами.

Можно и из командной строки:

```bash
python tools/play.py --bot brains/examples/buddy.py --inst 20 --rpg --map "<каталог игры>\Maps\Scenario\(4)WarChasers.w3m"
```

`--rpg` (в манифесте схемы — `"judge": false`) означает, что победа и поражение не определяются по правилам обычного матча: в RPG герои воскресают, и правила «не осталось зданий — проиграл» там тоже нет. Многие RPG-карты после загрузки останавливаются на экране «Нажмите любую клавишу, чтобы продолжить»; когда SDK видит, что «матч идёт, а игровые часы всё ещё на нуле», он сам нажимает пробел (`g.press_to_continue()` — только отправляет окну игры сообщение о нажатии клавиши и не перехватывает фокус).

## Пишем своего компаньона

```python
from openwar3.companion import Companion
from openwar3.talk import Talk

class MyBuddy(Companion):
    mode = "ally"                        # режим, см. таблицу ниже
    unit = "Hpal"                        # кого создать: любой четырёхсимвольный код, в том числе заданный картой
    nickname = "Светик"
    heal = ("holybolt", "AHhb", 0.55)    # (приказ заклинания, способность для изучения, лечить, когда здоровье хозяина ниже этой доли); None = не лечить
    follow_distance = 350
    talk = Talk(persona="Жизнерадостный юный паладин, любит подбадривать хозяина")
```

### Четыре режима

| mode | Кто компаньон | Пояснение |
|---|---|---|
| `ally` (по умолчанию) | Занимает свободный слот игрока и становится вашим **союзником** | Свой цвет и имя (в таблице счёта и на панели союзников показывается `nickname`); командовать им вы не можете, он воюет сам. Союз и общий обзор фреймворк настраивает автоматически |
| `own` | Создаётся **под вашим управлением** | Вы в любой момент можете командовать им вручную; когда вы им не занимаетесь, за вас им управляет ИИ |
| `adopt` | Берёт под контроль юнита, **который уже есть** на карте | Переопределите `adopt(g)`, чтобы он возвращал этого юнита (питомца или спутника, которого вам даёт карта) |
| `voice` | Юнита не создаёт, **только говорит** | Болтовня и напоминания; мир не меняет, поэтому работает и в многопользовательской игре |

Если свободного слота нет, `ally` автоматически переходит в `own`; в многопользовательской игре или если юнита создать не удалось — в `voice`.

> **Примечание**
>
> Компаньон в режиме `ally` ориентируется на «лучшего юнита в этом слоте прямо сейчас» (герои в приоритете), а не держится за одного конкретного юнита. В реальной проверке одна карта приняла компаньона за живого игрока, удалила паладина и выдала ему героя карты — компаньон просто взял этого героя под контроль и изучил способности, которые ему задала карта. Если герой погибает, компаньон по возможности воскрешает его на месте; если карта воскрешает героя сама — продолжает им пользоваться.

### Что он делает каждый тик

Проверяет условия по порядку и выполняет первое сработавшее:

| Порядок | Действие | Условие | Настройка |
|---|---|---|---|
| 1 | Отступление | Своё здоровье ниже 25% и рядом враги: отойти за спину хозяина | `retreat_at` |
| 2 | Лечение | Здоровье хозяина ниже порога, способность перезарядилась, расстояние не больше 900 | `heal` (None — выключить) |
| 3 | Помощь в бою | Рядом с хозяином враги: **кто бьёт хозяина > кого бьёт хозяин > ближайший** | `assist_radius` или переопределить `pick_target` |
| 4 | Следование | Слишком далеко от хозяина — догнать; если совсем далеко — бежать обратно, не ввязываясь в бой | `follow_distance`, `leash` |
| 5 | Болтовня | Когда врагов нет — одна фраза раз в 1 ~ 2.5 минуты | Таблица реплик |

«Враги» определяются по союзным отношениям в игре (обновляются раз в 20 секунд). В RPG-картах часто несколько союзных игроков, поэтому нельзя просто считать врагами всех игроков, кроме себя.

Переопределяемые хуки: `find_master` (кто хозяин; по умолчанию герой самого высокого уровня у локального игрока), `adopt`, `pick_target`, `on_poke` (хозяин щёлкнул по компаньону правой кнопкой), а также методы бота `on_start` / `on_tick` / `on_event` / `on_end`. Сколько раз компаньон лечил, помогал в бою, убивал, следовал, отступал, говорил и воскрешал, записывается в `self.stats` и выводится в конце.

### Как к нему обратиться

- **Команды чата**: наберите в чате `-follow` (за мной), `-stay` (стой здесь), `-heal` (вылечи сейчас) или `-hi` (поздороваться). Список команд задаётся в `commands`, реакции меняются переопределением `on_command`.
- **Правый клик по компаньону**: вызывает `on_poke`. В примере реакция такая: если у хозяина не полное здоровье — подлечить, иначе сказать что-нибудь.
- **Диалог с портретом**: приветствие, гибель хозяина, повышение уровня хозяина и возвращение компаньона звучат через встроенный в игру диалог с портретом (портрет внизу меняется на компаньона, на экране появляются субтитры); остальное — облачком над головой.
- **Панель состояния**: панель в левой части экрана с полоской здоровья компаньона, тем, чем он сейчас занят, настроением (радость / азарт / тревога / страх / грусть), числом убийств и лечений. Она рисуется на [холсте](https://war3ai.com/ru/docs/canvas/), поэтому безопасна и в многопользовательской игре.

### Реплики и локальные LLM

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

| Событие | Когда | Событие | Когда |
|---|---|---|---|
| `hello` | Только появился | `master_low` | У хозяина мало здоровья |
| `poke` | Хозяин щёлкнул по нему правой кнопкой | `master_levelup` | Хозяин повысил уровень |
| `fight` | Начался бой | `master_died` / `master_back` | Хозяин пал / воскрес |
| `kill` | Убил крипа (называет его имя) | `buddy_low` / `buddy_died` / `buddy_back` | У самого компаньона мало здоровья / он пал / вернулся |
| `healed` | Подлечил хозяина | `idle` / `item` | Болтовня / подобрал предмет |

В репликах можно использовать подстановки `{master}`, `{me}`, `{map}`, `{enemy}`, `{level}`, `{item}`; сами реплики правятся прямо в `talk.lines`, перезарядка — в `talk.cooldown`.

**Подключение локальной LLM**: `Talk(llm=LocalLLM(url, model))`, подходит любой OpenAI-совместимый API (LM Studio, Ollama…). Модель отвечает в фоновом потоке, и реплика звучит, только когда ответ пришёл; если модель не запущена, не уложилась по времени или вернула ошибку, звучит заготовленная реплика — игра не подвисает. Запросы уходят только на указанный вами локальный адрес, а содержат они то, что произошло в игре (как зовут хозяина, каких крипов он убил).

## Имена юнитов в пользовательских картах

Юниты, предметы и герои RPG-карт в основном созданы самой картой (четырёхсимвольные коды вроде `HC07`, `I00A`), и во встроенной таблице имён их нет. `g.map_data` читает файл карты текущего матча напрямую:

```python
md = g.map_data
md.name_of("HC07")        # 'Optimus Primo' — приоритет у имён, изменённых картой
md.hero_names("HC07")     # список собственных имён героя
md.hero_skills("OC10")    # способности, которые карта задала этому герою
md.tooltip("I00A")        # текст подсказки
```

Защищённые и оптимизированные карты (а таковы многие популярные RPG) не содержат стандартных файлов данных объектов — тогда имена читаются из текстовых данных карты. В проверке на этом компьютере все 38 RPG- и пользовательских карт разобраны успешно, в 37 из них найдены имена юнитов.

## Поделиться как схемой

Компаньон — это обычный подкласс `openwar3.Bot`, так что его можно оформить как [ИИ-схему](https://war3ai.com/ru/docs/schemes/) и поделиться с другими. В манифест добавляются ещё два поля:

```json
{"id": "my-buddy", "name": "Мой компаньон", "entry": "my_buddy.py", "fair": false, "judge": false}
```

`"fair": false` — нужно, чтобы пользоваться JASS-каналом (создавать юнитов, настраивать союзы); `"judge": false` — не определять победу и поражение по правилам обычного матча.

## Результаты проверки

2026-09-24, тестовый экземпляр, карта WarChasers, скорость 2×:

- Все 18 проверок JASS-канала пройдены: слоты игроков, преобразование юнитов в дескрипторы и обратно, вещественные возвращаемые значения, строковые аргументы, создание юнита в свободном слоте, настройка союза, переименование, удаление юнита; вызов из полосы игрока и неверное число аргументов корректно отклоняются.
- Компаньон: сам прошёл экран «Нажмите любую клавишу» → появился рядом с хозяином и поздоровался → вслед за хозяином зашёл в круг выбора героя, получил от карты героя и взял его под контроль → следовал за хозяином (на расстоянии 200 ~ 400) → бил крипов, после убийства сказал «Красиво!» → при малом запасе здоровья отступил → погиб, был воскрешён картой и продолжил следовать за хозяином.

## Что ещё не сделано

1. **Произвольный текст, который игрок пишет в чат, прочитать нельзя.** Фиксированные команды чата уже работают; чтобы компаньон по-настоящему свободно болтал с вами, нужно получать сам текст.
2. **Компаньон не понимает геймплей конкретной карты** (задания, магазины, сюжет). Он умеет общее: следовать, помогать в бою, лечить. Чтобы он понимал конкретную карту, опишите её в своём подклассе — `g.map_data` подскажет имена, `g.jass` вызовет любую функцию. Именно эта часть оставлена на ваше усмотрение.
