LUACN论坛

 找回密码
 加入我们

QQ登录

只需一步,快速开始

搜索
热搜: YJWOW MagicStone BoL
查看: 1481|回复: 127

【Ni辅助保姆级入门教程】四、Ni辅助脚本编写实战

  [复制链接]
发表于 2024-6-13 11:21:29 | 显示全部楼层 |阅读模式
本帖最后由 吾奶常煽赵子龙 于 2024-6-21 10:33 AM 编辑

上一篇我们讲了Ni脚本的简单结构。这一篇我们通过改写一个魔蜂的脚本来进一步熟悉下ni脚本该怎么写。
(这一篇需要有一定的编程基础,不然可能看不懂,这里篇幅有限,就不讲基础语法了)


下面是一个由魔蜂脚本改写的LR输出脚本。为了方便阅读,不会看懵,代码风格尽量保持魔蜂脚本的风格。直接上代码:


游客,如果您要查看本帖隐藏内容请回复




所有输出逻辑都在SmartAttack()函数里。上面的代码注释已经非常详细了,具体逻辑就不再赘述了,只解释几个ni的函数。


ni.debug.print:用于输出调试信息,当 ni.vars.debug = true 的时候,会在游戏的综合窗口里打印日志信息。
ni.player.enemiesinrange:获取玩家周围指定范围内的敌人。实际调用的是:ni.unit.enemiesinrange(unit, range)。用这个可以获取任何单位指定范围内的敌人。
ni.unit.threat(unit1, unit2):获取unit1对unit2的威胁值状态,-1表示没有威胁,也就是unit2没有进入战斗。返回值的具体含义可以参考官方函数UnitThreatSituation。

ni.player.runtext:用于执行宏命令,比如代码中的 ni.player.runtext("/cleartarget") 就是执行 /cleartarget 这个清除目标的宏。
ni.unit.isfacing(unit1, unit2, 90):检查unit2是否在unit1正前方左右90度的扇形范围内。
ni.player.lookat(unit):转向,面对unit。
ni.unit.isimmune(unit):判断指定单位是否免疫攻击。
ni.player.isstunned():判断玩家是否被眩晕了。

ni.player.los(unit):判断玩家和指定目标是否有视线碰撞(两者之间有没有卡视角)。
ni.spell.available(spell):判断指定技能是否准备好可以释放了。
ni.player.hasitem(item):判断玩家背包中是否有指定物品。
ni.player.itemcd(item):获取指定物品的cd时间。
ni.player.itemready(item):判断指定物品是否准备好可以使用了。

ni.player.useitem(item):使用背包中的指定物品。



其他ni的函数看不明白的可以去官方文档查看。


注意:官方文档更新不及时,如果发现api效果和文档描述不一致的时候,可以去 Loader.DLL.v0.0.65\addon\Core\ 下面的源码里查看具体的实现。


上面我们将输出逻辑全部写在了一个函数里,其实我们还可以利用queue顺序执行且可中断的机制,可以将逻辑拆成多个行为放到queue里。比如:
[Lua] 纯文本查看 复制代码
[/size]
[size=3]local format = format;

local p = "player"
local t = "target"


