Skip to main content

Custom Barriers

What are barriers?

In this tutorial I'll be going over what barriers are, and how they are created and handled.

To start off, we should get ourselves a little more familiar with barriers in general. Barriers come in 3 flavours;

  • Universal : Blocks All damage (gold)
  • Physical : Blocks Physical damage (red)
  • Spell : Blocks Magic damage (blue)

Physical and Spell barriers only block their respective damage type, but Universal blocks both of those as well as pure damage. Conceptually, barriers are simple to understand. It's when we start getting into the execution that it gets more complex.

Barrier Functions

MODIFIER_PROPERTYFunction Name
MODIFIER_PROPERTY_INCOMING_DAMAGE_CONSTANTGetModifierIncomingDamageConstant( event )
MODIFIER_PROPERTY_INCOMING_PHYSICAL_DAMAGE_CONSTANTGetModifierIncomingPhysicalDamageConstant( event )
MODIFIER_PROPERTY_INCOMING_SPELL_DAMAGE_CONSTANTGetModifierIncomingSpellDamageConstant( event )
The event in question is a 'ModifierAttackEvent'
attackerCDOTA_BaseNPC
damagefloat
damage_typeDAMAGE_TYPES
damage_categoryDamageCategory_t
damage_flagsDOTADamageFlag_t
inflictor?CDOTABaseAbility
original_damagefloat
ranged_attackbool
targetCDOTA_BaseNPC
no_attack_cooldownbool
recordint
fail_typeattackfail
report_max?bool
The ? on inflictor and report_max denote that they are optional and may not be defined, depending on how the damage was dealt (such as through an auto attack).

The main things you're going to want to care about from the event are:

  • damage
  • report_max

While each property is useful when you need them, most of the time a barrier just acts as a bonus health pool for the unit, so you only care about how much damage was done. damage is a float that contains the amount of post-reduction damage the parent took, and report_max is a bool for the client that determines whether we return the max or the current health of the barrier.

Since a barrier is dynamic and requires properties on both the client and the server side of the code to be in sync, we need to make use of custom transmitter data. This guide assumes you are familiar with the custom transmitter concept, but if you're not, then you can refer to the guide here and continue afterwards. It's alright, I'll wait.

Simple Example

Below is the code for a basic all damage barrier that starts with 100 health and is removed when the health reaches 0.

Simple Universal Barrier
modifier_custom_all_barrier = class({})

function modifier_custom_all_barrier:OnCreated()
if not IsServer() then return end

-- Set up our properties for the barrier's health
self.max_barrier_health = 100
self.current_barrier_health = self.max_barrier_health

-- Tell the modifier we want to transmit the data
self:SetHasCustomTransmitterData(true)
end

function modifier_custom_all_barrier:OnRefresh()
self:OnCreated()

-- Tell the client that we need to get the properties again
if IsServer() then self:SendBuffRefreshToClients() end
end

-- Send our properties to client
function modifier_custom_all_barrier:AddCustomTransmitterData()
return {
max_barrier_health = self.max_barrier_health,
current_barrier_health = self.current_barrier_health
}
end

-- Get the properties from the server
function modifier_custom_all_barrier:HandleCustomTransmitterData(data)
self.max_barrier_health = data.max_barrier_health
self.current_barrier_health = data.current_barrier_health
end

-- Declare which barrier type it is
function modifier_custom_all_barrier:DeclareFunctions()
return {
MODIFIER_PROPERTY_INCOMING_DAMAGE_CONSTANT
}
end

function modifier_custom_all_barrier:GetModifierIncomingDamageConstant( event )

-- Return the max health on the client if it's a max report, otherwise return the current health
if IsClient() then
if event.report_max then
return self.max_barrier_health
else
return self.current_barrier_health
end
end

-- Reduce the barrier's health by the damage
self.current_barrier_health = self.current_barrier_health - event.damage

-- Tell the client that we need to update the health property
self:SendBuffRefreshToClients()

-- Check if the damage exceeded the barrier's health
if self.current_barrier_health <= 0 then
self:Destroy()

-- If it did, return the amount of damage we blocked as a negative to reduce it
return -(event.damage + self.current_barrier_health)
else

-- Otherwise, return the full value of the damage as a negative to cancel it out
return -event.damage
end
end

It certainly looks like a lot, but let's go through and break it down into digestible chunks and see what is happening.

