# RPG 玩伴

> 在 RPG / 自訂地圖裡幫玩家配一個 AI 夥伴：跟著你、幫你打怪、殘血時幫你補血、陪你聊天。四種形態，繼承一個類別、改幾個屬性，就是你自己的玩伴。

來源: https://war3ai.com/zh-tw/docs/companion/

不只是對戰。在 RPG、自訂地圖裡，你可以幫自己配一個 **AI 夥伴**：它跟著你走、幫你打怪，你殘血時幫你補血，沒事的時候陪你聊兩句 —— 台詞還能接本機 LLM。

**怎麼用由你自己決定。** 這件事拆成三層 API，從底到頂，哪一層都能直接用：

| 層 | 是什麼 | 適合 |
|---|---|---|
| **JASS 通道** `g.jass` | 地圖作者能用的 1291 個 JASS 函式，依名稱直接呼叫（造單位、設盟友、給物品、改名、顯示文字、復活英雄……） | 想自己打造玩法 |
| **便捷 API** | `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/zh-tw/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` 子類別，可以做成 [AI 方案](https://war3ai.com/zh-tw/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` 能呼叫任何函式。這正是留給你自己決定的部分。
