# RPG コンパニオン

> RPG / カスタムマップで、プレイヤーに AI の相棒をつけます。あなたについて歩き、敵との戦いを手伝い、HP が減れば回復し、話し相手にもなります。4 つのモードがあり、クラスを 1 つ継承していくつかの属性を変えるだけで、あなただけのコンパニオンになります。

出典: https://war3ai.com/ja/docs/companion/

対戦だけではありません。RPG やカスタムマップでは、自分に **AI の相棒**をつけられます。あなたについて歩き、敵との戦いを手伝い、HP が減れば回復してくれて、手が空いているときは話しかけてきます —— セリフはローカル LLM につなぐこともできます。

**どう使うかはあなた次第です。** この機能は 3 層の 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`：継承していくつかの属性を変えるだけで、追従、援護、回復、会話をこなす相棒になります | 相棒がほしい |

> **注意**
>
> **ローカル、または LAN で自分で立てた**ゲーム専用です。ユニット作成や同盟設定のような操作は、自分のマシンが一方的にワールドを変更するものです。シングルプレイ（コンピューターとの対戦）なら問題ありませんが、マルチプレイでは他のプレイヤーと同期がずれます。そのためマルチプレイでは、JASS チャネルは読み取り専用の関数だけを許可し、コンパニオンは自動的に「話すだけ」に切り替わります。

## 最速で始める：Farsight でワンクリック起動

1. **マップを選ぶ**：Farsight の「インスタンス」ページ →「次のゲームの設定」→ マップで、RPG マップを 1 つ選びます（ゲームディレクトリの `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 のまま」であることに気づくと、自分でスペースキーを 1 回押します（`g.press_to_continue()`。ゲームウィンドウにキー入力のメッセージを送るだけで、フォーカスは奪いません）。

## 自分のコンパニオンを書く

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

class MyBuddy(Companion):
    mode = "ally"                        # モード。下の表を参照
    unit = "Hpal"                        # 作るユニット：任意の 4 文字コード。マップ独自のものも可
    nickname = "ヒカリ"
    heal = ("holybolt", "AHhb", 0.55)    # (スペルのオーダー名, 習得するスキル, 主人の HP がこの割合を下回ったら回復)；None = 回復しない
    follow_distance = 350
    talk = Talk(persona="元気な小さなパラディン。主人を応援するのが大好き")
```

### 4 つのモード

| mode | コンパニオンの正体 | 説明 |
|---|---|---|
| `ally`（デフォルト） | 空いているプレイヤースロットを 1 つ使い、あなたの**同盟者**になる | 自分の色と名前を持ちます（スコアボードや同盟パネルに `nickname` が表示されます）。あなたからは操作できず、自分で戦います。フレームワークが自動で同盟 + 視界共有に設定します |
| `own` | **あなたの所有ユニット**として作る | いつでも手動で指揮できます。あなたが操作していないときは、AI が代わりに動かします |
| `adopt` | マップに**すでにある**ユニットを引き継ぐ | `adopt(g)` をオーバーライドしてそのユニットを返します（マップがくれたペットやお供など） |
| `voice` | ユニットは作らず、**話すだけ** | 話し相手、リマインダー。ワールドを変更しないので、マルチプレイでも使えます |

空きスロットがなければ、`ally` は自動で `own` に切り替わります。マルチプレイの場合やユニットを作れない場合は、自動で `voice` に切り替わります。

> **補足**
>
> `ally` モードのコンパニオンが追うのは「そのスロットにいま存在する最適なユニット」（ヒーロー優先）で、特定の 1 体に固定されるわけではありません。実測では、コンパニオンを本物のプレイヤーとして扱い、パラディンを削除してマップ独自のヒーローを支給したマップがありました —— コンパニオンはそのヒーローをそのまま引き継ぎ、マップがそのヒーローに設定したスキルも習得します。ヒーローが死んだら、その場での蘇生を優先します。マップ側が蘇生した場合は、そのまま使い続けます。

### ティックごとに何をするか

順番にチェックし、条件が成立した行動を取ります。

| 順番 | 行動 | 条件 | 調整項目 |
|---|---|---|---|
| 1 | 撤退 | 自分の HP が 25% 未満で、近くに敵がいる：主人の後ろまで下がる | `retreat_at` |
| 2 | 回復 | 主人の HP が設定値を下回り、スキルのクールダウンが明けていて、距離が 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` が呼ばれます。サンプルでの反応は、主人の HP が満タンでなければ回復を 1 回かけ、満タンなら一言話す、というものです。
- **ポートレート会話**：あいさつ、主人が倒れたとき、主人のレベルアップ、コンパニオンの復帰。この数種類のセリフは、ゲーム自身のポートレート会話で話します（下部のポートレートがコンパニオンに切り替わり、画面に字幕が出ます）。それ以外は頭上の吹き出しで表示します。
- **ステータスパネル**：画面左側のパネルに、コンパニオンの HP バー、いま何をしているか、気分（ご機嫌 / わくわく / 緊張 / 怖い / 悲しい）、撃破数と回復回数を表示します。[キャンバス](https://war3ai.com/ja/docs/canvas/) で描いているので、マルチプレイでも安全です。

### 会話とローカル LLM

`Talk` はイベントに応じてセリフを選び、頭上に吹き出しを出します。`voice` モードのときや吹き出しを出せないときは、画面の左下に表示します。各セリフはスキームのログにも書き込まれるので、何を話したかを後から確認できます。

| イベント | タイミング | イベント | タイミング |
|---|---|---|---|
| `hello` | 登場したとき | `master_low` | 主人の HP が少ない |
| `poke` | 主人に右クリックされた | `master_levelup` | 主人がレベルアップした |
| `fight` | 戦闘開始 | `master_died` / `master_back` | 主人が倒れた / 復活した |
| `kill` | 敵を 1 体倒した（倒した相手の名前を口にする） | `buddy_low` / `buddy_died` / `buddy_back` | コンパニオン自身の HP が少ない / 倒れた / 戻ってきた |
| `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 マップのユニット、アイテム、ヒーローの多くはマップ独自に作られたもので（4 文字コードは `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 の多く）には標準のオブジェクトデータファイルが含まれていないため、名前はマップ内のテキストデータから読み取ります。実測では、手元の RPG / カスタムマップ 38 個すべての解析に成功し、そのうち 37 個でユニット名を取得できました。

## スキームにして共有する

コンパニオンは `openwar3.Bot` のサブクラスにすぎないので、[AI スキーム](https://war3ai.com/ja/docs/schemes/) にして他の人と共有できます。マニフェストに 2 項目を追加します。

```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 体倒して「お見事！」と言う → HP が減って撤退 → 戦死後にマップによって蘇生され、再び追従。

## まだできていないこと

1. **プレイヤーが入力した任意のチャット文は読み取れません**。決まったチャットコマンドはすでに使えます。コンパニオンと本当に自由に会話するには、入力されたテキストそのものを取得する必要があります。
2. **コンパニオンは個々のマップの遊び方（クエスト、ショップ、ストーリー）を理解していません**。できるのは汎用的な追従、援護、回復です。特定のマップを理解させたいなら、サブクラスでそのマップ向けに書きます —— `g.map_data` で名前を調べられ、`g.jass` で任意の関数を呼べます。ここはまさに、あなた自身が決めるために残してある部分です。