-- 搜寻附近的可攻击目标
-- maxDistance:最大距离范围,只搜寻该范围内的怪
function searchTarget(maxDistance)
    local hasTarget = ni.unit.exists(t);
    maxDistance = tonumber(maxDistance) or 40;

    if not hasTarget then
        ni.debug.print("目标搜寻:当前没有目标,寻找新目标");
    elseif UnitIsDeadOrGhost(t) then
        ni.debug.print("目标搜寻:目标死亡,寻找新目标");
    elseif not UnitCanAttack(p, t) then
        ni.debug.print("目标搜寻:目标不能攻击,寻找新目标");
    elseif not ni.player.los(t) then
        ni.debug.print("目标搜寻:目标不在视野中,寻找新目标");
    elseif ni.unit.isimmune(t) then
        ni.debug.print("目标搜寻:目标免疫攻击,寻找新目标");
    elseif ni.unit.threat(p, t) == -1 then
        ni.debug.print("目标搜寻:目标没有进入战斗,寻找新目标");
    elseif ni.unit.los(p, t) then
        -- 有可攻击的目标,后面会继续判断该目标是否是最近的目标
    end
    
    -- 找到最近的那个敌人
    local enemies = ni.player.enemiesinrange(maxDistance);
    local enemy = nil;
    for i = 1, #enemies do
        if ni.unit.los(p, enemies[i].guid) and ni.unit.threat(p, enemies[i].guid) ~= -1 then
            if enemy == nil or enemy.distance > enemies[i].distance then
                enemy = enemies[i];
            end
        end
    end
    
    if enemy == nil then
        ni.debug.print("目标搜寻:附近没有可攻击目标");
        
        if hasTarget then
            ni.player.runtext("/cleartarget");
        end
        return false;
    end
    
    if hasTarget then
        if UnitGUID(t) == enemy.guid then
            ni.debug.print("目标搜寻:当前目标就是最近目标,不用切换");
            return true;
        else
            ni.player.runtext("/cleartarget");
        end
    end
    
    -- 将最近的敌人设置为目标
    ni.player.target(enemy.guid);
    
    -- 面对敌人
    if not ni.unit.isfacing(p, t, 90) then
        --ni.player.moveto(enemy.guid)
        ni.player.lookat(t);
    end
    
    ni.debug.print(format("目标搜寻:搜寻到可攻击目标【%s】", enemy.name));
    return true;
end


