距离上一篇教程也过去一个多月了,是时候写一篇新教程了。上一篇文章主要介绍了如何选择阵容和技能的使用,在这一篇文章中,我们将介绍如何配置英雄出装和其相关的一些模块。

勘误

首先先纠正一下上一篇文章中的一些错误,在选择阵容部分,下面的table.remove函数中出现了一个错误,bad argument #2 to 'remove' (number expected, got string),就是第二个参数应该为数字而不是字符串。这个错误将导致已选择的英雄不能从英雄列表中移除,从而会选择出重复的英雄。

function Think()
    for i,id in pairs(GetTeamPlayers(GetTeam())) 
    do
        if(IsPlayerBot(id) and (GetSelectedHeroName(id)=="" or GetSelectedHeroName(id)==nil))
        then
            local num=hero_pool_my[RandomInt(1, #hero_pool_my)]        --取随机数
            SelectHero( id, num );        --在保存英雄名称的表中,随机选择出AI的英雄
            table.remove(hero_pool_my,num)        --移除这个英雄
        end
    end
end

所以中间几行应该修改为,这样从表中才能正确移除已经选择的英雄

            local i=RandomInt(1, #hero_pool_my)        --取随机数
            local heroname=hero_pool_my[i]        --获取英雄的名称
            SelectHero( id, heroname );        --在保存英雄名称的表中,随机选择出AI的英雄
            table.remove(hero_pool_my,i)        --移除这个英雄

出装系统简介

官方开发者维基中有着这样的说明:

物品购买
如果你只想重写在购买物品时的决策,你可以在文件名为item_purchase_generic.lua的文件中实现如下函数:

ItemPurchaseThink() - 每帧被调用。负责物品购买。
你也可以仅重写某个特定英雄的物品购买逻辑,比如火女(Lina),写在文件名为item_purchase_lina.lua的文件中。

dota 2 beta\game\dota\scripts\vscripts\bots_example V社的示例文件中,我们可以找到item_purchase_lina.lua文件,这就是为火女配置出装的文件。
打开这个文件,我们可以看到:

--这个表用来记录AI出装的顺序
local tableItemsToBuy = { 
                "item_tango",
                "item_tango",
                "item_clarity",
                "item_clarity",
                "item_branches",
                "item_branches",
                "item_magic_stick",
                "item_circlet",
                "item_boots",
                "item_energy_booster",
                "item_staff_of_wizardry",
                "item_ring_of_regen",
                "item_recipe_force_staff",
                "item_point_booster",
                "item_staff_of_wizardry",
                "item_ogre_axe",
                "item_blade_of_alacrity",
                "item_mystic_staff",
                "item_ultimate_orb",
                "item_void_stone",
                "item_staff_of_wizardry",
                "item_wind_lace",
                "item_void_stone",
                "item_recipe_cyclone",
                "item_cyclone",
            };


------------------------------------------------------------------------------------------

--用来实现基本的物品购买函数
function ItemPurchaseThink()

    local npcBot = GetBot();

    if ( #tableItemsToBuy == 0 )
    then
        npcBot:SetNextItemPurchaseValue( 0 );
        return;
    end

    local sNextItem = tableItemsToBuy[1];

    npcBot:SetNextItemPurchaseValue( GetItemCost( sNextItem ) );    --用于默认AI的相关函数

    if ( npcBot:GetGold() >= GetItemCost( sNextItem ) )            --判断金钱是否足够
    then
        npcBot:Action_PurchaseItem( sNextItem );                --购买物品
        table.remove( tableItemsToBuy, 1 );
    end

end

大家注意到,在上面那个记录出装顺序的表中,物品的名称似乎和平时用的有所区别,其实也可以在这个页面找到。

在上面这个函数中,V社为我们提供了购买物品的基本函数,但是这只能实现在泉水的商店中购买物品,如果要去神秘商店和边路商店购买物品怎么办呢?如果要配置多种出装风格应该怎么办?如何控制信使去购买物品?那就需要靠我们自己实现了。而且如果要配置物品出装,那么需要手动填写每一个配方。不过在V社最近的api更新之后,添加了一个函数GetItemComponents(sItemName),可以直接获取物品的配方。

下面,我们开始在V社的基本函数上逐渐添加更多的功能。

在神秘商店和边路商店购买装备

由于物品购买是一个通用的功能,所以我们可以在item_purchase_generic.lua中编写函数,以便重复调用。在默认AI的框架下,有两个模式用于物品购买secret_shop和side_shop,我们需要通过在主函数中控制前往神秘商店和边路商店购买物品。(尽管将选择前往哪个商店的功能耦合在一个函数中不好。更好的方法是在其它两个模式中重新编写GetDesire()函数。)

在游戏中,一个能在神秘商店购买的物品必定不能在基地商店购买,而边路商店的物品则有可能来自于其他商店。所以便能通过IsItemPurchasedFromSideShop(sNextItem)和IsItemPurchasedFromSecretShop(sNextItem )函数来判断一个物品能在哪被购买,进而决定下一个物品的购买位置。

        if(npcBot.secretShopMode~=true and npcBot.sideShopMode ~=true)
        then
            if (IsItemPurchasedFromSideShop( sNextItem ) and npcBot:DistanceFromSideShop() <= 2000)    --只有在离边路商店较近时才前往边路商店
            then
                npcBot.sideShopMode = true;
            end
            if (IsItemPurchasedFromSecretShop( sNextItem )) 
            then
                npcBot.secretShopMode = true;
            end
        End

然后我们在mode_secret_shop_generic.lua和mode_side_shop_generic.lua函数中分别实现控制英雄前往神秘商店和边路商店购买的操作。
在开发者维基中提到了以下内容:

模式重写

如果你想在已有模式体系下修改某个模式逻辑的需求和行为,例如修改对线模式(laning mode),你可以在文件名为mode_laning_generic.lua的文件中实现如下函数:
GetDesire() - 每帧都被调用,需要返回一个0到1之间的浮点值,该值标志了该模式有多大可能成为当前激活模式
OnStart() - 当该模式成为当前激活模式时调用
OnEnd() - 当该模式让出控制权给其他被激活模式时调用
Think() - 当该模式为当前激活模式时,每帧都被调用。负责发出机器人的行为指令。
你也可以仅重写某个特定英雄的模式逻辑,比如火女(Lina),写在文件名为mode_laning_lina.lua的文件中。如果你想在特定英雄的模式重写时调用泛用的英雄模式代码,请参考附件A的实现细节。

所以,我们便需要GetDesire()和Think()以重写该模式。当英雄离边路商店较近,而且下一件物品可以在边路商店购买时,英雄便不需要用信使或自己返回基地购买物品,而是直接前往边路商店。
mode_side_shop_generic.lua 边路商店模式

function GetDesire()

    local npcBot = GetBot();
    
    local desire = 0.0;
    
    if ( npcBot:IsUsingAbility() or npcBot:IsChanneling() )        --不应打断持续施法
    then
        return 0
    end
    
    if ( npcBot.sideShopMode == true and npcBot:GetGold() >= npcBot:GetNextItemPurchaseValue()) then
        local d=npcBot:DistanceFromSideShop()
        if d<2000
        then
            desire = (2000-d)/d*0.3+0.3;                    --根据离边路商店的距离返回欲望值
        end
    else
        npcBot.sideShopMode = false
    end

    return desire

end

function Think()
    
    local npcBot = GetBot();
    
    local shopLoc1 = GetShopLocation( GetTeam(), SHOP_SIDE );
    local shopLoc2 = GetShopLocation( GetTeam(), SHOP_SIDE2 );

    if ( GetUnitToLocationDistance(npcBot, shopLoc1) <= GetUnitToLocationDistance(npcBot, shopLoc2) ) then    --选择前往距离自己更近的商店
        npcBot:Action_MoveToLocation( shopLoc1 );
    else
        npcBot:Action_MoveToLocation( shopLoc2 );
    end
end

mode_secret_shop_generic.lua 神秘商店模式

function GetDesire()
    
    local npcBot = GetBot();

    local desire = 0.0;
    
    if ( npcBot:IsUsingAbility() or npcBot:IsChanneling() )        --不应打断持续施法
    then
        return 0
    end
    
    if ( npcBot.secretShopMode == true and npcBot:GetGold() >= npcBot:GetNextItemPurchaseValue()) then
        local d=npcBot:DistanceFromSecretShop()
        if d<3000
        then
            desire = (3000-d)/d*0.3+0.3;                --根据离边路商店的距离返回欲望值
        end
    else
        npcBot.secretShopMode = false
    end
  
    return desire

end

function Think()

    local npcBot = GetBot();

    local shopLoc1 = GetShopLocation( GetTeam(), SHOP_SECRET );
    local shopLoc2 = GetShopLocation( GetTeam(), SHOP_SECRET2 );

    if ( GetUnitToLocationDistance(npcBot, shopLoc1) <= GetUnitToLocationDistance(npcBot, shopLoc2) ) then    --选择前往距离自己更近的商店
        npcBot:Action_MoveToLocation( shopLoc1 );
    else
        npcBot:Action_MoveToLocation( shopLoc2 );
    end

end

然后需要在主购买函数中检查英雄是否已经到达神秘或野外商店附近,并购买物品。

        local PurchaseResult        --接收购买结果,后文会介绍
        
        if(npcBot.sideShopMode == true)
        then
            if(npcBot:DistanceFromSideShop() <= 200)
            then
                PurchaseResult=npcBot:ActionImmediate_PurchaseItem( sNextItem )
            end
        elseif(npcBot.secretShopMode == true)        --如果目标是神秘商店,则命令信使购买物品
        then
            if(npcBot:DistanceFromSecretShop() <= 200)
            then
                PurchaseResult=npcBot:ActionImmediate_PurchaseItem( sNextItem )
            end
            
            local courier=GetCourier(0)
            if(courier==nil)
            then
                BuyCourier()        --没有信使的话则会购买
            else
                if(courier:DistanceFromSecretShop() <= 200)        --信使已到达商店
                then
                    PurchaseResult=GetCourier(0):ActionImmediate_PurchaseItem( sNextItem )
                end
            end
        else
            PurchaseResult=npcBot:ActionImmediate_PurchaseItem( sNextItem )
        end

购买信使的函数

function BuyCourier()
    local npcBot=GetBot()
    local courier=GetCourier(0)
    if(courier==nil)        --购买小鸡
    then
        if(npcBot:GetGold()>=GetItemCost("item_courier"))
        then
            local info=npcBot:ActionImmediate_PurchaseItem("item_courier");
            if info ==PURCHASE_ITEM_SUCCESS then
                print(npcBot:GetUnitName()..' buy the courier',info);
            end
        end
    else                    --购买飞行信使
        if DotaTime()>60*3 and npcBot:GetGold()>=GetItemCost("item_flying_courier") and (courier:GetMaxHealth()==75) then
            local info=npcBot:ActionImmediate_PurchaseItem("item_flying_courier");
            if info ==PURCHASE_ITEM_SUCCESS then
                print(npcBot:GetUnitName()..' has upgraded the courier.',info);
            end
        end
    end
end

检查购买结果

npcBot:Action_PurchaseItem( sNextItem )函数会返回一个购买结果的值,其可能的值为:

    Item Purchase Results
    PURCHASE_ITEM_SUCCESS                 --成功购买
    PURCHASE_ITEM_OUT_OF_STOCK             --物品栏已满
    PURCHASE_ITEM_DISALLOWED_ITEM        --不被允许的物品 
    PURCHASE_ITEM_INSUFFICIENT_GOLD        --金钱不足 
    PURCHASE_ITEM_NOT_AT_HOME_SHOP        --不在基地商店 
    PURCHASE_ITEM_NOT_AT_SIDE_SHOP         --不在边路商店
    PURCHASE_ITEM_NOT_AT_SECRET_SHOP     --不在神秘商店
    PURCHASE_ITEM_INVALID_ITEM_NAME     --不存在的物品名称

在购买物品时,用一个变量记录购买结果,并随后检查,以免出现购买失败的情况。
local PurchaseResult=npcBot:ActionImmediate_PurchaseItem( sNextItem )

        if(PurchaseResult==PURCHASE_ITEM_SUCCESS)        --成功购买便从出装表中移除该物品
        then
            npcBot.secretShopMode = false;
            npcBot.sideShopMode = false;
            table.remove( ItemsToBuy, 1 )
        end
        if(PurchaseResult==PURCHASE_ITEM_OUT_OF_STOCK)    --物品栏已满,出售多余的物品
        then
            SellExtraItem()
        end
        if(PurchaseResult==PURCHASE_ITEM_INVALID_ITEM_NAME or PurchaseResult==PURCHASE_ITEM_DISALLOWED_ITEM)    --不存在的物品,移除该物品
        then
            table.remove( ItemsToBuy, 1 )
        end
        if(PurchaseResult==PURCHASE_ITEM_INSUFFICIENT_GOLD )    --金额不足(其实该情况也较少出现,因为我们已经在上面判断了金钱)
        then
            npcBot.secretShopMode = false;
            npcBot.sideShopMode = false;
        end
        if(PurchaseResult==PURCHASE_ITEM_NOT_AT_SECRET_SHOP)    --不在神秘商店,前往神秘商店
        then
            npcBot.secretShopMode = true
            npcBot.sideShopMode = false;
        end
        if(PurchaseResult==PURCHASE_ITEM_NOT_AT_SIDE_SHOP)        --不在边路商店(其实该情况不会出现,因为在边路商店的物品能在其他商店购买)
        then
            npcBot.sideShopMode = true
            npcBot.secretShopMode = false;
        end
        if(PurchaseResult==PURCHASE_ITEM_NOT_AT_HOME_SHOP)        --不在基地商店(也不会出现的情况,因为如果英雄不在家中,那么物品会购买于贮藏处)
        then
            npcBot.secretShopMode = false;
            npcBot.sideShopMode = false;
        end

两个效用函数,用于判断物品栏是否已满和出售特定物品

function SellSpecifiedItem( item_name )        --出售特定物品

    local npcBot = GetBot();

    local itemCount = 0;
    local item = nil;

    for i = 0, 14 
    do
        local sCurItem = npcBot:GetItemInSlot(i);
        if ( sCurItem ~= nil ) 
        then
            itemCount = itemCount + 1;
            if ( sCurItem:GetName() == item_name ) 
            then
                item = sCurItem;
            end
        end
    end

    if ( item ~= nil and itemCount > 5 and (npcBot:DistanceFromFountain() <= 600 or npcBot:DistanceFromSideShop() <= 200 or npcBot:DistanceFromSecretShop() <= 200) ) then
        npcBot:ActionImmediate_SellItem( item );
    end

end

function SellExtraItem()        --出售已经没有用处的物品
    if(GameTime()>15*60)
    then
        SellSpecifiedItem("item_faerie_fire")
        SellSpecifiedItem("item_enchanted_mango")
        SellSpecifiedItem("item_tango")
        SellSpecifiedItem("item_clarity")
        SellSpecifiedItem("item_flask")
    end
    if(GameTime()>20*60)
    then
        SellSpecifiedItem("item_stout_shield")
        SellSpecifiedItem("item_orb_of_venom")
    end
    if(GameTime()>30*60)
    then
        SellSpecifiedItem("item_branches")
        SellSpecifiedItem("item_bottle")
        SellSpecifiedItem("item_magic_wand")
        SellSpecifiedItem("item_magic_stick")
        SellSpecifiedItem("item_urn_of_shadows")
        SellSpecifiedItem("item_drums_of_endurance")
    end
end

信使控制

信使又称为小鸡,一直都是dota中有很大用处的单位,一直以来都是各方神圣追杀的对象。但是因为7.00版本刚出时,默认AI的信使控制机制有些问题,不知道现在修复了没有。而且为了与我们之前所写的神秘商店控制系统配合,所以需要重写信使控制模块。
在前文中我们提到过,信使控制的函数位于ability_item_usage_generic.lua的CourierUsageThink()中,所以我们只要在这个文件中重写此函数,便能完全地控制信使。

信使有以下几种状态和命令,参见开发者维基。通过ActionImmediate_Courier( hCourier, nAction ) 函数命令信使,int GetCourierState( hCourier )函数则能获取信使的状态。

Courier Actions and States

    COURIER_ACTION_BURST                --加速
    COURIER_ACTION_ENEMY_SECRET_SHOP    --前往敌方的神秘商店
    COURIER_ACTION_RETURN                --返回基地
    COURIER_ACTION_SECRET_SHOP            --前往神秘商店
    COURIER_ACTION_SIDE_SHOP            --前往边路商店
    COURIER_ACTION_SIDE_SHOP2            --前往边路商店
    COURIER_ACTION_TAKE_STASH_ITEMS        --拾起物品
    COURIER_ACTION_TAKE_AND_TRANSFER_ITEMS    --拾起并运送物品
    COURIER_ACTION_TRANSFER_ITEMS        --运送物品

    COURIER_STATE_IDLE                    --空闲
    COURIER_STATE_AT_BASE                --处于基地
    COURIER_STATE_MOVING                --移动
    COURIER_STATE_DELIVERING_ITEMS        --运送物品
    COURIER_STATE_RETURNING_TO_BASE        --返回基地
    COURIER_STATE_DEAD                    --信使已死亡

所以信使控制模块需要先检测信使的状态,然后再根据英雄是否有装备需要运送来决定信使的行为。
以下便是信使控制主函数

function CourierUsageThink()

    local npcBot=GetBot()
    local courier=GetCourier(0)        --获取信使句柄
    if(npcBot:IsAlive()==false or courier==nil or npcBot:IsHero()==false or npcBot:HasModifier("modifier_arc_warden_tempest_double"))    --判断使用者是不是真正的英雄
    then
        return
    end
    
    local state=GetCourierState(courier)        --获取信使状态
    local burst=courier:GetAbilityByName("courier_burst")
    local CanCastBurst=burst~=nil and burst:IsFullyCastable()        --检查信使加速能否使用
    
    if(state == COURIER_STATE_DEAD)        --信使已死亡
    then
        return
    end
    
    if(courier:GetHealth()/courier:GetMaxHealth()<=0.9)        --信使受到攻击,立刻回家
    then
        if(CanCastBurst)
        then
            npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_BURST)
        end
        npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_RETURN)
        return
    end
    
    if(state == COURIER_STATE_IDLE)        --信使处于空闲状态10秒以上则回家
    then
        if(courier.idletime==nil)
        then
            courier.idletime=GameTime()
        else
            if(GameTime()-courier.idletime>10)
            then
                npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_RETURN)
                courier.idletime=nil
                return
            end
        end
    end

    if(state == COURIER_STATE_RETURNING_TO_BASE and npcBot:GetCourierValue()>0 and not utility.IsItemSlotsFull())        --运送物品
    then
        npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_TRANSFER_ITEMS)
        return
    end
    
    if (state == COURIER_STATE_AT_BASE )         --从基地运送
    then
        if( npcBot:GetStashValue() >= 400)
        then
            if(courier.time==nil)
            then
                courier.time=DotaTime()
            end
            if(courier.time+1<DotaTime())
            then
                npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_TAKE_AND_TRANSFER_ITEMS)
                courier.time=nil
            end
            return
        end
    end
    
    if(state == COURIER_STATE_AT_BASE or state == COURIER_STATE_RETURNING_TO_BASE)        --前往神秘商店
    then
        if(npcBot.secretShopMode == true and npcBot:GetActiveMode() ~= BOT_MODE_SECRET_SHOP)
        then
            npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_SECRET_SHOP)
            return
        end
    end
    
    if(state == COURIER_STATE_DELIVERING_ITEMS)        --当信使正在运送物品时加速
    then
        if(CanCastBurst)
        then
            npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_BURST)
        end
    end