Going through the example

Outside of the basic functions, we have some new ones popping up already. Some noteworthy additions are SetHasCustomTransmitterData() and SendBuffRefreshToClients().

Basic declarations
modifier_custom_all_barrier = class({})

function modifier_custom_all_barrier:OnCreated()
if not IsServer() then return end

-- Set up our properties for the barrier's health
self.max_barrier_health = 100
self.current_barrier_health = self.max_barrier_health

-- Tell the modifier we want to transmit the data
self:SetHasCustomTransmitterData(true)
end

function modifier_custom_all_barrier:OnRefresh()
self:OnCreated()

-- Tell the client that we need to get the properties again
if IsServer() then self:SendBuffRefreshToClients() end
end

These are used to keep our client's current health property up to date with each time it changes on the server, so we need to make sure to call SendBuffRefreshToClients() when the barrier's health changes.

But when does it change?

Taking barrier damage
function modifier_custom_all_barrier:GetModifierIncomingDamageConstant( event )

-- Return the max health on the client if it's a max report, otherwise return the current health
if IsClient() then
if event.report_max then
return self.max_barrier_health
else
return self.current_barrier_health
end
end

-- Reduce the barrier's health by the damage
self.current_barrier_health = self.current_barrier_health - event.damage

-- Tell the client that we need to update the health property
self:SendBuffRefreshToClients()

-- Check if the damage exceeded the barrier's health
if self.current_barrier_health <= 0 then
self:Destroy()

-- If it did, return the amount of damage we blocked as a negative to reduce it
return -(event.damage + self.current_barrier_health)
else

-- Otherwise, return the full value of the damage as a negative to cancel it out
return -event.damage
end
end

The barrier's health is updated when GetModifierIncomingDamageConstant() is called. The client only cares about what the properties for max health and current health are, so it returns immediately, leaving the rest of the code for the server to deal with the logic of the barrier losing health.

The way that a barrier works is that it's a flat damage reduction on the incoming damage. So after you reduce the health of the barrier, you need to return the amount of damage that you blocked so that Dota knows how much to reduce the damage by. The client has a positive number returned to it, while the server has a negative number.

note

The client calls GetModifierIncomingDamageConstant() twice per frame, once to get the max health of the barrier (report_max = true), and another time to get the current health of the barrier (report_max = false). The server only calls GetModifierIncomingDamageConstant() when the parent takes damage.

And there we have it. We've gone through and seen how the barrier is set up and how the health is updated on both the client and the server so that it stays in sync.

The rest of this page will be about a library I've been working on to both add more features to and streamline the process of making barriers in general.

Custom Barrier Library

Setting up the library

This library is meant to be an extension of the normal modifier class and adds some useful methods to a modifier, and will turn it into a barrier for you. It can be put into the vscripts folder, but must be required on both the server and the client, so put it into the addon_init.lua file. If you don't have one, simply create it and put it in the vscripts folder where your addon_game_mode.lua file is.

Creating a custom barrier

CustomBarriers:TurnModifierIntoBarrier( CDOTA_Buff )

This function does most of the heavy lifting, it will add all the necessary functions to your modifier and turn it into a barrier. This needs to be called after all your modifier functions have been declared. The modifier also needs to know what type of barrier it is, and how much max health it has.

Setting type and max health
-- You can either use the get functions to declare them
function modifier:GetBarrierType()
return DAMAGE_TYPE
end

function modifier:GetBarrierMaxHealth()
return int
end

-- Or you can set them in the modifier's OnCreated()
function modifier:OnCreated()
self:SetBarrierType( DAMAGE_TYPE )
self:SetBarrierMaxHealth( int )
end

This is already enough to have a functional barrier of the type you want.

Library Functions

These functions are added to the modifier when you call CustomBarriers:TurnModifierIntoBarrier( CDOTA_Buff ), they're not added into the CDOTA_Buff base class. IsBarrier() is the only one that is added to the CDOTA_Buff base class, so calling the others on a modifier you haven't turned into a barrier will cause an error.

