Skip to content

Ability Lua Tutorial 8: Slark's Dark Pact

Elfansoer edited this page Aug 2, 2018 · 1 revision

Waiting in interval.

Overview

I think we'll go straight to the action. And by action I mean to the modifier file, since there's nothing happened on the main Lua than creating a modifier for the caster. For clarity, here's the ability file.

-- slark_dark_pact_lua.lua
slark_dark_pact_lua = class({})
LinkLuaModifier( "modifier_slark_dark_pact_lua", "lua_abilities/slark_dark_pact_lua/modifier_slark_dark_pact_lua", LUA_MODIFIER_MOTION_NONE )

--------------------------------------------------------------------------------
-- Ability Start
function slark_dark_pact_lua:OnSpellStart()
	-- Add timer
	self:GetCaster():AddNewModifier(
		self:GetCaster(),
		self,
		"modifier_slark_dark_pact_lua",
		{}
	)
end

Well, on this case, the modifier's duration isn't specified by the creator (the ability). The modifier we create will self-destruct, once the timer expires.

Logic: GetAttributes

We start off by declaring the modifier's properties:

-- modifier_slark_dark_pact_lua.lua 
modifier_slark_dark_pact_lua = class({})

--------------------------------------------------------------------------------
-- Classifications
function modifier_slark_dark_pact_lua:IsHidden()
	return true
end

function modifier_slark_dark_pact_lua:GetAttributes()
	return MODIFIER_ATTRIBUTE_MULTIPLE 
end

function modifier_slark_dark_pact_lua:IsPurgable()
	return false
end

The modifier is hidden in gui, since it is not a buff, gameplay-wise. IsPurgable is self-explanatory.

GetAttributes declare more specific attribute of the modifier. You can see here for references, but here's some explanation of modifiers I often use:

  • MODIFIER_ATTRIBUTE_PERMANENT: Basically states that the modifier won't get removed by in-game effects. Removed only when Destroy() is called.
  • MODIFIER_ATTRIBUTE_MULTIPLE: By default, AddNewModifier creates new modifier to a unit if it doesn't have it, and refreshes modifier if it already exist. This attribute declares that AddNewModifier always creates new modifier, allowing multiple instances over a unit.

Logic: Interval

Let's initialize the modifier by collecting information:

-- modifier_slark_dark_pact_lua.lua 
--------------------------------------------------------------------------------
-- Initializations
function modifier_slark_dark_pact_lua:OnCreated( kv )
	-- references
	self.delay = self:GetAbility():GetSpecialValueFor( "delay" )
	self.pulse_duration = self:GetAbility():GetSpecialValueFor( "pulse_duration" )
	self.radius = self:GetAbility():GetSpecialValueFor( "radius" )
	self.total_damage = self:GetAbility():GetSpecialValueFor( "total_damage" )
	self.total_pulses = self:GetAbility():GetSpecialValueFor( "total_pulses" )
	self.pulse_interval = self:GetAbility():GetSpecialValueFor( "pulse_interval" )
end

function modifier_slark_dark_pact_lua:OnDestroy( kv )

end

Now, the great question came,
"What does this ability do?"

When cast, Dark Pact purges and damages nearby units, after a delay. We can create delays by using modifier:StartIntervalThink( float ) function. When it is called, the modifier will periodically calls OnIntervalThink() for the specified interval until it is stopped or the modifier is destroyed.

Since we're having a delay right from the start, we'll do this:

-- modifier_slark_dark_pact_lua.lua 
--------------------------------------------------------------------------------
-- Initializations
function modifier_slark_dark_pact_lua:OnCreated( kv )
	-- references
	self.delay = self:GetAbility():GetSpecialValueFor( "delay" )
	--/--/--

	if IsServer() then
		-- Start interval
		self:StartIntervalThink( self.delay )
	end
end

--/--/--

function modifier_slark_dark_pact_lua:OnIntervalThink()
	-- delayed logic here
end

StartIntervalThink is called within IsServer block, since this is about logic which will be computed only on Server, and Client will only follow it. StartIntervalThink will only be called on server, so OnIntervalThink method doesn't have to call IsServer again.

One and Only Interval

Now, what happened after the delay? It purges and deals damage. But there's a twist that the purge and damage are happened continuously! We'll have intervals again then.

But wait. At the moment, the current engine can only have 1 interval instances. That is, when you set another interval, the previous one will got replaced.

For example, if I were to do this:

function modifier_slark_dark_pact_lua:OnCreated( kv )
	if IsServer() then
		-- Start interval
		self:StartIntervalThink( 1 )
	end
end

--/--/--

function modifier_slark_dark_pact_lua:OnIntervalThink()
	self:StartIntervalThink( 0.1 )
end

OnIntervalThink will be called after 1 second, and then every 0.1 seconds until stopped or destroyed.
You know what it leads to?

The implication of the twist is that any delayed or interval effects and logic must be within one single OnIntervalThink method. Therefore, the method should be able to tell on which interval it is invoked upon, to be able to give appropriate logic.

Confused? Well, look at this:

function modifier_slark_dark_pact_lua:OnCreated( kv )
	-- additional variaable
	self.cast = false

	if IsServer() then
		-- Start interval
		self:StartIntervalThink( 1 )
	end
end

--/--/--