-- 智能输出
local function SmartAttack()
    -- 人物死亡或灵魂状态
    if UnitIsDeadOrGhost(p) then
        return;
    end
    
    -- 在载具上或移动中
    if UnitInVehicle(p) or ni.unit.ismoving(p) then
        return;
    end

    -- 如果没进战斗
    if not UnitAffectingCombat(p) then
        return;
    end
    
    -- 切换目标
    local targetExist = searchTarget(50);

    -- 玩家当前血量
    local health = ni.player.hp();
    -- 是否有活着的宠物
    local hasPet = (UnitExists("pet") and not UnitIsDead("pet"));
    -- 当前蓝量
    local mana = ni.power.current(p);

    -- 生命低于80%时向目标释放胁迫
    if health < 80 and targetExist and hasPet and ni.player.petcd("胁迫") == 0 then
        ni.spell.cast("胁迫", t);
        return;
    end
    
    -- 没有目标时尝试切换到宠物的目标
    if not targetExist then
        local pt = "pettarget";
        if hasPet and UnitExists(pt) then
            if not UnitIsDeadOrGhost(pt) 
               and UnitCanAttack(p, pt)
               and ni.player.los(pt)
               and not ni.unit.isimmune(t) -- 免疫攻击
               and ni.unit.distance(p, pt) <= 31 then
                -- 切换到宠物的目标
                ni.player.target(pt);
            end
        end
        return;
    end
    
    -- 正在施法中
    if ni.unit.iscasting(p) or ni.unit.ischanneling(p) then
        return;
    end
    
    -- 强击光环
    if not ni.unit.aura(p, "强击光环") and ni.spell.available("强击光环") then
        ni.spell.cast("强击光环");
        return;
    end
    
    if mana <= 10 then
        -- 蓝量小于10%时切换到蝰蛇守护
        if not ni.unit.aura(p, "蝰蛇守护") and ni.spell.available("蝰蛇守护") then
            ni.spell.cast("蝰蛇守护");
            return;
        end
    elseif mana < 80 then
        -- 蓝量在10-80%时如果没有龙鹰守护也没有蝰蛇守护,则使用蝰蛇守护
        if not ni.unit.aura(p, "龙鹰守护") and not ni.unit.aura(p, "蝰蛇守护") and ni.spell.available("蝰蛇守护") then
            ni.spell.cast("蝰蛇守护");
            return;
        end
    else
        -- 蓝量高于80%切换到龙鹰守护
        if not ni.unit.aura(p, "龙鹰守护") and ni.spell.available("龙鹰守护") then
            ni.spell.cast("龙鹰守护");
            return;
        end
    end
    
    -- 释放误导(这里的误导是一个光环,和官服不一致)
    if hasPet and not ni.unit.aura(p, "误导") and ni.spell.available("误导") then
        ni.spell.cast("误导", "pet");
        return
    end
    
    -- 目标不在前方左右90度范围内时,转向面对目标
    if not ni.unit.isfacing(p, t, 90) then
        ni.player.lookat(t);
    end
    
    if ni.unit.debuffremaining(t, "毒蛇钉刺", p) <= 0 and ni.spell.available("毒蛇钉刺") then  
        ni.spell.cast("毒蛇钉刺", t);
        return;
    end
    
    local targetHealth = ni.unit.hp(t);
    if targetHealth < 20 and ni.spell.available("杀戮射击") then
        ni.spell.cast("杀戮射击", t);
        return;
    end
    
    if hasPet and ni.spell.available("杀戮命令") then
        ni.spell.cast("杀戮命令");
        return;
    end
    
    if ni.spell.available("奥术射击") then
        ni.spell.cast("奥术射击", t);
        return;
    end
 
    local isBoss = ni.unit.isboss(t);
    if isBoss then
        if ni.unit.debuffremaining(t, "猎人印记") <= 0 and ni.spell.available("猎人印记") then
            ni.spell.cast("猎人印记", t);
            return;
        end
    end
    
    if UnitPlayerControlled(t) then
        if ni.unit.debuffremaining(t, "瞄准射击") <= 0 and ni.spell.available("瞄准射击") then   
            ni.spell.cast("瞄准射击", t);
            return;
        end
    else
        if ni.spell.available("多重射击") then   
            ni.spell.cast("多重射击", t);
            return;
        end
    end
        
    if hasPet and ni.spell.available("狂野怒火") then
        ni.spell.cast("狂野怒火");
        return;
    end
        
    if isBoss then
        if ni.spell.available("急速射击") then
            ni.spell.cast("急速射击");
            return;
        end
        
        if ni.unit.buffremaining(p, "急速射击") <= 0 and ni.spell.available("准备就绪") then
            ni.spell.cast("准备就绪");
            return;
        end        
    end
    
    ni.spell.cast("稳固射击", t);
end


local needLoot = true;           -- 是否需要拾取
local lootToolName = "范围拾取"; -- 一键拾取道具名字


-- 自动拾取
local function AutoLoot()   
    -- 没有被标记为可以拾取
    if not needLoot then
        return;
    end
    
    -- 在坐骑上(且是移动中)或在载具中,则放弃拾取
    if (ni.unit.ismounted(p) and ni.unit.ismoving(p)) or UnitInVehicle(p) then
        needLoot = false;
        return;
    end
    
    -- 公共CD中或被眩晕了
    if ni.spell.gcd() or ni.player.isstunned() then
        return;
    end
    
    -- 使用一键拾取道具进行拾取
    if ni.player.hasitem(lootToolName) then
        ni.player.useitem(lootToolName);
    end
    needLoot = false;
end


local queue = {
    "Attack",
    "Loot"
};

local abilities = {
    ["Attack"] = SmartAttack,
    ["Loot"] = AutoLoot
};


local function CombatEventCatcher(event, ...)
    -- 处理战斗日志事件,当有目标死亡时标记可以进行一次范围拾取
    if event == "COMBAT_LOG_EVENT_UNFILTERED" then        
        local _, subevent, _, sourceName, _, _, destName = ...;
        if subevent == "UNIT_DIED" and destName ~= UnitName(p) then
            needLoot = true;
        end
    end
end

local function OnLoad()
    ni.vars.debug = true
    -- 启动脚本的时候注册游戏事件(不清楚游戏事件的可以先了解下wow的event机制)
    ni.combatlog.registerhandler("Hunter-Auto-Attack-Event", CombatEventCatcher);
