本帖最后由 吾奶常煽赵子龙 于 2024-6-24 01:23 PM 编辑
前面几篇讲了Ni的一些入门知识。这一篇来讲讲通过Ni如何实现地图传送功能。
游戏背景
主城里有个球形的传送NPC(如图):
(图1)
NPC信息为:
(图2)
假设我们要传送到新手副本下的死亡矿井。步骤是首先和这个NPC对话,然后依次选择【新手副本】、【死亡矿井】:
(图3)
(图4)
我们来看看用Ni该怎么做。
第一步,移动到NPC位置
要使用这个NPC的传送功能,首先肯定是要靠近这个NPC(假设当前我们已经在主城了)。核心代码如下:
[Lua] 纯文本查看 复制代码
-- 将传送员设置为玩家的当前目标
ni.player.target("传送员");
-- 向目标移动
ni.player.moveto("target");
这里我们直接以NPC的名字作为目标移动。大多数情况下这样是没问题的。
但是某些时候,游戏数据有可能没有完整加载(比如某些差点的sf),NPC的名字看到的可能是“未知目标”。这种情况下通过名字处理就会出问题。解决办法是用NPC的ID(比如上面图二里这个NPC的ID是60000),具体用法为:
[Lua] 纯文本查看 复制代码
-- 通过NPC的ID,获取NPC的GUID
-- NPC在玩家附近才能获取得到
local function GetNpcGUIDById(npcId)
-- ni.objects 会自动缓存玩家周围一定范围内的单位信息,我们直接从ni.objects中遍历查找就可以了
-- ni.unit.id()用于获取指定单位的ID
for guid, obj in pairs(ni.objects) do
if type(guid) == "string" and ni.unit.id(guid) == npcId then
return guid;
end
end
end
-- 根据NPC的ID获取该NPC的GUID
local guid = GetNpcGUIDById(60000);
-- 直接通过npc的guid进行移动
ni.player.moveto(guid);
上面这样直接通过目标的guid进行移动,就可以避免“未知目标”的问题。
wow中每个单位的GUID都是唯一的。官方也有函数可以获取指定单位的GUID,比如获取目标单位的guid:guid = UnitGUID("target")。
第二步:和NPC交互
然后就是和npc对话了:
[Lua] 纯文本查看 复制代码
ni.player.interact(guid);
当然,这里的参数也可以是目标(具体看ni的官方API文档):
[Lua] 纯文本查看 复制代码
ni.player.interact("target");
这样就打开了上面的图3了。
第三步:选择【新手副本】、【死亡矿井】
这个该怎么选呐?这里介绍一个命令:/fstack。它用来显示游戏中各窗口、各组件的名字。比如我们在游戏的聊天窗口输入这个命令后,将鼠标放到上面图3的窗口上,就会有如下显示:
上面会有一个“框架栈”的提示窗(显示在游戏界面的上下左右四个角之一位置)。几个带编号的信息里,最下面是一个<0> UIParent,这个是我们游戏的主界面(可以理解为游戏中的所有组件都是这个窗口的子元素)。UIParent上面的GossipFrame就是我们这个窗口的名字(后续会用到)。我们继续将鼠标放到“新手副本”上面,就会看到下面的提示:
可以看到“新手副本”的组件名字是“GossipTitleButton1”(后续会用到),对于GossipTitleButtonX的选项可以用SelectGossipOption(i)函数来进行选择,我们这里“新手副本”是第一个选项,所以应该是:
[Lua] 纯文本查看 复制代码
SelectGossipOption(1);
同样,第二页的“死亡矿井”是第二项,那么就应该是:
[Lua] 纯文本查看 复制代码
SelectGossipOption(2);
按照上面几步,就可以传送到“死亡矿井”了。完整代码如下:
上面的代码里用数字标注了大致的执行顺序。
这里我们用到了Ni的几个关键函数:
ni.player.moveto(guid):向指定单位移动;
ni.player.interact(guid):和指定单位交互(目标是友方npc就是对话,目标是矿就是挖矿,目标是草药就是采药,目标是鱼漂就是钓鱼等等);
ni.unit.id(guid):获取指定单位的ID;
补充知识
在某些情况下我们可能想要遍历当前显示的所有GossipTitleButton,假设又不确定数量有多少,该怎么处理呐?通过上面我们可以看出GossipTitleButton1、GossipTitleButton2这些其实都是一个全局变量。在wow里全局变量除了可以直接访问之外,还可以通过_G访问。比如我们想要遍历当前的所有GossipTitleButton,那么可以这么做:
[Lua] 纯文本查看 复制代码
-- 通过NPC的ID,获取NPC的GUID
-- NPC在玩家附近才能获取得到
local function GetNpcGUIDById(npcId)
-- ni.objects 会自动缓存玩家周围的单位信息,我们直接从ni.objects中遍历查找就可以了
-- ni.unit.id()用于获取指定单位的ID
for guid, obj in pairs(ni.objects) do
if type(guid) == "string" and ni.unit.id(guid) == npcId then
return guid;
end
end
end
-- 这个加不加看情况,某些服传送有延迟,就需要加一下
local hasTransmitting = false;
local function Test()
-- 如果在传送了(地图加载),则只检查是否传送到了副本门口,不再重复执行下面的逻辑了。
if hasTransmitting then
if GetSubZoneText() == "死亡矿井" then
print("传送成功,停止脚本");
-- 6: 停止通用脚本
ni.togglegeneric(ni.vars.profiles.generic);
hasTransmitting = false;
end
return;
end
-- 2: 如果正在移动中(向NPC移动)就暂时不处理,直接return
if ni.player.ismoving() then
return;
end
-- 通过传送NPC的ID,获取该单位的GUID
local guid = GetNpcGUIDById(60000);
-- 1: 判断和传送NPC的距离,如果大于1码就向传送NPC移动
if ni.player.distance(guid) > 1 then
ni.player.moveto(guid);
return;
end
-- 3: 判断如果传送界面没有打开,就和npc对话
if not GossipFrame or not GossipFrame:IsVisible() then
-- 和传送NPC对话
ni.player.interact(guid);
-- 界面打开有延迟,这里让脚本延迟个0.5秒
ni.rotation.delay(0.5);
return;
end
-- 走到这里说明传送界面已经打开了
-- 判断第一个选项是否显示了,但是由于第一页和第二页都有第一个选项,所以还需要进一步判断
if GossipTitleButton1:IsVisible() then
-- 获取第一个选项的文字
local text = GossipTitleButton1:GetText();
-- 4: 如果文字里包含里"新手副本",就选择这个选项
-- 注意这里用的是包含,而不是等于,因为text里还可能包含其他信息,比如图标
if string.find(text, "新手副本") then
SelectGossipOption(1);
ni.rotation.delay(0.5);
return;
end
end
-- 判断第二个选项是否显示了,GossipTitleButton后面的数字是几就代表第几项
if GossipTitleButton2:IsVisible() then
-- 获取第二个选项的文字
local text = GossipTitleButton2:GetText();
-- 5: 为了保险起见,我们还是判断一下选项的文本内容是否包含了死亡矿井
if string.find(text, "死亡矿井") then
SelectGossipOption(2);
-- 传送后我们加一个标志,让上面的代码不会重复执行
hasTransmitting = true;
end
end
end
local queue = {
"进入死亡矿井"
};
local abilities = {
"进入死亡矿井" = Test
};
|