end

最后需要每个英雄调用主函数。例如ability_item_usage_zuus.lua。

function CourierUsageThink()
    ability_item_usage_generic.CourierUsageThink()
end

示例

为宙斯配置的出装:ability_item_usage_zuus.lua

require( GetScriptDirectory().."/item_purchase_generic" ) 

local ItemsToBuy = 
{ 
    "item_tango",
    "item_clarity",
    "item_branches",
    "item_branches",
    "item_faerie_fire",
    "item_bottle",
    "item_boots",
    "item_energy_booster",            --秘法鞋
    "item_mantle",
    "item_circlet",
    "item_recipe_null_talisman",    --无用挂件
    "item_mantle",
    "item_circlet",
    "item_recipe_null_talisman",    --无用挂件
    "item_helm_of_iron_will",
    "item_recipe_veil_of_discord",    --纷争
    "item_void_stone",
    "item_energy_booster",
    "item_recipe_aether_lens",        --以太之镜7.06
    "item_staff_of_wizardry",
    "item_void_stone",
    "item_recipe_cyclone",
    "item_wind_lace",                --风杖
    "item_point_booster",
    "item_staff_of_wizardry",
    "item_ogre_axe",
    "item_blade_of_alacrity",        --蓝杖
    "item_point_booster",
    "item_vitality_booster",
    "item_energy_booster",
    "item_mystic_staff",            --玲珑心
}

function ItemPurchaseThink()
    purchase.ItemPurchase(ItemsToBuy)        --将出装表传至主函数
end

总结

看完了这篇文章后,大家应该能够了解AI出装的相关功能了,虽然说这篇文章因为作者太懒拖了很久,不过还是要比V社好一点。V社说好的战役第二章迟迟没有推出,即将重新定义“七月下旬”。
下面上张图。
出装效果图

附件

1.示例代码