end

local function OnUnload()
    -- 停止脚本的时候取消注册的游戏事件
    ni.combatlog.unregisterhandler("Hunter-Auto-Attack-Event");
end

ni.bootstrap.profile("智能输出-猎人", queue, abilities, OnLoad, OnUnload);

这样将整个输出逻辑拆分成多个小功能,看着更加的简单明了。



配置界面

上面脚本中有个自动拾取的功能。但是有时候我们不想自动拾取怎么办?是停止脚本还是临时修改脚本?
这时候可以增加一个配置界面,通过配置来开启/关闭自动拾取。见代码:
游客,如果您要查看本帖隐藏内容请回复



添加配置界面的步骤如下:
第一步:定义界面配置项内容,也就是上面代码中的items。其中:
            settingsfile:指定保存配置的文件名,将保存在 Loader.DLL.v0.0.65\addon\Settings\ 目录下面;
            { type = "title", text = "LR输出配置" }:type是"title",表示是一个标题(其实就是一个普通文本),text是界面上显示的文本内容;
            { type = "separator" }:type是"separator",表示是一个分割线;
            { type = "entry", ... }:type是"entry"的有多种样式,text是界面上显示的文字,tooltip是鼠标放上去后的提示文字,enabled是选项的开启/关闭状态(默认状态),key是该项配置的唯一key,后续将通过这个key来获取该配置的值。


第二步:创建配置界面,OnLoad函数里的:ni.GUI.AddFrame("Hunter-Auto-Attack-Ui", items) 表示用items的配置信息创建一个配置界面。第一个参数是一个key,后续用来销毁界面(见OnUnload)。


由于上面代码是在OnLoad中创建配置界面的,所以当脚本启动后才能看得见配置脚本。配置界面在屏幕中间顶部,鼠标放上去后配置界面就会显示出来。


第三步:获取配置(具体见上面的AutoLoot()函数)。调用上面的 GetSetting() 方法获取配置的值,参数就是每个配置项的key。

配置项目除了上面的entry,还有input和dropdown:
[Lua] 纯文本查看 复制代码
local queue = {
    "死亡检查",
    "切换目标",
    "召唤宠物",
    "守护检查",
    "输出",
};

local abilities = {
    ["死亡检查"] = function() if UnitIsDeadOrGhost("player") return true end end,
    ["切换目标"] = function() ... end,
    ["召唤宠物"] = function() ... end,
    ["守护检查"] = function() ... end,
    ["输出"] = function() ... end,
};



[Lua] 纯文本查看 复制代码
[/size]
[size=3]local format = format;

local p = "player"
local t = "target"

local items = {
    settingsfile = "Hunter.json",
    { type = "title", text = "LR输出配置" },
    { type = "separator" },
    { 
        type = "entry", 
        text = "自动拾取", 
        tooltip = "目标死亡后进行自动拾取。", 
        enabled = false, 
        key = "AutoLoot" 
    },
};


local function GetSetting(name)
    for k, v in ipairs(items) do
        if v.type == "entry"
        and v.key ~= nil
        and v.key == name then
            return v.value, v.enabled;
        end
        
        if v.type == "dropdown"
        and v.key ~= nil
        and v.key == name then
            for k2, v2 in pairs(v.menu) do
                if v2.selected then
                    return v2.value;
                end
            end
        end
        
        if v.type == "input"
        and v.key ~= nil
        and v.key == name then
            return v.value;
        end
    end
end;


-- 搜寻附近的可攻击目标
-- maxDistance:最大距离范围,只搜寻该范围内的怪
function searchTarget(maxDistance)
    ...
end


-- 智能输出
local function SmartAttack()
    ...
end


local needLoot = true;           -- 是否需要拾取
local lootToolName = "范围拾取"; -- 一键拾取道具名字


