文档 玩法扩展

界面与输入

画板上的按钮、选项卡能点,悬停自动高亮;登记热键、点地面选位置、读鼠标指着哪儿、知道本机玩家选中了谁。点击、热键、放技能、聊天全文、玩家离开,都进事件流。

画板 画出来的东西,现在能点了。运行时接管游戏窗口的输入,外部程序可以:

能力一句话游戏收得到吗
可点的画板项按钮、选项卡、面板:点中发 ui.click,悬停自动高亮点在按钮上的这一下收不到
热键登记 F5、ctrl+shift+Q 这样的组合,按下发 hotkey可选吞掉(连同它生成的字符)
点地面点在世界上发 mouse.world,带地面坐标可选吞掉(「点一个位置放塔」)
鼠标位置每帧更新:屏幕像素、指着的地面点、悬停的画板项——
选择本机玩家选中了谁;一变就发 selection.changed——

全部是本机输入 + 本机绘制:不进指令流,多人局也安全。但回调里要是改了世界(刷单位、改属性),那还是只能单人局。

Python:g.ui

ui = g.ui                                                   # 第一次用时运行时接管窗口输入
ui.button("shop", "买一瓶药(50 金)", screen=(40, 300), on_click=lambda g, ev: buy(g))
c = ui.choice("升级了!选一个奖励", [("力量 +5", "更能扛"), ("攻速 +20%", "更能打"), ("召唤狼", "多一个帮手")],
              pause=True, on_pick=lambda g, i: give(g, i))  # 屏幕中间一排卡片;pause=True 选的时候游戏暂停
i = c.wait(timeout=30)                                      # 也可以阻塞等(期间照样泵事件,不丢)
ui.hotkey("F5", lambda g, ev: g.say(hero, "收到!"))        # 默认吞掉
ui.hotkey("ctrl+shift+Q", on_press=..., swallow=False)
ui.mouse(on_click, capture=True, buttons=("left", "right"))  # 抓地面点击:左键、右键都报,并吞掉
xy = ui.pick_point("点地面:塔建在哪?")                    # 阻塞版:下一次左键点地面 -> (x, y);Esc 或超时 -> None
ui.cursor()                                                 # {'screen': (x, y), 'world': (x, y, z) 或 None, 'hover': 'shop'}
ui.toast("第 3 波来了!", seconds=3)
ui.close()                                                  # 收起自己的控件和热键;没有别的程序在用输入时才交还窗口输入
g.close()                                                   # 或者整个断开(也可以写 with Game(...) as g:)

回调的参数是 (g, ev),在你调 g.events() 时触发 —— Bot 和玩法模组的运行器每拍都会调。没给回调的点击进 ui.clicks。回调里抛了异常只记日志,不影响别的回调和事件。

画板底层也能直接用:g.canvas.text(..., clickable=True, hover=颜色),点击从事件流里收,ev.key 就是画的时候给的 key。画可点的元素会自动打开输入,不用先碰 g.ui。

热键写法:F1 ~ F24、A ~ Z、0 ~ 9、numpad0 ~ numpad9、space enter esc tab backspace insert delete home end pageup pagedown left up right down,前面可以加 ctrl+、shift+、alt+。

不带修饰键的字母和数字会和聊天输入、游戏快捷键打架。优先用 F5 ~ F8 这类游戏没占的键,或者组合键。

新事件

g.events() 里多了这些(完整字段见 W3P 协议):

kind什么时候便捷字段
ui.click可交互的画板项被点.key 画板 key、.button('left' / 'right')、.mods 修饰键
ui.hover鼠标移进 / 移出画板项.key(移出时是 None)
hotkey登记的热键按下.key 热键写法、.mods
mouse.world开了地面点击时,点在世界上.x .y 地面坐标、.button、.value(1 = 被吞了)
selection.changed本机玩家的选择变了用 g.selection() 取单位
spell.cast单位放了技能(技能开始冷却).spell 四字码、.b 等级、.value 冷却秒、.x .y 施法点
message屏幕消息框里出了一条.text 全文、.frame 哪个框、.chat(是聊天时)
player.left玩家离开或被判负移除.player
game.ended离开对局——

聊天和屏幕消息

玩家在聊天框打的字,直接从 message 事件的 .chat 读:

for ev in g.events():
    if ev.kind == "message" and ev.chat and ev.chat["text"] == "-follow":
        ...                                    # ev.chat = {'channel': '所有人', 'sender': '玩家名', 'text': '-follow'}

g.messages() 另有一份独立的游标:游戏提示(「需要更多的农场」「不能在那里建造」)也在里面。写 Bot 时用它就知道一条命令为什么没做成。

从别的语言用

  • 网关:ui.button、ui.choice、ui.hotkey、ui.mouse、ui.cursor 这些方法在 网关 上同名可用。远端给不了回调函数,点击和热键从事件推送里收(ui.click 事件带 key)。
  • 直接写共享内存:先发语义命令 input_enable(W3P 操作码 74),运行时开始接管输入;在输入块 Local\War3Input_<pid> 里你写热键表和鼠标开关,它回写鼠标位置、指着的地面点和悬停项。画板项的标志位 0x40 表示「可交互」。布局见 W3P 协议。

好几个程序同时用

模组、远见、MCP、网关的每个会话可能同时往一局游戏里放按钮、登记热键,互不干扰:

  • 每个程序登记自己的热键和地面点击开关,SDK 把大家的合并成一张表交给运行时。同一个键只留一条,事件发给所有人,各自按键认自己的热键;
  • ui.close() 只撤自己的,最后一个程序走了才交还窗口输入;
  • 程序被强行结束、没来得及收尾:运行时每 2 秒检查一次,登记过的程序全都退出了,就清掉它们留下的热键和地面点击拦截,它们画的按钮也不再拦点击。

实测

2026-09-25,演练实例上的实机核对 16/16:

  • 点按钮 → ui.click + 回调,运行时的拦截计数 +1(游戏没收到这一下);点按钮外面不触发;
  • F6 → hotkey;点地面 → mouse.world(被吞掉);
  • 刷一个圣骑士、选中它 → selection.changed,g.selection() 对得上;放神圣护甲 → spell.cast('AHds', 1, 35.0);
  • 地图文字 → message;聊天 → message,.chat 解析出发言人和内容;
  • 判负电脑 → player.left;结束对局 → game.ended。

真人鼠标点按钮、悬停高亮也逐一看过。

边界和注意

  • 位置用的是真鼠标:游戏自己按系统光标读位置,所以悬停、cursor() 反映的是真鼠标。拦截只管按键。
  • 画在鼠标指针下面:魔兽把指针当作画面的一部分每帧画进去。画板和头顶气泡都在游戏画指针的那一步之前画:盖住游戏界面,被指针盖住。这一帧没画指针(藏起来或过场)时才退回最后一步画。
  • 系统缩放:自己写测试、用窗口消息投递点击时,不感知 DPI 的进程投过去的坐标会被系统放大(150% 缩放时实测 ×1.5)。测试程序先声明感知 DPI。真人点击不受影响。
  • 第一次要预热字体,约 1 秒。这期间按钮还没画出来,点不中。
  • 不在对局里时不报地面点击:主菜单和结算页上 mouse.world 不发,也不吞。
  • 按下时被吞掉的那一下,松开前切到别的程序或鼠标拖出了窗口,也会复位,不会连下一次松开也吞掉。
  • 1.27 没有新建游戏界面框的函数(1.31 才有):这里的按钮、卡片都是运行时画的,样式随意,但不会出现在游戏自己的菜单层级里。