function modifier_slark_dark_pact_lua:OnIntervalThink()
	if self.cast then
		-- first logic here

		self:StartIntervalThink( 0.1 )
		self.cast = true
	else
		-- second logic here
	end
end

This way, first logic will get invoked after 1 second delay, because the variable self.cast is false.
At the same time, the variable is set to true and a new interval is set every 0.1 second.
After 0.1 seconds (and goes on periodically), second logic will be invoked due to self.cast is true.

Logic: Purge

At each tick after the delay, Dark Pact purges the caster and deals damage around the caster, including the caster themselves. To make life easier, let's do the tick logic in separate function, named Splash()

-- modifier_slark_dark_pact_lua.lua 
function modifier_slark_dark_pact_lua:Splash()
	-- tick logic here

end

First, we have to purge. Coincidentally, a function named Purge exist, so we'll have that:

-- modifier_slark_dark_pact_lua.lua 
function modifier_slark_dark_pact_lua:Splash()
	-- tick logic here
	self:GetParent():Purge(false, true, false, true, false)

end

Explanation time:
Purge method is owned by unit, so the one above means that this modifier's (self) parent unit (GetParent()) will be purged, with parameters (all boolean):

  1. RemovePositiveBuffs: Remove buffs if true
  2. RemoveDebuffs: Remove debuffs if true
  3. BuffsCreatedThisFrameOnly: Remove debuffs that happened at this exact time only. Probably, I don't know. I always set it to false anyway.
  4. RemoveStuns: Can remove stuns if true
  5. RemoveExceptions: Considered as strong dispel if true

Things about searching for enemies and deals damage have been covered, so I'll show the full version instead:

function modifier_slark_dark_pact_lua:Splash()
	-- purge
	self:GetParent():Purge(false, true, false, true, false)

	-- get damage per interval
	local damage_interval = self.total_damage/self.total_pulses

	-- find units in radius
	local enemies = FindUnitsInRadius(
		self:GetParent():GetTeamNumber(),	-- int, your team number
		self:GetParent():GetOrigin(),	-- point, center point
		nil,	-- handle, cacheUnit. (not known)
		self.radius,	-- float, radius. or use FIND_UNITS_EVERYWHERE
		DOTA_UNIT_TARGET_TEAM_ENEMY,	-- int, team filter
		DOTA_UNIT_TARGET_HERO + DOTA_UNIT_TARGET_BASIC,	-- int, type filter
		0,	-- int, flag filter
		0,	-- int, order filter
		false	-- bool, can grow cache
	)

	-- do for each enemies caught
	for _,enemy in pairs(enemies) do
		-- Apply damage
		local damageTable = {
			victim = enemy,
			attacker = self:GetParent(),
			damage = damage_interval,
			damage_type = DAMAGE_TYPE_MAGICAL,
			ability = self:GetAbility()
		}
		ApplyDamage(damageTable)
	end

	-- Apply damage to self
	local damageTable = {
		victim = self:GetParent(),
		attacker = self:GetParent(),
		damage = damage_interval/2,
		damage_type = DAMAGE_TYPE_MAGICAL,
		ability = self:GetAbility()
	}
	ApplyDamage(damageTable)
end

Logics for ticks

Now, time to put the Splash() appropriately. The first tick happened right after the delay, and then happened at every self.pulse_interval seconds. Therefore, we have:

-- modifier_slark_dark_pact_lua.lua 
--------------------------------------------------------------------------------
-- Interval Effects
function modifier_slark_dark_pact_lua:OnIntervalThink()
	if not self.cast then
		self:Splash()

		self.cast = true
		self:StartIntervalThink( self.pulse_interval )
	else
		self:Splash()
	end
end

Something's missing, no? How is it going to stop???

Well, we know that there will be self.total_pulses ticks, so after the last tick, the modifier self-destruct. First, we'll have to track the number of ticks happened, in self.current_pulse:

--------------------------------------------------------------------------------
-- Initializations
function modifier_slark_dark_pact_lua:OnCreated( kv )
	-- references
	self.delay = self:GetAbility():GetSpecialValueFor( "delay" )
	self.total_pulses = self:GetAbility():GetSpecialValueFor( "total_pulses" )
	self.pulse_interval = self:GetAbility():GetSpecialValueFor( "pulse_interval" )

	--/--/--

	-- Additional variables
	self.cast = false
	self.current_pulse = 0

	if IsServer() then
		-- Start interval
		self:StartIntervalThink( self.delay )
	end
end

Then, we'll increment the numbers each time splash happened. If the number of ticks already equal to total pulses, the modifier self-destructs:

--------------------------------------------------------------------------------
-- Interval Effects
function modifier_slark_dark_pact_lua:OnIntervalThink()
	if not self.cast then
		self:Splash()

		-- add counter
		self.current_pulse = self.current_pulse + 1

		self.cast = true
		self:StartIntervalThink( self.pulse_interval )
	else
		self:Splash()

		-- add counter
		self.current_pulse = self.current_pulse + 1
		
		-- self destruct
		if self.current_pulse >=10 then
			self:StartIntervalThink( -1 )
			self:Destroy()
		end
	end
end

Conclusion

Okay, a lot happened, it's time to stop. Next time, we'll having a Linear Projectiles and How To Use Them, in Mirana's Sacred Arrow. Stay Tuned.