-- 自动拾取
local function AutoLoot()
    local _, autoLootEnabled = GetSetting("AutoLoot")
    if not autoLootEnabled then
        -- 没有开启自动拾取
        return;
    end
    
    -- 没有被标记为可以拾取
    if not needLoot then
        return;
    end
    
    -- 在坐骑上(且是移动中)或在载具中,则放弃拾取
    if (ni.unit.ismounted(p) and ni.unit.ismoving(p)) or UnitInVehicle(p) then
        needLoot = false;
        return;
    end
    
    -- 公共CD中或被眩晕了
    if ni.spell.gcd() or ni.player.isstunned() then
        return;
    end
    
    -- 使用一键拾取道具进行拾取
    if ni.player.hasitem(lootToolName) then
        ni.player.useitem(lootToolName);
    end
    needLoot = false;
end


local queue = {
    "Attack",
    "Loot"
};

local abilities = {
    ["Attack"] = SmartAttack,
    ["Loot"] = AutoLoot
};


local function CombatEventCatcher(event, ...)
    -- 处理战斗日志事件,当有目标死亡时标记可以进行一次范围拾取
    if event == "COMBAT_LOG_EVENT_UNFILTERED" then        
        local _, subevent, _, sourceName, _, _, destName = ...;
        if subevent == "UNIT_DIED" and destName ~= UnitName(p) then
            needLoot = true;
        end
    end
end

local function OnLoad()
    ni.vars.debug = true
    
    -- 启动脚本的时候注册游戏事件(不清楚游戏事件的可以先了解下wow的event机制)
    ni.combatlog.registerhandler("Hunter-Auto-Attack-Event", CombatEventCatcher);
    
    -- 添加配置界面
    ni.GUI.AddFrame("Hunter-Auto-Attack-Ui", items);
end

local function OnUnload()
    -- 停止脚本的时候取消注册的游戏事件
    ni.combatlog.unregisterhandler("Hunter-Auto-Attack-Event");
    
    -- 销毁配置界面
    ni.GUI.DestroyFrame("Hunter-Auto-Attack-Ui");
end

ni.bootstrap.profile("智能输出-猎人", queue, abilities, OnLoad, OnUnload);[/size]
[size=3]


另外entry还有其他用法:
[Lua] 纯文本查看 复制代码
-- 一个文本输入框

{ 
        type = "input", 
        value = "", 
        width = 225, 
        height = 65, 
        wordwrap = true, 
        key = "WhatToPrint" 
}

-- 值的读取方式:读取value中的值 
local text = [size=3]GetSetting([/size]"WhatToPrint"[size=3]);[/size]


在配置项可以显示图标,比如上面的 text = ni.spell.icon("乱射") .. " AOE配置" 就是在文字 AOE配置 前面添加乱射技能的图标。


更加详细的可以参考 Loader.DLL.v0.0.65\addon\Rotations\Generic\GUIExample.lua。




配置使用优化
上面在读取配置信息的时候使用的 GetSetting() 函数,是通过遍历items来查看对应key的值。还有另外一种方式:
[Lua] 纯文本查看 复制代码
-- 下拉选择框

{ 
        type = "dropdown", 
        menu = {
           -- selected = true 是默认选中项

            { selected = true,  value = 1, text = "跳跃" },
            { selected = false, value = 2, text = "向前移动" },
            { selected = false, value = 3, text = "向后移动" },
            { selected = false, value = 4, text = "向右平移" },
            { selected = false, value = 5, text = "向左平移" },
        }, 
        key = "ActionEvent" 
}

-- 值的读取方式:读取value中的值 
local value = [size=3]GetSetting([/size]"ActionEvent"[size=3]);[/size]




在 items 中可以添加一个 callback,当配置被修改后会将最新的值回调给GUICallback,然后在GUICallback中将最新的值保存起来,要使用的地方直接使用 enables.AutoLootEnabled 来判断就可以了:
[Lua] 纯文本查看 复制代码
{ 
        type = "entry", 
        text = ni.spell.icon("乱射") .. " AOE配置", 
        tooltip = "周围怪物数量达到设定值后使用|cFF00FF00乱射|r。", 
        enabled = false, 
        value = 3, 
        min = 2, 
        max = 10, 
        step = 1, 
        width = 40, 
        key = "AoE" 
}