Function NameReturnDescription
IsBarrier()boolReturns true if the barrier is visible.
IsBarrierFor( DAMAGE_TYPES )boolReturns true if the damage type will be blocked by the barrier.
SetBarrierType( DAMAGE_TYPES )nilSets the barrier to block the specified type of damage.
GetBarrierType()DAMAGE_TYPESReturns what type of damage the barrier will block.
SetBarrierMaxHealth( int )nilSets the barrier's max health.
GetBarrierMaxHealth()intGets the barrier's max health.
SetBarrierHealth( int )nilSets the barrier's current health.
GetBarrierHealth()intGets the barrier's current health.
SetBarrierInitialHealth( int )nilSets the barrier's initial health.
GetBarrierInitialHealth()intGets the barrier's initial health.
IsPersistent()boolWhether the modifier will be destroyed when the barrier's health reaches 0. Defaults to false.
ShowOnZeroHP()boolWhether the barrier bar is visible at 0 hp. Defaults to false.
OnBarrierDamagedFilter( event )boolA filter for damage to the barrier. Return false if you want the unit to be damaged instead. Set the event.damage to 0 if you want no damage to be done at all. (Only called on server)
  • GetBarrierType() and SetBarrierType() are designed to be used with the 3 main damage types; Physical, Magical, and Pure. Pure refers to the Universal barrier in this case.
  • ShowOnZeroHP() will only work if IsPersistent() is true.

Example of a library barrier

Let's say we want to have a magic damage barrier that starts at 0, is always around, increases based on the magic damage we deal, and has a cap of based on our max hp. Sounds very complex right? Let's see how we do.

modifier_magic_barrier = class({})

-- We want to hide it when the hp is 0
function modifier_magic_barrier:IsHidden()
return self:GetBarrierHealth() <= 0
end

function modifier_magic_barrier:IsPermanent()
return true
end

-- Make sure the modifier isn't destroyed when it's at 0 hp
function modifier_magic_barrier:IsPersistent()
return true
end

-- We want a magic barrier
function modifier_magic_barrier:GetBarrierType()
return DAMAGE_TYPE_MAGICAL
end

-- Declare the max barrier health as % of our max hp
function modifier_magic_barrier:GetBarrierMaxHealth()
return math.ceil(self:GetParent():GetMaxHealth() * self.barrier_cap)
end

-- Start at 0 barrier hp
function modifier_magic_barrier:GetBarrierInitialHealth()
return 0
end

-- Here we want to just declare some properties to make our life easier
function modifier_magic_barrier:OnCreated()
self.barrier_cap = self:GetAbility():GetSpecialValueFor("barrier_cap")/100

if IsServer() then
self.barrier_conversion = self:GetAbility():GetSpecialValueFor("barrier_conversion")/100
self:StartIntervalThink(0.2)
end
end

-- We need to keep track of our max hp somehow, if we dont then it will only update when we take damage
function modifier_magic_barrier:OnIntervalThink()
self:SetBarrierMaxHealth( math.ceil(self:GetParent():GetMaxHealth() * self.barrier_cap) )
end

-- We want to know when something takes damage to check if it's from us
function modifier_magic_barrier:DeclareFunctions()
return {
MODIFIER_EVENT_ON_TAKEDAMAGE
}
end

-- Here we check when we do magic damage
function modifier_magic_barrier:OnTakeDamage( event )
if IsClient() then return end -- don't need the client here

if keys.unit == self:GetParent() or keys.attacker ~= self:GetParent() then
return -- we only care about us doing damage
end

if keys.damage_type ~= DAMAGE_TYPE_MAGICAL then return end -- we only want magic damage

-- now we can turn a portion of the magic damage we've done into our barrier health!
local amount = keys.damage * self.barrier_conversion

-- Set the health directly, but don't exceed the max health
self:SetBarrierHealth( math.min( self:GetBarrierHealth() + amount, self:GetBarrierMaxHealth() ) )
end

-- Now that we've created our modifier, we can tell the library to turn it into a barrier!
CustomBarriers:TurnModifierIntoBarrier( modifier_magic_barrier )

Compact Example

Let's take the Simple Example from earlier and remake it using the Library.

Simple Universal Barrier (Lib Edition)
modifier_custom_all_barrier = class({})
function modifier_custom_all_barrier:GetBarrierType() return DAMAGE_TYPE_PURE end
function modifier_custom_all_barrier:GetBarrierMaxHealth() return 100 end
CustomBarriers:TurnModifierIntoBarrier( modifier_custom_all_barrier )

This still functions the exact same as the previous version, all of the barrier specific code is just being handled by CustomBarriers instead.