# RPG 玩伴

> 在 RPG / 自定义地图里给玩家配一个 AI 伙伴：跟着你、帮你打怪、残血时给你加血、陪你说话。四种形态，继承一个类、改几个属性就是你自己的玩伴。

来源: https://war3ai.com/docs/companion/

不只是对战。在 RPG、自定义地图里，你可以给自己配一个 **AI 伙伴**：它跟着你走、帮你打怪，你残血时给你加血，没事的时候陪你说两句 —— 台词还能接本地大模型。

**怎么用由你自己决定。** 这件事拆成三层接口，从底到顶，哪一层都能直接用：

| 层 | 是什么 | 适合 |
|---|---|---|
| **JASS 通道** `g.jass` | 地图作者能用的 1291 个 JASS 函数，按名字直接调（造单位、设盟友、给物品、改名、显示文字、复活英雄……） | 想自己造玩法 |
| **便捷接口** | `g.spawn`、`g.set_alliance`、`g.player_slots`、`g.show_text`、`g.map_data`：常用的几件事包好了 | 写自己的辅助脚本 |
| **玩伴框架** | `openwar3.companion.Companion` + `openwar3.talk.Talk`：继承一下、改几个属性，就是一个会跟随、助战、加血、聊天的伙伴 | 想要一个伙伴 |

> **注意**
>
> 只用于**单机、局域网自建**的游戏。造单位、设盟友这类操作是本机单方面改世界：单人局（和电脑打）没问题；多人局会让其他玩家不同步，所以多人局里 JASS 通道只放行只读的函数，玩伴自动退成「只说话」。

## 最快上手：在远见里一键开

1. **选地图**：远见「实例」页 →「下一局设置」→ 地图，选一张 RPG 地图（游戏目录 `Maps` 下的 `Scenario`、`Download` 里的都会列出来，比如 `(4)WarChasers`）。
2. **选方案**：实例卡片的「AI 方案」下拉里选 **玩伴示例（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 发现「在局里、但游戏时钟一直是 0」时会自己按一下空格（`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` | 造在**你名下** | 你随时能手动指挥它；你不管的时候，AI 替你操作 |
| `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`（主人右键点了玩伴），以及 Bot 的 `on_start` / `on_tick` / `on_event` / `on_end`。加血、助战、击杀、跟随、撤退、说话、复活的次数都记在 `self.stats` 里，结束时打印。

### 怎么叫它

- **聊天命令**：在聊天框打 `-follow` 跟着我、`-stay` 原地守着、`-heal` 马上加血、`-hi` 打招呼。改命令表就改 `commands`，改反应就覆盖 `on_command`。
- **右键点玩伴**：触发 `on_poke`。示例里的反应是：主人没满血就给主人加一口血，否则说句话。
- **头像对白**：打招呼、主人倒下、主人升级、玩伴回来，这几句会用游戏自己的头像对白说（底部头像换成玩伴，屏幕上出字幕），其余的冒头顶气泡。
- **状态面板**：屏幕左侧一块面板，显示玩伴的血条、正在干什么、心情（开心 / 兴奋 / 紧张 / 害怕 / 难过）、击杀和加血次数。它用 [画板](https://war3ai.com/docs/canvas/) 画，多人局也安全。

### 说话，和本地大模型

`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`。

**接本地大模型**：`Talk(llm=LocalLLM(url, model))`，任何 OpenAI 兼容接口都行（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` 子类，可以做成 [AI 方案](https://war3ai.com/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 倍速：

- JASS 通道 18 项检查全部通过：玩家槽、单位和句柄的往返换算、实数返回值、字符串参数、给空槽造单位、设盟友、改名、删单位；从玩家车道调、参数个数不对，都被正确拒绝。
- 玩伴：自己按过「按任意键继续」→ 出现在主人身边打招呼 → 跟进选英雄的能量圈、被地图发了英雄并接管 → 跟随（离主人 200 ~ 400）→ 打怪、打死一个说「漂亮！」→ 残血撤退 → 阵亡后被地图复活，接着跟。

## 还没做的

1. **读不到玩家打的任意聊天文字**。固定的聊天命令已经能用；要让玩伴真的陪你自由聊天，还需要拿到文字本身。
2. **玩伴不懂具体地图的玩法**（任务、商店、剧情）。它是通用的跟随、助战、加血；要懂某一张图，就在子类里按那张图写 —— `g.map_data` 能查名字，`g.jass` 能调任何函数。这正是留给你自己决定的部分。