配置项的key命名规则建议使用lua的变量命名规则:以字母或下划线开头,不包含空格。比如上面的 key = "AutoLootEnabled" 。这样我们就可以直接将key当做变量使用,比如:

[Lua] 纯文本查看 复制代码
local enables = {
     AutoLootEnabled = false,
};

local function GUICallback(key, item_type, value)
             if item_type == "enabled" then
                          enables[key] = value;
             end
end

local items = {
         settingsfile = "Hunter.json",
         callback = GUICallback,
         { type = "title", text = "LR输出配置" },
         { type = "separator" },
         { 
                  type = "entry", 
                  text = "自动拾取", 
                  tooltip = "目标死亡后进行自动拾取。", 
                  enabled = enables.AutoLootEnabled, 
                  key = "AutoLootEnabled" 
         },
};

如果包含了空格或者是中文,比如:key = "Auto  Loot  Enabled"。那么在使用的时候就只能这样:
[Lua] 纯文本查看 复制代码
local function AutoLoot()
    -- 直接使用 enables.AutoLootEnabled 进行判断
    if not enables.AutoLootEnabled then
        -- 没有开启自动拾取
        return;
    end
    
    ...
end







回复

使用道具 举报

发表于 2024-6-13 16:16:12 | 显示全部楼层
必须支持,这个好像很好用的样子
回复 支持 反对

使用道具 举报

发表于 2024-6-13 17:36:21 | 显示全部楼层
支持支持 感谢大佬分享
回复 支持 反对

使用道具 举报

发表于 2024-6-13 17:58:36 | 显示全部楼层
占个座占个座占个座占个座占个座
回复 支持 反对

使用道具 举报

发表于 2024-6-13 18:14:27 | 显示全部楼层
UI界面的问题能讲下不,还有看到原有的一些脚本,各种定义,搞得很晕,
还有就是关于判断主T和副T,以及如何使用的问题,一直搞不清
回复 支持 反对

使用道具 举报

 楼主| 发表于 2024-6-13 18:38:37 | 显示全部楼层
ijkzheng 发表于 2024-6-13 06:14 PM
UI界面的问题能讲下不,还有看到原有的一些脚本,各种定义,搞得很晕,
还有就是关于判断主T和副T,以及如 ...

T的判断是有API的:
[Lua] 纯文本查看 复制代码
for i = 1, #ni.members do
     if ni.members[i]:istank() then
        -- The unit is tank, do stuff...
    end
end


但是代码没法直接判断那个是主T,哪个是副T,这个需要在配置界面进行配置。然后通过以下代码获取:
[Lua] 纯文本查看 复制代码
local tank1, tank2, ... = ni.tanks();


如果想要自己手动获取配置的T信息,可以参考,addon\Core\healing.lua 里的 gettanks() 函数。

回复 支持 反对

使用道具 举报

发表于 2024-6-13 18:59:28 | 显示全部楼层
这个好像比魔蜂要复杂好多倍吧,都看不懂啊
回复 支持 反对

使用道具 举报

发表于 2024-6-14 09:47:25 | 显示全部楼层
为啥每次点开,楼主的头像都不一样 这么神奇么
回复 支持 反对

使用道具 举报

发表于 2024-6-14 10:40:57 | 显示全部楼层
感谢大佬指导~~~~~~~~
回复 支持 反对

使用道具 举报

发表于 2024-6-14 10:51:39 | 显示全部楼层
感谢大佬指导~~~~感谢大佬指导~~~~
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 加入我们

本版积分规则

小黑屋|手机版|Archiver|LUACN论坛

GMT+8, 2026-10-10 12:56 AM , Processed in 0.102705 second(s), 7 queries , Gzip On, Redis On.

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表