文档 玩法扩展

RPG 玩伴

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

不只是对战。在 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. 开始测试:游戏起来后,你自己在游戏窗口里玩。玩伴 —— 一个叫「小圣」的圣骑士 —— 会出现在你身边。

也可以用命令行:

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(),只往游戏窗口发按键消息,不抢焦点)。

写一个自己的玩伴

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。示例里的反应是:主人没满血就给主人加一口血,否则说句话。
  • 头像对白:打招呼、主人倒下、主人升级、玩伴回来,这几句会用游戏自己的头像对白说(底部头像换成玩伴,屏幕上出字幕),其余的冒头顶气泡。
  • 状态面板:屏幕左侧一块面板,显示玩伴的血条、正在干什么、心情(开心 / 兴奋 / 紧张 / 害怕 / 难过)、击杀和加血次数。它用 画板 画,多人局也安全。

说话,和本地大模型

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 直接读当前这一局的地图文件:

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 方案 分享给别人。说明书里多写两项:

{"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 能调任何函数。这正是留给你自己决定的部分。