RPG companion
Give the player an AI companion in RPG and custom maps that follows you, helps you fight, heals you when you're low, and chats with you. Four modes; subclass one class and change a few attributes to make it your own.
It’s not just for melee games. In RPG and custom maps, you can give yourself an AI companion: it follows you around, helps you fight creeps, heals you when you’re low on health, and chats with you when nothing’s happening — its lines can even come from a local LLM.
How you use it is up to you. It’s split into three layers of APIs, from the bottom up, and you can use any of them directly:
| Layer | What it is | Good for |
|---|---|---|
JASS channel g.jass | The 1291 JASS functions available to map makers, called directly by name (create units, set alliances, give items, rename, show text, revive heroes…) | Building your own gameplay |
| Convenience APIs | g.spawn, g.set_alliance, g.player_slots, g.show_text, g.map_data: the common tasks, already wrapped | Writing your own helper scripts |
| Companion framework | openwar3.companion.Companion + openwar3.talk.Talk: subclass it, change a few attributes, and you have a companion that follows you, fights alongside you, heals and chats | Just wanting a companion |
Only for single-player and self-hosted LAN games. Creating units or setting alliances means your machine changes the world unilaterally: that’s fine in a single-player game (against the computer), but in a multiplayer game it would desync the other players. So in multiplayer games the JASS channel only allows read-only functions, and the companion automatically falls back to “talk only”.
Fastest start: one click in Farsight
- Pick a map: in Farsight, go to the “Instances & Setup” page → “Next game” → Map, and choose an RPG map (everything under
ScenarioandDownloadin the game folder’sMapsis listed, e.g.(4)WarChasers). - Pick a scheme: in the instance card’s “AI scheme” dropdown, choose Companion example (buddy) → “Select”.
- Start test: once the game is up, play it yourself in the game window. The companion — a paladin named “Sunny” — appears right next to you.
You can also use the command line:
python tools/play.py --bot brains/examples/buddy.py --inst 20 --rpg --map "<game folder>\Maps\Scenario\(4)WarChasers.w3m"
--rpg ("judge": false in the scheme manifest) means the game isn’t judged by melee rules: in RPGs heroes can revive, and there’s no “you lose when all your buildings are gone”. Many RPG maps stop at “Press any key to continue” after loading; when the SDK notices it’s “in a game, but the game clock is stuck at 0”, it presses Space by itself (g.press_to_continue(), which only sends a key message to the game window and doesn’t steal focus).
Write your own companion
from openwar3.companion import Companion
from openwar3.talk import Talk
class MyBuddy(Companion):
mode = "ally" # mode, see the table below
unit = "Hpal" # what to create: any four-character code, including the map's custom ones
nickname = "Sunny"
heal = ("holybolt", "AHhb", 0.55) # (cast order string, ability to learn, heal when the master's HP drops below this); None = no healing
follow_distance = 350
talk = Talk(persona="A cheerful young paladin who loves cheering on its master")
Four modes
| mode | Who the companion is | Notes |
|---|---|---|
ally (default) | Takes an empty player slot and becomes your ally | Has its own color and name (the scoreboard and allies panel show nickname); you can’t command it, it fights on its own. The framework sets up the alliance + shared vision automatically |
own | Created under your control | You can command it manually at any time; when you leave it alone, the AI controls it for you |
adopt | Takes over a unit already on the map | Override adopt(g) to return that unit (a pet or follower the map gives you) |
voice | Creates no unit, only talks | Chat and reminders; doesn’t change the world, so it works in multiplayer games too |
When there’s no empty slot, ally automatically falls back to own; in multiplayer games, or when the unit can’t be created, it falls back to voice.
An ally companion goes by “the best unit in that slot right now” (heroes first) instead of sticking to one specific unit. In testing, one map treated the companion as a real player, removed the paladin and handed out a map hero — the companion simply took over that hero and learned the abilities the map defined for it. When the hero dies, it prefers to revive it on the spot; if the map revives it by itself, it just keeps using it.
What it does each tick
It checks these in order and does the first one that applies:
| Order | Behavior | Condition | Tunable |
|---|---|---|---|
| 1 | Retreat | Its own HP is below 25% and enemies are nearby: fall back behind the master | retreat_at |
| 2 | Heal | The master’s HP is below the threshold, the ability is off cooldown, and the master is within 900 range | heal (None turns it off) |
| 3 | Assist | Enemies near the master: attacking the master > the master’s target > the nearest one | assist_radius, or override pick_target |
| 4 | Follow | Catch up when too far from the master; when far enough away, run straight back instead of staying in the fight | follow_distance, leash |
| 5 | Idle chat | When there are no enemies, say a line every 1–2.5 minutes | The line table |
“Enemies” are determined by the in-game alliance settings (refreshed every 20 seconds). RPG maps often have several allied players, so you can’t simply treat “every player except me” as an enemy.
Hooks you can override: find_master (who the master is; by default the local player’s highest-level hero), adopt, pick_target, on_poke (the master right-clicked the companion), and the Bot’s on_start / on_tick / on_event / on_end. The number of heals, assists, kills, follows, retreats, lines spoken and revives is recorded in self.stats and printed at the end.
How to call it
- Chat commands: type
-follow(follow me),-stay(hold position here),-heal(heal me now) or-hi(say hello) in the chat box. To change the command table, editcommands; to change the reactions, overrideon_command. - Right-click the companion: triggers
on_poke. In the example, the reaction is: if the master isn’t at full health, give them a heal; otherwise say something. - Portrait dialogue: greetings, the master falling, the master leveling up, and the companion coming back are spoken through the game’s own portrait dialogue (the portrait at the bottom switches to the companion and a subtitle appears on screen); everything else pops up as a speech bubble.
- Status panel: a panel on the left side of the screen showing the companion’s health bar, what it’s doing, its mood (happy / excited / nervous / scared / sad), and its kill and heal counts. It’s drawn with the canvas, so it’s safe in multiplayer games too.
Talking, and local LLMs
Talk picks lines by event and shows them as speech bubbles; in voice mode, or when a bubble can’t be shown, they appear in the bottom-left corner of the screen. Every line is also written to the scheme log, so you can check afterwards what it said.
| Event | When | Event | When |
|---|---|---|---|
hello | Just arrived | master_low | Master is low on HP |
poke | Master right-clicks it | master_levelup | Master levels up |
fight | A fight starts | master_died / master_back | Master falls / revives |
kill | Kills a creep (says the creep’s name) | buddy_low / buddy_died / buddy_back | Companion itself is low / falls / comes back |
healed | Healed the master | idle / item | Idle chat / picked up an item |
Lines can use placeholders such as {master}, {me}, {map}, {enemy}, {level} and {item}; to change the lines, edit talk.lines directly; the cooldown is in talk.cooldown.
Connecting a local LLM: Talk(llm=LocalLLM(url, model)) works with any OpenAI-compatible API (LM Studio, Ollama…). The model answers in a background thread, and the line is spoken only once the answer comes back; if the model isn’t running, times out or errors, a fixed line is used instead, so the game never stalls. Requests go only to the local address you provide, and contain what happened in the game (the master’s name, which creeps were killed).
Unit names in custom maps
Most units, items and heroes in RPG maps are created by the map itself (four-character codes like HC07, I00A), so they aren’t in the built-in name table. g.map_data reads the current game’s map file directly:
md = g.map_data
md.name_of("HC07") # 'Optimus Primo' — names changed by the map take priority
md.hero_names("HC07") # list of proper names
md.hero_skills("OC10") # abilities the map defines for this hero
md.tooltip("I00A") # tooltip text
Protected and optimized maps (many popular RPGs) don’t include the standard object data files, so names are read from the map’s text data instead. In testing, all 38 RPG / custom maps on this machine were parsed successfully, and unit names were found in 37 of them.
Share it as a scheme
A companion is just an openwar3.Bot subclass, so you can package it as an AI scheme and share it with others. Add two more fields to the manifest:
{"id": "my-buddy", "name": "My companion", "entry": "my_buddy.py", "fair": false, "judge": false}
"fair": false: needed to use the JASS channel (create units, set alliances); "judge": false: don’t judge wins and losses by melee rules.
Test log
2026-09-24, test instance, WarChasers map, 2× speed:
- All 18 JASS channel checks passed: player slots, round-trip conversion between units and handles, real return values, string arguments, creating a unit in an empty slot, setting alliances, renaming, removing units; calls from a player lane and wrong argument counts were both correctly rejected.
- Companion: got past “Press any key to continue” by itself → appeared next to the master and said hello → followed into the circle of power for picking heroes, was given a hero by the map and took it over → followed (200–400 from the master) → fought creeps and said “Nice!” after a kill → retreated when low on HP → died, was revived by the map, and kept following.
Not done yet
- It can’t read arbitrary chat text the player types. Fixed chat commands already work; for the companion to really chat freely with you, we still need access to the text itself.
- The companion doesn’t understand a specific map’s gameplay (quests, shops, story). What it does is generic: following, assisting, healing. To make it understand a particular map, write that into your subclass for that map —
g.map_datacan look up names, andg.jasscan call any function. That’s exactly the part left for you to decide.