Methods in Lua Abilities and Items

edited August 2015 in Drafts

Methods in Lua Abilities and Items.

For this tutorial we don't do hand holding, I suggest using my lua item tutorial or the valve provided examples if you are just starting lua. In this tutorial we discus advanced uses for ability_lua and item_lua baseclasses. Most of the stuff is the same for both but when there is difference then I will mention it. Since the items are essentially abilities I will keep referring to them as such to limit repetition. Lets Start!


-- Basic Setup --

There is one key thing that ties lua abilities to game. USE SAME NAME EVERYWHERE!
When you have ability you refere to as "ability_super_power" in your npc_abilities_custom.txt, you have to use the same name as the class table of your ability. With items it's also important to use the same name for your texture name (though without the 'item_' in the png name) so it shows up in the shop correctly. This is true for all items, not just for lua based.
Ability doesn't need anything but the declaration of the class table. All other parts of the script are completely optional. It also helps out to organize your abilities so you don't get the same cluster-fuck of files as in the lua ability example valve provided. Better example of organizing is in the rpg_example from valve.
It's good to use 'special values' that you define in the npc_abilities_custom.txt file rather than hard code values. This helps if you want to balance the ability later and also help on creating tool tips to look sensible.
To refer to special values of the ability (defined in npc_abilities_custom.txt), you can use:

 self:GetSpecialValueFor("super_bonus")

If you need to get special value for diferent level than the ability's current one:

 self:GetLevelSpecialValueFor("super_bonus", 3) --super_bonus for level 3

2 - Override Funcions

