Документация Расширения игры

RPG-компаньон

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

Не только для обычных матчей. В 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. «Начать тест»: когда игра запустится, играйте сами в окне игры. Компаньон — паладин по имени «Светик» — появится рядом с вами.

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

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() — только отправляет окну игры сообщение о нажатии клавиши и не перехватывает фокус).

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

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ЛечениеЗдоровье хозяина ниже порога, способность перезарядилась, расстояние не больше 900heal (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. В примере реакция такая: если у хозяина не полное здоровье — подлечить, иначе сказать что-нибудь.
  • Диалог с портретом: приветствие, гибель хозяина, повышение уровня хозяина и возвращение компаньона звучат через встроенный в игру диалог с портретом (портрет внизу меняется на компаньона, на экране появляются субтитры); остальное — облачком над головой.
  • Панель состояния: панель в левой части экрана с полоской здоровья компаньона, тем, чем он сейчас занят, настроением (радость / азарт / тревога / страх / грусть), числом убийств и лечений. Она рисуется на холсте, поэтому безопасна и в многопользовательской игре.

Реплики и локальные 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 читает файл карты текущего матча напрямую:

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, так что его можно оформить как ИИ-схему и поделиться с другими. В манифест добавляются ещё два поля:

{"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 вызовет любую функцию. Именно эта часть оставлена на ваше усмотрение.