This is bread and butter of lua based abilities. You can almost every function ability has and even create your own.
The problem you might face is actually finding the function name (as valve has not exposed them at all or the function is not listed.) with good example of 'GetAOERadius()' not being listed in official API page but still working just fine. Another problem might be that the function is only called when UI needs updating. For example GetManaCost is only updated when you select the unit after having selected different unit. (and in case of items the value for mana cost (in UI) is never updated from lua but the item still tells you that you don't have enough mana to cast it if it doesn't reach the updated mana cost.
Creating your own functions into the ability is useful when you want to use some part of code often or just want to simplify some conditional event.
Good references for the functions available:
https://developer.valvesoftware.com/wiki/Dota_2_Workshop_Tools/Scripting/API#CDOTA_Ability_Lua
https://developer.valvesoftware.com/wiki/Dota_2_Workshop_Tools/Scripting/API#CDOTABaseAbility
Few things to take heed in making lua abilities and modifiers is that you can always refer to the particular instance of ability or modifier with 'self' and if you want to refer to the ability in modifier you can use 'self:GetAbility()' and for the unit the modifier is attached to you can use 'self:GetParent()'
Another thing you can refer is the base values already defined by baseclass or by your npc_abilities_custom.txt. You do this with 'self.BaseClass.'.
Example:

 return self.BaseClass.GetCooldown( self, nLevel ) -- Remember to define nLevel as the current level.


-- Basics of Dynamic Values --

Almost every part of the ability can be dynamic. We can refer to game events like current mana of player for cooldown or use GetSystemTime() to connect the inevitability of real world with the damage your chain lightning will do.
Most common use though for this are commented on this example:

function modifier_super_power:GetModifierPhysicalArmorBonus()
    local iBonus = self:GetSpecialValueFor("super_bonus")
    local iNight = self:GetSpecialValueFor("night_bonus")
    local iDay = self:GetSpecialValueFor("day_bonus")
    if self:GetCaster():HasScepter() then -- If we want aghanim's upgrade
        iBonus = iBonus*2
    end

    if not GameRules:IsDaytime() then -- If we want the value change depending of day time.
        iBonus = iBonus + iNight
    else
        iBonus = iBonus + iDay
    end

    --We finally return the value in this part.
    if self:IsItem() then
        return self:GetCurrentCharges()*iBonus -- for items with charges
    else
        return self:GetStackCount()*iBonus -- in modifiers for stack based bonus
    end
end


-- Cast Filtering --

Cast filtering happens before ability is cast. This means we can interrupt the ability and display error or some fizzling particle effect in the event. Even running game logic here is an option but keep in mind that ability cooldown and mana cost are not spent at this point.
There is also a trick to make sure we don't have to write the filter and the error message functions twice when they would most likely end up identical.

function ability_super_power:CastFilterResultTarget( hTarget )
    return self:CCastFilter( hTarget, false )
end

function ability_super_power:GetCustomCastErrorTarget( hTarget)
    return self:CCastFilter( hTarget, true )
end

function ability_super_power:CCastFilter( hTarget, bError )
        local hCaster = self:GetCaster()
        local nTargetID = hTarget:GetPlayerOwnerID()
        local nCasterID = hCaster:GetPlayerOwnerID()
        if nTargetID and nCasterID then --making sure they both exist
            if PlayerResource:IsDisableHelpSetForPlayerID(nTargetID, nCasterID) then --target hates having caster help him out.
                if bError then
                    return "#dota_hud_error_target_has_disable_help"
                else
                    return UF_FAIL_CUSTOM
                end
            end
        end
        if not bError then
            return UF_SUCCESS
        end
end

As you can see we just have extra boolean value in the new function to indicate what we want to return. Essentially the function is ran twice in case of the bad target.


-- Complex Modifier Uses --

Lua based modifiers are in almost every way superior to their datadriven brothers. Only short fall they have is the current bug with auras. Instead of working like normal the aura modifier runs OnCreated function every 0.03 seconds. So if you want to make auras in current version of reborn, I suggest using datadriven instead.
Modifers can now be used extremely efficiently with the stack count. We can refer to it in any function. The ability example for fiery soul used it in function "IsHidden()" to return true if the stack count was less than 1. You can use it to directly multiply values to return in any of the declared modifier events.

    function modifier_super_power:DeclareFunctions()
        local funcs = {
            MODIFIER_PROPERTY_PHYSICAL_ARMOR_BONUS,
            MODIFIER_EVENT_ON_HERO_KILLED
        }
        return funcs
    end

    function modifier_super_power:GetModifierPhysicalArmorBonus()
        return self:GetStackCount()
    end

    function modifier_super_power:OnHeroKilled()
        self:IncrementStackCount()
    end

Another thing we can do is add values to our modifier and for example ID our modifier with the creation time. This is useful if we have chaining ability like chain lightning where we only want to let that particular lightning hit once but still allow multiple casts from refresher orb or some multicast passive.

    function modifier_super_power:GetAttributes() 
        return MODIFIER_ATTRIBUTE_MULTIPLE + MODIFIER_ATTRIBUTE_IGNORE_INVULNERABLE
    end

    function modifier_super_power:OnCreated( kv )
        self.mod_id = kv.mod_id or GameRules:GetGameTime()
    end

    function modifier_super_power:WasHit(hTarget)
        if IsServer() then
            if hTarget:HasModifier("modifier_super_power") then
                local tFades = hTarget:FindAllModifiersByName("modifier_super_power")
                if tFades then
                    for k, v in pairs(tFades) do
                        if v.mod_id == self.mod_id then
                        return true
                        end
                    end
                end
            end
            return false
        end
    end

WasHit(hTarget) function would be called when searching for new targets. Returns true if the target was hit by this instance of the spell. The mod_id should then be passed in the key value table.

In above clip I have dummy modifier created with id based on the game time on first cast. It will have duration bit bigger than the spell takes time to jump twice. This means if the spell has at least three targets it will continue bouncing indefinitely. If however it has less than that then it will only bounce on each available unit once. I also have another hidden value where the 'safe time' of unit not being bounced from is doubled for every consecutive ally bounce (ally bounces heal on that spell) so that you cannot get unlimited healing. That value in decreased by one for every enemy bounce so in full blown battle the spell can still last to the end.
Another method we have available is custom declaration of function. First we decide variable to use in the function:

    function modifier_super_power:OnCreated( kv )
        self.spell_mod = true
    end

Then we add a function to our ability to check if the player has a modifier with 'spell_mod'. In this example we use OnProjectileHit.

function ability_super_power:OnProjectileHit( hTarget, vLocation )
    local tKeys = {}
    tKeys.bDestroy = true
    tKeys.nDamage = 500
    tKeys.nDmgType = DAMAGE_TYPE_MAGICAL
    local tMods = self:GetCaster():FindAllModifiers()
    if tMods[1] then
        for k,v in pairs(tMods) do
            if v.spell_mod then
                tKeys = v:OnSpellMod( self , hTarget , vLocation, tKeys )
            end
        end
    end
    if tKeys.nDamage > 0 then
        local damage = {
            victim = hTarget,
            attacker = self:GetCaster(),
            damage = tKeys.nDamage,
            damage_type = tKeys.nDmgType,
            ability = self
        }
        ApplyDamage( damage )
    end
    if tKeys.bDestroy then
        return true
    else
        return false
    end
end

Now all we need in the modifier is to define what we want to do when OnSpellMod is called.

function modifier_super_power:OnSpellMod( hAbility , hTarget , vPoint, tKeys  )
    if IsServer() then
        tKeys.nDamage = self:GetCaster():GetManaPercent()*5
        if GameRules:IsDaytime() then
            tKeys.nDamage = tKeys.nDamage/2
            tKeys.bDestroy = false
        end
        tKeys.nDmgType = DAMAGE_TYPE_PURE
        return tKeys
    end
end

Here the mod changes the projectile damage into pure and based on the caster's mana. If day time the damage is halved and the projectile doesn't get destroyed on hit.
This kind of method can be employed even from items so you can design game mode around single projectile spell that is modified by getting the right items.

In the above clip I have the projectile ability that fires simple projectile that is destroyed on contact and two toggle abilities that when toggled on will add modifier to caster. One modifier is responsible of creating two split projectiles with the original ability and second modifier is responsible for the shock effect. As you can see they both can be toggled on and off and the projectile will automatically use them even in mid flight.
Now before lua abilities if you wanted to use custom modifier in a map or just in game logic you would have to create 'item' to provide 'ability' that would let you define the modifier desired. With Lua Modifiers we can do that in much more strait forward method. You can simply 'link' the modifier in your script. Only thing you do have to remember that you cannot refer to caster or the original ability in this method. But if you need out side parameters like the tick rate or particle effect you can always use GameRules to store your parameters.
The following is simple script that is intended to use map trigger events "OnStartTouch" and "OnStartEnd" to add and remove a modifier when unit enters/leaves the trigger mesh.

GameRules.PitModifier = "mod_pit_modifier"
--We can define GameRules.PitModifier in our game mode file
LinkLuaModifier( GameRules.PitModifier, "map_effects/mod_pit_effect.lua", LUA_MODIFIER_MOTION_NONE )
--You can have multiple modifiers defined in same file if you want to organize it that way.
function OnPitEnter(trigger)
    local ent = trigger.activator
    if not ent then return end
    if ent:IsAlive() then
        ent:AddNewModifier( ent, nil, GameRules.PitModifier, {} ) 
        --Fail safe if something changes the modifier added
        ent.TriggerModTemp = GameRules.PitModifier
    return
    end
end

function OnPitExit(trigger)
    local ent = trigger.activator
    if not ent then return end
    if ent:IsAlive() then
--Fail safe if something changes the modifier added and game would try to remove different modifier.
        if ent.TriggerModTemp then
            ent:RemoveModifierByName(ent.TriggerModTemp) 
            ent.TriggerModTemp = nil
        end
        return
    end
end

Now for the modifier we do something fancy. We create modifier that every 0.5 seconds finds closest enemy in 500 radius and zaps it with 100 physical damage. For zap effect we use lina's laguna blade and for modifier effect we use generic buff. Remember to precache both!
We also separate the flashy effect function to different function to stay organized.

if mod_pit_modifier == nil then
    mod_pit_modifier = class({})
end

function mod_pit_modifier:OnCreated( kv )   
    if IsServer() then
        self.nFXIndex = ParticleManager:CreateParticle( "particles/econ/generic/generic_buff_1/generic_buff_1.vpcf", PATTACH_OVERHEAD_FOLLOW, self:GetParent())
        ParticleManager:SetParticleControl( self.nFXIndex, 14, Vector(2, 2, 2 ) )
        ParticleManager:SetParticleControl( self.nFXIndex, 15, Vector(100, 100, 255 ) )
        self:AddParticle( self.nFXIndex, false, false, -1, false, false )
        self:StartIntervalThink( 0.5 )
    end
end

function mod_pit_modifier:IsHidden() return false end
function mod_pit_modifier:IsDebuff() return true end

function mod_pit_modifier:GetTexture()
    return "lina_laguna_blade"
end

function mod_pit_modifier:OnIntervalThink()
    if IsServer() then
        if self:GetParent():IsAlive() then
            local tTargets = FindUnitsInRadius(self:GetParent():GetTeam(),
                self:GetParent():GetOrigin(),
                nil,
                500,
                DOTA_UNIT_TARGET_TEAM_ENEMY,
                DOTA_UNIT_TARGET_ALL,
                DOTA_UNIT_TARGET_FLAG_NONE,
                FIND_CLOSEST,
                false
            )
            local hTarget = false
            if tTargets then
                for k,v in pairs(tTargets) do
                    if v ~= self:GetParent() then
                        hTarget = v
                        break
                    end
                end
            end
            if hTarget then
            self:Shock(hTarget)
            end
        end
    end
end

function mod_pit_modifier:Shock(hTarget)
    local nFXIndex = ParticleManager:CreateParticle( "particles/units/heroes/hero_lina/lina_spell_laguna_blade.vpcf", PATTACH_CUSTOMORIGIN, nil );
    ParticleManager:SetParticleControlEnt( nFXIndex, 0, self:GetParent(), PATTACH_POINT_FOLLOW, "attach_hitloc", self:GetParent():GetOrigin() + Vector( 0, 0, 100 ), true );
    ParticleManager:SetParticleControlEnt( nFXIndex, 1, hTarget, PATTACH_POINT_FOLLOW, "attach_hitloc", hTarget:GetOrigin() + Vector( 0, 0, 100 ), true );
    ParticleManager:ReleaseParticleIndex( nFXIndex );
    local hAttacker = self:GetParent()
    local damageTable = {
        victim = hTarget,
        attacker = self:GetParent(),
        damage = 100,
        damage_type = DAMAGE_TYPE_PHYSICAL,
    }
    ApplyDamage(damageTable)
end

The map trigger would not differ from normal script calling: http://i.imgur.com/qOvbf2b.png?1 End result should look something like this.


-- Complex Ability Uses --

When dealing with castable abilities we can use few different methods to give it different effects on X conditions. Like in our Spell Mod example in previous section we can simply call functions from modifiers to change spell effects. Then we can also use similar method to cast filters and figure out what we are targeting to create context sensitive spell. Good example of this would be Pugnas Life Drain where he can cast on ally to give life instead of taking it. Another method is to give the ability a value for different states that we can change depending on time of day or every time the spell is cast on the Caster.

function ability_super_power:SpellChange()
    local nSpellCount = 2
    self.CurrentSpell = self.CurrentSpell + 1
    if self.CurrentSpell > nSpellCount then self.CurrentSpell = 1 end
end

function ability_super_power:OnSpellCast()
    if not self.CurrentSpell then self.CurrentSpell = 1 end
    local hCaster = self:GetCaster()
    local hTarget = false
    local nSpell = self.CurrentSpell
    if not self:GetCursorTargetingNothing() then
    hTarget = self:GetCursorTarget()
    end
    local vPoint = self:GetCursorPosition()
    if hTarget and hTarget == hCaster then
        self:SpellChange()
        self:EndCooldown()
        self:RefundManaCost() 
    else
        if nSpell = 1 then
            self:OnSpell_A_Cast()
        elseif nSpell = 2 then
            self:OnSpell_B_Cast()
        end
    end
end

function ability_super_power:OnSpell_A_Cast()
    print("spell A go!")
end

function ability_super_power:OnSpell_B_Cast()
    print("spell B go!")
end

Another method is as follows:

ability_super_power.Spells = {}
ability_super_power.Spells[1] = function( hAbility )
    print("spell A go!")
end
ability_super_power.Spells[2] = function( hAbility )
    print("spell B go!")
end
ability_super_power.Spells[3] = function( hAbility )
    print("spell C go!")
end
ability_super_power.Spells[4] = function( hAbility )
    print("spell D go!")
end

function ability_super_power:SpellChange()
    self.CurrentSpell = self.CurrentSpell + 1
    if self.CurrentSpell > #self.Spells then self.CurrentSpell = 1 end
end

function ability_super_power:OnSpellCast()
    if not self.CurrentSpell then self.CurrentSpell = 1 end
    local hCaster = self:GetCaster()
    local hTarget = false
    local nSpell = self.CurrentSpell
    if not self:GetCursorTargetingNothing() then
    hTarget = self:GetCursorTarget()
    end
    local vPoint = self:GetCursorPosition()
    if hTarget and hTarget == hCaster then
        self:SpellChange()
        self:EndCooldown()
        self:RefundManaCost() 
    else
        self.Spells[nSpell]( self )
    end
end

In the last example we cannot refer to our 'ability_super_power' as 'self' when inside the actual spell function so we must pass the 'self' on as parameter. Other than that minor difference you can act like you were writing OnSpellCast() function instead.
Problem with all these methods is that currently there is no way to change the ability texture on fly. To give visual clue you either have to add particle or UI element for player or use something like modifier stack count or item charge count.

For passives its pretty strait forward.

function ability_super_power:GetIntrinsicModifierName()
    return "modifier_super_power"
end

But things get more complex if you need the passive to exist before you upgrade the ability. Since there is no built-in function for that we can work our way around it. You can choose any other function to over ride but I like using OnHeroCalculateStatBonus(). Then simply add the modifier as long as the this function has (not been ran before) to your hero. NOTE: OnHeroCalculateStatBonus() only works with hero units. For other units you have to use something else.

function ability_super_power:OnHeroCalculateStatBonus() 
    if IsServer() and not self.PassiveAdded then
        self:GetCaster():AddNewModifier(self:GetCaster(), self, "modifier_super_power", {}) 
        self.PassiveAdded = true -- We make sure we only do this once.
    end
end

To make sure the passive modifier doesn't get destroyed make sure to add following to it:


function modifier_super_power:GetAttributes() return MODIFIER_ATTRIBUTE_IGNORE_INVULNERABLE + MODIFIER_ATTRIBUTE_PERMANENT end function modifier_super_power:IsPurgable() return false end function modifier_super_power:IsPurgeException() return false end function modifier_super_power:IsDebuff() return false end function modifier_super_power:RemoveOnDeath() return false end

IMPORTANT!
Make sure you add the boolean value check (if passive was added or not) on your 'OnHeroCalculateStatBonus()' passive check or you might cause feedback loop.


--Importance of Order--

Sometimes you are coding ability that involves modifier that deals damage. If there is game logic that involves the parent (target unit) of that modifier then you have to do that before the damage is dealt. If you don't take that into account the script might not run as intended. This problem can be encountered with spells like chain lightning where you might start the code simply with damage to unit before searching for the next target. Also for visual and audio to not be horrid in these types of spells I suggest using either timer library by @BMD or have the chaining happen in modifier's OnDestroy function with modifier having 0.1 or so duration rather than simply OnCreate. This has advantage also to give player more visual and auditory clues on what is going on.

Another thing I should mention is calculation of stat bonuses. Because game assumes stats are quite static they are not updated unless some events ask for the update. This usually means they are only updated on level up or inventory item change. If your passive adds stats and they change with stack count, remember to hHero:CalculateStatBonus() every time you increase or decrease the stack count. This should of course happen after the stack is added but not in the function that returns your stat bonus value.
Neglecting to recalculate causes issues such as If your hero is has strength as primary, only the damage is updated (not health) also you agi is only updated if you are in combat.

--Debuging--

Most common error is:

[ W VScript ]: ...s\common\dota 2 beta\game\core\scripts\vscripts\init.lua:189: in function <...s\common\dota 2 beta\game\core\scripts\vscripts\init.lua:187>

This is simply that something basic is wrong. Usually the solution is just that you are using wrong file class name for ability or modifier and it is not found in the file you are using. Just check your name references everywhere (npc_abilities_custom.txt, the lua file, the modifier lua file etc.)
Also if you copy paste code form examples the function might be refering to a different class name than intended. Do check that aswell.
There are many debug commands available in the API. If you are in doubt and the console error spew is not being helpful then use these:

Print("Succesfully reached this bit of code and the ability name still says " .. GetAbilityName())
--Print is useful for many things. Just add unique strings to print and possibly some helpful values from your function.
DeepPrintTable(hTarget)
--DeepPrintTable can be used for normal tables and even for handles. It provides all the methods you can call for the handle. Only problem is that the console spew is long. So be prepared to search through it.
GameRules:AddMinimapDebugPoint(iType, vLocation, r, g, b, alpha, fDuration)
GameRules:AddMinimapDebugPointForTeam(iType, vLocation, r, g, b, alpha, fDuration, iTeam)
--For all your minimap needs
DebugDrawBox(Vector origin, Vector min, Vector max, int r, int g, int b, int a, float duration)
DebugDrawBoxDirection(cent, min, max, forward, vRgb, a, duration) 
DebugDrawCircle(center, vRgb, a, rad, ztest, duration) 
DebugDrawLine(origin, target, r, g, b, ztest, duration) 
DebugDrawSphere(center, vRgb, a, rad, ztest, duration)
DebugDrawText(origin, text, bViewCheck, duration) 
--When dealing with geometry or invisible dummy units, these are very useful.

NOTE: THIS IS STILL A DRAFT!
If you have something you would like me to explain how to do in this tutorial. Please send pm or contact me in IRC.
Also contact me If you want me to add more Gfy clips of working examples.