Accurate Attack Stats & Triggers

This bundle is marked as high quality. It exceeds standards and is highly desirable.
  • An alternative to invisible, instant attack projectiles and damage detection to accurately trigger effects on attacks.
  • Easily add effects that are triggered at a ranged unit's projectile launch point.
  • Detect if the attack animation was interrupted, so that no trigger is executed without a projectile launch (which is the problem with EVENT_UNIT_ATTACKED).
  • Accurately calculate a unit's attack damage, attack speed, attack delay, and dps.
I was looking at ways to improve the "Whenever this unit attacks, do X"-triggers in my map and was told that I should replace the units' attacks with invisible, instant attacks and trigger the projectiles with damage detection. I did not feel like doing that to a giant map with 100+ different units, so I set out to write systems that would work reliably with standard missile attacks. Little did I know what I was getting myself into. But I persevered and the result are these three libraries.

Here's the bad news: To get an accurate value for a unit's attack damage and speed, there seems to be no way around brute-forcing all buffs, auras, and items. Luckily, these libraries make this task super easy; barely an inconvenience. The user simply has to declare all buffs, auras, and items the system should look out for, and then it does all the work of tracking those buffs and extracting the correct values for their modifiers. You do not need to update anything as you change the numbers on those spells.

This tracking system works on most buffs and items. Getting the attack damage and speed to 95% accuracy will be very easy, but the remaining 5% could be quite tough. However, for most applications, 95% accuracy should be more than enough.

If you do not need attack triggers, you may still be interested in an accurate calculation of a unit attack stats, for example to give players the option to display their hero's dps.

vJASS:
/**************************************************************************************************
				T R I G G E R   O N   A T T A C K
		 		U N I T   A T T A C K   S T A T S
						&
				A T T A C K   M O D   B U F F S
					created by Antares
***************************************************************************************************

These three libraries do different tasks related to units' auto-attacks.

TriggerOnAttack is designed to accurately determine the point when a ranged hero launches an attack 
projectile and easily set up effects that trigger on that event. It requires UnitAttackStats to 
calculate the delay between the beginning of the attack to the projectile launch point. It provides 
an alternative to using invisible, instantaneous projectiles and using a damage event to trigger 
the actual attack.

UnitAttackStats can calculate various stats around a unit's auto-attack, including attack delay,
used by TriggerOnAttack. This library can also be used to simply give players the option to display
their attack cooldown or dps. UnitAttackStats requires AttackModBuffs to give an accurate result if
attack speed and damage modifying buffs and auras are present, but it will function without it.

AttackModBuffs is a library designed to track attack speed and damage modifying buffs and auras. It
requires you to declare every such buff and aura in your map, but makes it maximally convenient to
do so.

***************************************************************************************************

				H O W   T O   G E T   S T A R T E D :

===================================================================================================
					TriggerOnAttack
===================================================================================================

Copy the TriggerOnAttack library into your map. There are two parameters you can customize: 

TIMESTEP_CHECK determines the accuracy of the On-Attack-Effect.

If CLEAN_UP_ON_DEATH is enabled, it will clean-up everything upon death of a nonhero unit. If disabled,
you can do so manually with:
function UnitClearAllAttackActions takes unit whichUnit returns nothing

To create an On-Attack-Effect, use the function:
AddUnitAttackAction takes unit whichUnit, triggerOnAttack whichCallback, real procChance, boolean isStacking returns nothing

whichCallback is a function that requires two unit arguments:
function interface triggerOnAttack takes unit whichUnit, unit whichTarget returns nothing

Examples of such functions are given in the Test script.

The Interrupt function will register if the attack of the unit was interrupted. There are large number 
of fringe commands that a unit can be given that shouldn't interrupt the attack, but aren't added
to the function. If any of these become an issue in your map, change the condition in the Interrupt
function.

===================================================================================================
					UnitAttackStats
===================================================================================================

Copy the UnitAttackStats library into your map. Set the ATTACK_SPEED_PER_AGILITY parameter to the
value of your map. If your map does not have attack speed and damage modifying buffs, auras, and/or
items, you can disable those checks in the parameters to increase performance.

For item checks to work, you need to declare all item abilities based on Item Damage Bonus, Item
Attack Speed Bonus, and Orb Abilities (such as Item Attack Fire Bonus) with:
attackDamageItemAbility.create takes integer abilityId returns attackDamageItemAbility
attackSpeedItemAbility.create takes integer abilityId returns attackSpeedItemAbility

You only have to declare these abilities. The library will take care of everything else. Examples
of those declarations are given in the Test script.

Note that items that grant auras are not handled by this library, but by AttackModBuffs.

===================================================================================================
					AttackModBuffs
===================================================================================================

Copy the AttackModBuffs library into your map. There are three parameters to customize:

ENABLE_CAST_DETECTION will allow you to declare spells that apply an attack speed or damage modifying
buff. The library will detect those spells being cast and track the buff automatically. These spells 
are declared with:
attackModSpell.create takes integer abilityId, integer buffId, integer spellType returns attackModSpell
Examples of those declarations are given in the Test script.

If IGNORE_NONHERO_UNITS is enabled, only buffs on heroes are tracked. Chances are, you don't care
about the exact attack speed of a random footman.

BUFF_CHECK_INTERVAL sets the interval at which the expiration of the buff is checked.

Cast Detection will not work on all spells. Poison attack, Cold Arrows, Thunderclap, and Frost Armor
have to be applied manually. This is done with:
AddAttackModifyingBuff takes unit whichUnit, unit source, integer abilityId, integer buffId, integer spellType returns nothing

Note that this library cannot extract the correct attack speed and damage modifiers from buffs
if they weren't detected as they were applied.


The tracking of auras works a bit differently. Auras have to be declared on a per-source-basis
(sources can be either units or items) with:
attackModAura.create takes integer abilityId, integer buffId, integer auraType, unit unitSource, item itemSource returns attackModAura

To extract the correct modifyers, this library will search for the friendly source with the highest 
level of the aura in range. If you changed the aura to affect enemies, you have to change the logic
in GetAttackDamagePercentAuras and/or GetAttackSpeedPercentAuras.

Setting up most buffs is super easy, barely an inconvenience. If you are fine with less than 100% 
accuracy, you can ignore all other buffs and simply focus on the easy or important ones.

***************************************************************************************************

					    A P I :

===================================================================================================
					Attack Mod Buffs
===================================================================================================

attackModSpell.create takes integer abilityId, integer buffId, integer spellType returns attackModSpell
attackModAura.create takes integer abilityId, integer buffId, integer auraType, unit unitSource, item itemSource returns attackModAura
AddAttackModifyingBuff takes unit whichUnit, unit source, integer abilityId, integer buffId, integer spellType returns nothing

Valid arguments for spellType:
BLOODLUST
SLOW
INNER_FIRE
UNHOLY_FRENZY
ROAR
CRIPPLE
SILENCE
SOULBURN
BERSERK
POISON_ATTACK
FROST_NOVA
FROST_ARMOR
COLD_ARROWS
THUNDERCLAP

Valid arguments for auraType:
TRUESHOT_AURA
COMMAND_AURA
ENDURANCE_AURA

===================================================================================================
					Unit Attack Stats
===================================================================================================

GetUnitAttackDamage takes unit whichUnit, integer weaponIndex returns real
GetUnitAttackSpeedBonus takes unit whichUnit returns real
GetUnitAttackDelay takes unit whichUnit, integer weaponIndex returns real
GetUnitAttackCooldown takes unit whichUnit, integer weaponIndex returns real
GetUnitDPS takes unit whichUnit, integer weaponIndex returns real

===================================================================================================
					Trigger On Attack
===================================================================================================

AddUnitAttackAction takes unit whichUnit, triggerOnAttack whichCallback, real procChance, boolean isStacking returns nothing
RemoveUnitAttackAction takes unit whichUnit, triggerOnAttack whichCallback returns nothing
UnitClearAllAttackActions takes unit whichUnit returns nothing

***************************************************************************************************/



library AttackModBuffs initializer Init

globals
	//=========================================================================================================

	private constant boolean ENABLE_CAST_DETECTION		= true		//If cast detection is activated, you may declare spells that add attack damage/
										//attack speed modifying spells with attackModSpell.create.
										//Only works for targeted spells and Berserk.
	private constant boolean IGNORE_NONHERO_UNITS		= true		//If enabled, attack modifying buffs will only be tracked on heroes.
	private constant real BUFF_CHECK_INTERVAL 		= 0.2		//How often is checked if a buff has expired.

	//=========================================================================================================

	private hashtable buffTable				= InitHashtable()
	private constant abilityreallevelfield DATA_A		= ABILITY_RLF_DEFENSE_BONUS_HAV1
	private constant abilityreallevelfield DATA_B		= ABILITY_RLF_HIT_POINT_BONUS
	private constant abilityreallevelfield DATA_C		= ABILITY_RLF_DAMAGE_BONUS_HAV3
	private constant abilityreallevelfield DATA_D		= ABILITY_RLF_MAGIC_DAMAGE_REDUCTION_HAV4
	private constant abilityreallevelfield DATA_E		= ABILITY_RLF_MAX_DAMAGE_UCO5
	private abilityreallevelfield array attackSpeedWhichField
	private abilityreallevelfield array attackDamageWhichField
	private integer array dataSign

	//=========================================================================================================
	//Spell types used for attackModSpell.create()
	//=========================================================================================================
	constant integer BLOODLUST				= 0		//Can be detected with RegisterCast.
	constant integer SLOW					= 1		//Can be detected with RegisterCast.
	constant integer INNER_FIRE				= 2		//Can be detected with RegisterCast.
	constant integer UNHOLY_FRENZY				= 3		//Can be detected with RegisterCast.
	constant integer ROAR					= 4		//Can be detected with RegisterCast.
	constant integer CRIPPLE				= 5		//Can be detected with RegisterCast.
	constant integer SILENCE				= 6		//Can be detected with RegisterCast.
	constant integer SOULBURN				= 7		//Can be detected with RegisterCast.
	constant integer BERSERK				= 8		//Can be detected with RegisterCast.
	constant integer POISON_ATTACK				= 9		//Cannot be detected with RegisterCast.
	constant integer FROST_NOVA				= 10		//Can be detected with RegisterCast.
	constant integer FROST_ARMOR				= 10		//Cannot be detected with RegisterCast.
	constant integer COLD_ARROWS				= 11		//Cannot be detected with RegisterCast.
	constant integer THUNDERCLAP				= 12		//Cannot be detected with RegisterCast.

	//=========================================================================================================
	//Spell types used for attackModAura.create()
	//=========================================================================================================
	constant integer TRUESHOT_AURA				= 0
	constant integer COMMAND_AURA				= 1
	constant integer ENDURANCE_AURA				= 2
endglobals

static if ENABLE_CAST_DETECTION then
	struct attackModSpell
		integer abilityId
		integer buffId
		integer spellType

		static attackModSpell array list
		static integer listSize = 0

		static method create takes integer abilityId, integer buffId, integer spellType returns attackModSpell
			local attackModSpell ams
			local integer A = 1

			loop
			exitwhen A > listSize
				if list[A].abilityId == abilityId then
					return 0
				endif
				set A = A + 1
			endloop

			set ams = attackModSpell.allocate()

			set ams.abilityId = abilityId
			set ams.buffId = buffId
			set ams.spellType = spellType
			set listSize = listSize + 1
			set list[listSize] = ams
			return ams
		endmethod
	endstruct
endif

struct attackModAura
//=========================================================================================================
//All abilities that are the source of auras. Must be created for each source individually. Source can be
//a unit or an item. Set the other argument to null.
//=========================================================================================================
	integer abilityId
	integer buffId
	integer auraType
	unit unitSource
	item itemSource

	static attackModAura array list
	static integer listSize = 0

	static method create takes integer abilityId, integer buffId, integer auraType, unit unitSource, item itemSource returns attackModAura
		local attackModAura ama
		local integer A = 1
		local trigger trig

		set ama = attackModAura.allocate()
		set ama.abilityId = abilityId
		set ama.buffId = buffId
		set ama.auraType = auraType
		set ama.unitSource = unitSource
		set ama.itemSource = itemSource

		call auraBuff.create(buffId,auraType)

		set listSize = listSize + 1
		set list[listSize] = ama

		if unitSource != null and not IsUnitType(unitSource , UNIT_TYPE_HERO) then
			set trig = CreateTrigger()
			call TriggerRegisterUnitEvent( trig , unitSource , EVENT_UNIT_DEATH )
			call TriggerAddAction( trig , function attackModAura.OnDeath )
			call SaveInteger( buffTable , GetHandleId(trig) , 0 , ama )
			set trig = null
		endif
		return ama
	endmethod

	private static method OnDeath takes nothing returns nothing
		local trigger trig = GetTriggeringTrigger()
		local attackModAura ama = LoadInteger( buffTable , GetHandleId(trig) , 0 )
		call ama.destroy()
		call FlushChildHashtable( buffTable , GetHandleId(trig) )
		call DestroyTrigger(trig)
		set trig = null
	endmethod

	method onDestroy takes nothing returns nothing
		local integer A = 1
		loop
		exitwhen A > listSize
			if this == list[A] and A < listSize then
				set list[A] = list[listSize]
				exitwhen true
			endif
			set A = A + 1
		endloop
		set listSize = listSize - 1
	endmethod
endstruct

struct auraBuff
//=========================================================================================================
//All buffs that are granted by auras. Automatically generated by attackModAura.create(). Not part of the API!
//=========================================================================================================
	integer buffId
	integer auraType

	static auraBuff array list
	static integer listSize = 0

	static method create takes integer buffId, integer auraType returns auraBuff
		local auraBuff ab
		local integer A = 1

		loop
		exitwhen A > listSize
			if list[A].buffId == buffId then
				return 0
			endif
			set A = A + 1
		endloop

		set ab = auraBuff.allocate()
		set ab.buffId = buffId
		set ab.auraType = auraType

		set listSize = listSize + 1
		set list[listSize] = ab
		return ab	
	endmethod
endstruct

function SetAttackDamagePercentBuffs takes unit whichUnit, real attackDamage returns nothing
	call SaveReal( buffTable , GetHandleId(whichUnit) , 2 , attackDamage )
endfunction

function SetAttackSpeedPercentBuffs takes unit whichUnit, real attackSpeed returns nothing
	call SaveReal( buffTable , GetHandleId(whichUnit) , 1 , attackSpeed )
endfunction

function GetAttackDamagePercentBuffs takes unit whichUnit returns real
	return LoadReal( buffTable , GetHandleId(whichUnit) , 2 )
endfunction

function GetAttackSpeedPercentBuffs takes unit whichUnit returns real
	return LoadReal( buffTable , GetHandleId(whichUnit) , 1 )
endfunction

function GetAttackDamagePercentAuras takes unit whichUnit returns real
	local integer A = 1
	local integer B
	local attackModAura ama
	local auraBuff ab
	local real attackDamageBonus = 0
	local real dist
	local attackModAura highestAma = 0
	local integer highestLevel 
	local integer level

	loop
	exitwhen A > auraBuff.listSize
		set ab = auraBuff.list[A]
		set highestAma = 0
		set highestLevel = 0
		if GetUnitAbilityLevel( whichUnit , ab.buffId ) > 0 and ab.auraType != ENDURANCE_AURA then
			set B = 1
			loop
			exitwhen B > attackModAura.listSize
				set ama = attackModAura.list[B]
				if ama.auraType == ab.auraType then
					if ama.unitSource != null and UnitAlive(ama.unitSource) and IsUnitAlly(ama.unitSource , GetOwningPlayer(whichUnit)) then
						set dist = SquareRoot((GetUnitX(ama.unitSource) - GetUnitX(whichUnit))*(GetUnitX(ama.unitSource) - GetUnitX(whichUnit)) + (GetUnitY(ama.unitSource) - GetUnitY(whichUnit))*(GetUnitY(ama.unitSource) - GetUnitY(whichUnit)))
						set level = GetUnitAbilityLevel(ama.unitSource , ama.abilityId)
						if dist < BlzGetAbilityRealLevelField( BlzGetUnitAbility( ama.unitSource , ama.abilityId ) , ABILITY_RLF_AREA_OF_EFFECT , level - 1 ) and level > highestLevel then
							set highestLevel = level
							set highestAma = ama
						endif
					elseif ama.itemSource != null then
						if highestLevel == 0 then
							set highestLevel = 1
							set highestAma = ama
						endif
					endif
				endif
				set B = B + 1
			endloop
			if highestAma != 0 then
				if highestAma.unitSource != null then
					set attackDamageBonus = attackDamageBonus + BlzGetAbilityRealLevelField( BlzGetUnitAbility(highestAma.unitSource , highestAma.abilityId) , DATA_A , highestLevel - 1 )
				else
					set attackDamageBonus = attackDamageBonus + BlzGetAbilityRealLevelField( BlzGetItemAbility(highestAma.itemSource , highestAma.abilityId) , DATA_A , 0 )
				endif
			endif
		endif
		set A = A + 1
	endloop
	return attackDamageBonus
endfunction

function GetAttackSpeedPercentAuras takes unit whichUnit returns real
	local integer A = 1
	local integer B
	local attackModAura ama
	local auraBuff ab
	local real attackSpeedBonus = 0
	local real dist
	local attackModAura highestAma = 0
	local integer highestLevel 
	local integer level

	loop
	exitwhen A > auraBuff.listSize
		set ab = auraBuff.list[A]
		set highestAma = 0
		set highestLevel = 0
		if GetUnitAbilityLevel( whichUnit , ab.buffId ) > 0 and ab.auraType == ENDURANCE_AURA then
			set B = 1
			loop
			exitwhen B > attackModAura.listSize
				set ama = attackModAura.list[B]
				if ama.auraType == ab.auraType then
					if ama.unitSource != null and UnitAlive(ama.unitSource) and IsUnitAlly(ama.unitSource , GetOwningPlayer(whichUnit)) then
						set dist = SquareRoot((GetUnitX(ama.unitSource) - GetUnitX(whichUnit))*(GetUnitX(ama.unitSource) - GetUnitX(whichUnit)) + (GetUnitY(ama.unitSource) - GetUnitY(whichUnit))*(GetUnitY(ama.unitSource) - GetUnitY(whichUnit)))
						set level = GetUnitAbilityLevel(ama.unitSource , ama.abilityId)
						if dist < BlzGetAbilityRealLevelField( BlzGetUnitAbility( ama.unitSource , ama.abilityId ) , ABILITY_RLF_AREA_OF_EFFECT , level - 1 ) and level > highestLevel then
							set highestLevel = level
							set highestAma = ama
						endif
					elseif ama.itemSource != null then
						if highestLevel == 0 then
							set highestLevel = 1
							set highestAma = ama
						endif
						exitwhen true
					endif
				endif
				set B = B + 1
			endloop
			if highestAma != 0 then
				if highestAma.unitSource != null then
					set attackSpeedBonus = attackSpeedBonus + BlzGetAbilityRealLevelField( BlzGetUnitAbility(highestAma.unitSource , highestAma.abilityId) , DATA_B , highestLevel - 1 )
				else
					set attackSpeedBonus = attackSpeedBonus + BlzGetAbilityRealLevelField( BlzGetItemAbility(highestAma.itemSource , highestAma.abilityId) , DATA_B , 0 )
				endif
			endif
		endif
		set A = A + 1
	endloop
	return attackSpeedBonus
endfunction

private function AttackModifyingBuffCheck takes nothing returns nothing
	local timer t = GetExpiredTimer()
	local integer Ht = GetHandleId(t)
	local unit whichUnit = LoadUnitHandle( buffTable , Ht , 1 )
	local integer buffId
	local real attackSpeed
	local real attackDamage

	if UnitAlive(whichUnit) then
		set buffId = LoadInteger( buffTable , Ht , 0 )
		if GetUnitAbilityLevel( whichUnit , buffId ) == 0 then
			set attackSpeed = GetAttackSpeedPercentBuffs(whichUnit) - LoadReal(buffTable, Ht, 2)
			set attackDamage = GetAttackDamagePercentBuffs(whichUnit) - LoadReal(buffTable, Ht, 3)

			if attackSpeed == 0 and attackDamage == 0 then
				call FlushChildHashtable( buffTable , GetHandleId(whichUnit) )
			else
				call RemoveSavedHandle( buffTable , GetHandleId(whichUnit) , buffId )
				call SetAttackSpeedPercentBuffs( whichUnit , attackSpeed )
				call SetAttackDamagePercentBuffs( whichUnit , attackDamage )
			endif

			call FlushChildHashtable( buffTable , Ht )
			call DestroyTimer(t)
		endif
	else
		call FlushChildHashtable( buffTable , GetHandleId(whichUnit) )
		call FlushChildHashtable( buffTable , Ht )
		call DestroyTimer(t)
	endif
	set t = null
endfunction

function AddAttackModifyingBuff takes unit whichUnit, unit source, integer abilityId, integer buffId, integer spellType returns nothing
	local timer t
	local integer Ht
	local integer Hu = GetHandleId(whichUnit)
	local ability abi
	local real attackSpeed
	local real attackDamage
	local real oldAttackSpeed = 0
	local real oldAttackDamage = 0
	local integer level

	static if IGNORE_NONHERO_UNITS then
		if not IsUnitType(whichUnit , UNIT_TYPE_HERO) then
			return
		endif
	endif

	if GetUnitAbilityLevel( whichUnit , buffId ) > 0 and HaveSavedHandle( buffTable , Hu , buffId ) then
		set t = LoadTimerHandle( buffTable , Hu , buffId )
		set Ht = GetHandleId(t)
		set oldAttackSpeed = LoadReal( buffTable , Ht , 2 )
		set oldAttackDamage = LoadReal( buffTable , Ht , 3 )
	else
		set t = CreateTimer()
		set Ht = GetHandleId(t)
		call TimerStart( t , BUFF_CHECK_INTERVAL , true , function AttackModifyingBuffCheck )
		call SaveTimerHandle( buffTable , Hu , buffId , t )
		call SaveInteger( buffTable , Ht , 0 , buffId )
		call SaveUnitHandle( buffTable , Ht , 1 , whichUnit )
	endif

	set abi = BlzGetUnitAbility(source,abilityId)
	set level = GetUnitAbilityLevel(source,abilityId)

	if spellType == FROST_NOVA then
		set attackSpeed = -0.25
		set attackDamage = 0
	else
		if attackSpeedWhichField[spellType] != null then
			set attackSpeed = dataSign[spellType] * BlzGetAbilityRealLevelField( abi , attackSpeedWhichField[spellType] , level-1 )
		else
			set attackSpeed = 0
		endif
		if attackDamageWhichField[spellType] != null then
			set attackDamage = dataSign[spellType] * BlzGetAbilityRealLevelField( abi , attackDamageWhichField[spellType] , level-1 )
		else
			set attackDamage = 0
		endif
	endif

	if attackSpeed != oldAttackSpeed then
		call SetAttackSpeedPercentBuffs(whichUnit , GetAttackSpeedPercentBuffs(whichUnit) + attackSpeed - oldAttackSpeed )
		call SaveReal( buffTable , Ht , 2 , attackSpeed )
	endif
	if attackDamage != oldAttackDamage then
		call SetAttackDamagePercentBuffs(whichUnit , GetAttackDamagePercentBuffs(whichUnit) + attackDamage - oldAttackDamage )
		call SaveReal( buffTable , Ht , 3 , attackDamage )
	endif
	set t = null
endfunction

static if ENABLE_CAST_DETECTION then
	private function RegisterCast takes nothing returns nothing
		local integer abilityId = GetSpellAbilityId()
		local unit caster
		local integer A = 1
		local attackModSpell ams

		loop
		exitwhen A > attackModSpell.listSize
			if abilityId == attackModSpell.list[A].abilityId then
				set caster = GetSpellAbilityUnit()
				set ams = attackModSpell.list[A]
				if ams.spellType == BERSERK then
					call AddAttackModifyingBuff(caster, caster, abilityId, ams.buffId, ams.spellType)
				elseif ams.spellType == THUNDERCLAP then
				else
					call AddAttackModifyingBuff(GetSpellTargetUnit(), caster, abilityId, ams.buffId, ams.spellType)
				endif
				exitwhen true
			endif
			set A = A + 1
		endloop
	endfunction
endif

private function Init takes nothing returns nothing

	static if ENABLE_CAST_DETECTION then
		local trigger trig = CreateTrigger()
		call TriggerRegisterAnyUnitEventBJ( trig, EVENT_PLAYER_UNIT_SPELL_EFFECT )
		call TriggerAddCondition( trig, function RegisterCast )
		set trig = null
	endif

	set attackSpeedWhichField[BLOODLUST] 		= DATA_A
	set attackDamageWhichField[BLOODLUST] 		= null
	set dataSign[BLOODLUST] 			= 1

	set attackSpeedWhichField[SLOW] 		= DATA_B
	set attackDamageWhichField[SLOW] 		= null
	set dataSign[SLOW] 				= -1

	set attackSpeedWhichField[INNER_FIRE] 		= null
	set attackDamageWhichField[INNER_FIRE] 		= DATA_A
	set dataSign[INNER_FIRE] 			= 1

	set attackSpeedWhichField[UNHOLY_FRENZY] 	= DATA_A
	set attackDamageWhichField[UNHOLY_FRENZY] 	= null
	set dataSign[UNHOLY_FRENZY] 			= 1

	set attackSpeedWhichField[ROAR] 		= null
	set attackDamageWhichField[ROAR] 		= DATA_A
	set dataSign[ROAR] 				= 1

	set attackSpeedWhichField[CRIPPLE] 		= DATA_B
	set attackDamageWhichField[CRIPPLE] 		= DATA_C
	set dataSign[CRIPPLE] 				= -1

	set attackSpeedWhichField[SILENCE] 		= DATA_D
	set attackDamageWhichField[SILENCE] 		= null
	set dataSign[SILENCE] 				= -1

	set attackSpeedWhichField[SOULBURN] 		= DATA_E
	set attackDamageWhichField[SOULBURN] 		= DATA_C
	set dataSign[SOULBURN] 				= -1

	set attackSpeedWhichField[BERSERK] 		= DATA_B
	set attackDamageWhichField[BERSERK] 		= null
	set dataSign[BERSERK] 				= 1

	set attackSpeedWhichField[POISON_ATTACK] 	= DATA_C
	set attackDamageWhichField[POISON_ATTACK] 	= null
	set dataSign[POISON_ATTACK] 			= -1

	set attackSpeedWhichField[COLD_ARROWS] 		= DATA_C
	set attackDamageWhichField[COLD_ARROWS] 	= null
	set dataSign[COLD_ARROWS] 			= -1

	set attackSpeedWhichField[THUNDERCLAP] 		= DATA_D
	set attackDamageWhichField[THUNDERCLAP] 	= null
	set dataSign[THUNDERCLAP] 			= -1
endfunction

endlibrary



library UnitAttackStats requires optional AttackModBuffs

globals
	//=========================================================================================================

	constant real ATTACK_SPEED_PER_AGILITY	= 0.02			//The amount of attack speed gained from 1 point of agility.

	constant boolean CHECK_BUFFS		= true			//Requires AttackModBuffs to be implemented.
	constant boolean CHECK_AURAS		= true			//Requires AttackModBuffs to be implemented.
	constant boolean CHECK_ITEMS		= true

	//=========================================================================================================

	private constant abilityintegerlevelfield DATA_ATTACK_DAMAGE	= ABILITY_ILF_NUMBER_OF_WAVES
	private constant abilityreallevelfield DATA_ATTACK_SPEED	= ABILITY_RLF_DEFENSE_BONUS_HAV1
endglobals

static if CHECK_ITEMS then
	struct attackDamageItemAbility
		integer abilityId

		static attackDamageItemAbility array list
		static integer listSize = 0

		static method create takes integer abilityId returns attackDamageItemAbility
			local attackDamageItemAbility adia
			local integer A = 1

			loop
			exitwhen A > listSize
				if list[A].abilityId == abilityId then
					return 0
				endif
				set A = A + 1
			endloop

			set adia = attackDamageItemAbility.allocate()

			set adia.abilityId = abilityId
			set listSize = listSize + 1
			set list[listSize] = adia
			return adia
		endmethod
	endstruct

	struct attackSpeedItemAbility
		integer abilityId

		static attackSpeedItemAbility array list
		static integer listSize = 0

		static method create takes integer abilityId returns attackSpeedItemAbility
			local attackSpeedItemAbility asia
			local integer A = 1

			loop
			exitwhen A > listSize
				if list[A].abilityId == abilityId then
					return 0
				endif
				set A = A + 1
			endloop

			set asia = attackSpeedItemAbility.allocate()

			set asia.abilityId = abilityId
			set listSize = listSize + 1
			set list[listSize] = asia
			return asia	
		endmethod
	endstruct
endif

function GetUnitAttackDamage takes unit whichUnit, integer weaponIndex returns real
	local integer A = 0
	local integer B
	local integer C
	local integer baseDamage = BlzGetUnitBaseDamage(whichUnit,weaponIndex)
	local real diceDamage = BlzGetUnitDiceNumber(whichUnit,weaponIndex)*I2R( 1+ BlzGetUnitDiceSides(whichUnit,weaponIndex))/2
	local integer flatDmgBonus = 0
	local item tempItem
	local ability itemAbi
	local integer id
	local real damageModPercent = 0

	static if LIBRARY_AttackModBuffs then
		static if CHECK_BUFFS then
			set damageModPercent = damageModPercent + GetAttackDamagePercentBuffs(whichUnit)
		endif
		static if CHECK_AURAS then
			set damageModPercent = damageModPercent + GetAttackDamagePercentAuras(whichUnit)
		endif
	endif

	static if CHECK_ITEMS then
		if IsHeroUnitId(GetUnitTypeId(whichUnit)) then
			loop
			exitwhen A > 5
				set tempItem = UnitItemInSlot(whichUnit,A)
				if tempItem != null then
					set B = 0
					loop
					exitwhen B > 3
						set itemAbi = BlzGetItemAbilityByIndex(tempItem,B)
						set id = BlzGetAbilityId(itemAbi)
						set C = 1
						loop
						exitwhen C > attackDamageItemAbility.listSize
							if id == attackDamageItemAbility.list[C].abilityId then
								set flatDmgBonus = flatDmgBonus + BlzGetAbilityIntegerLevelField( itemAbi , DATA_ATTACK_DAMAGE , 0 )
								exitwhen true
							endif
							set C = C + 1
						endloop
						set B = B + 1
					endloop
				endif
				set A = A + 1
			endloop
		endif
	endif

	//============================================================
	//Add additional damage bonus abilities here.
	//============================================================
	//set flatDmgBonus = flatDmgBonus + <Attack damage bonus abilities based on Item Damage Bonus>.

	set baseDamage = R2I(baseDamage + (baseDamage+diceDamage)*damageModPercent + flatDmgBonus + 0.5)
	return baseDamage + diceDamage
endfunction

function GetUnitAttackSpeedBonus takes unit whichUnit returns real
	local integer A = 0
	local integer B
	local integer C
	local real attackSpeedBonusItems = 0
	local real attackSpeedBonusAgility = 0
	local real attackSpeedBonusBuffs = 0
	local real attackSpeedBonus = 0
	local item tempItem
	local ability itemAbi
	local integer id

	static if LIBRARY_AttackModBuffs then
		static if CHECK_BUFFS then
			set attackSpeedBonusBuffs = attackSpeedBonusBuffs + GetAttackSpeedPercentBuffs(whichUnit)
		endif
		static if CHECK_AURAS then
			set attackSpeedBonusBuffs = attackSpeedBonusBuffs + GetAttackSpeedPercentAuras(whichUnit)
		endif
	endif

	static if CHECK_ITEMS then
		if IsHeroUnitId(GetUnitTypeId(whichUnit)) then
			set attackSpeedBonusAgility = ATTACK_SPEED_PER_AGILITY*GetHeroAgi( whichUnit , true )
			loop
			exitwhen A > 5
				set tempItem = UnitItemInSlot(whichUnit,A)
				if tempItem != null then
					set B = 0
					loop
					exitwhen B > 3
						set itemAbi = BlzGetItemAbilityByIndex(tempItem,B)
						if itemAbi != null then
							set id = BlzGetAbilityId(itemAbi)
							set C = 1
							loop
							exitwhen C > attackSpeedItemAbility.listSize
								if id == attackSpeedItemAbility.list[C].abilityId then
									set attackSpeedBonusItems = attackSpeedBonusItems + BlzGetAbilityRealLevelField( itemAbi , DATA_ATTACK_SPEED , 0 )
									exitwhen true
								endif
								set C = C + 1
							endloop
						endif
						set B = B + 1
					endloop
				endif
				set A = A + 1
			endloop
		endif
	endif

	set attackSpeedBonus = attackSpeedBonusBuffs + attackSpeedBonusItems + attackSpeedBonusAgility
	if attackSpeedBonus < -0.8 then
		return -0.8
	elseif attackSpeedBonus > 4.0 then
		return 4.0
	else
		return attackSpeedBonus
	endif
endfunction

function GetUnitAttackDelay takes unit whichUnit, integer weaponIndex returns real
	return BlzGetUnitWeaponRealField(whichUnit , UNIT_WEAPON_RF_ATTACK_DAMAGE_POINT , weaponIndex) / (1 + GetUnitAttackSpeedBonus(whichUnit))
endfunction

function GetUnitAttackCooldown takes unit whichUnit, integer weaponIndex returns real
	return BlzGetUnitAttackCooldown(whichUnit,weaponIndex) / (1 + GetUnitAttackSpeedBonus(whichUnit))
endfunction

function GetUnitDPS takes unit whichUnit, integer weaponIndex returns real
	return GetUnitAttackDamage(whichUnit,weaponIndex) * (1 + GetUnitAttackSpeedBonus(whichUnit)) / BlzGetUnitAttackCooldown(whichUnit,weaponIndex)
endfunction

endlibrary



library TriggerOnAttack requires optional UnitAttackStats

globals
	//=========================================================================================================

	private constant real TIMESTEP_CHECK 		= 0.05		//How accurately the attack action is synchronized with the attack.
	private constant boolean CLEAN_UP_ON_DEATH 	= true		//If enabled, destroys all attack actions and removes memory leaks on death of nonhero units.

	//=========================================================================================================

	private hashtable multiTable = InitHashtable()
endglobals

private function interface triggerOnAttack takes unit whichUnit, unit whichTarget returns nothing

static if CLEAN_UP_ON_DEATH then
	private function OnDeath takes nothing returns nothing
		local unit whichUnit = GetTriggerUnit()
		local integer Hu = GetHandleId(whichUnit)
		local integer numberOfAttackActions = LoadInteger( multiTable , Hu , 0 )
		local trigger trig
		local integer A = 4
		local integer Ht

		//=========================================================================================================
		//On death, destroy all Attack Actions, Interrupt trigger and OnDeath trigger. Only on nonhero units. If a
		//nonherounit is revived, the Attack Actions must be added again.
		//=========================================================================================================
		loop
		exitwhen A > numberOfAttackActions + 3
			set trig = LoadTriggerHandle( multiTable , Hu , A )
			set Ht = GetHandleId(trig)
			call DestroyTimer(LoadTimerHandle( multiTable , Ht , 1 ))
			call FlushChildHashtable( multiTable , Ht )
			call DestroyTrigger(trig)
			set A = A + 1
		endloop

		call DestroyTrigger(LoadTriggerHandle( multiTable , Hu , 1 ))
		call DestroyTrigger(LoadTriggerHandle( multiTable , Hu , 2 ))
		call FlushChildHashtable( multiTable , Hu )
		set whichUnit = null
		set trig = null
	endfunction
endif

private function Interrupt takes nothing returns nothing
	local unit whichUnit = GetOrderedUnit()
	local integer Hu = GetHandleId(whichUnit)
	local unit whichTarget = LoadUnitHandle( multiTable , Hu , 3 )
	local integer numberOfAttackActions
	local integer A = 4
	local trigger trig

	//=========================================================================================================
	//If a command is given that isn't "attack" or "smart" or the target of the command isn't the attacked unit,
	//the attack was interrupted.
	//=========================================================================================================
	if (GetIssuedOrderId() != 851983 and GetIssuedOrderId() != 851971) or GetOrderTargetUnit() != whichTarget then
		call DisableTrigger(GetTriggeringTrigger())
		set numberOfAttackActions =  LoadInteger( multiTable , Hu , 0 )
		loop
		exitwhen A > numberOfAttackActions + 3
			set trig = LoadTriggerHandle( multiTable , Hu , A )
			call PauseTimer( LoadTimerHandle( multiTable , GetHandleId(trig) , 1 ) )
			set A = A + 1
		endloop
	endif
endfunction

private function Check takes nothing returns nothing
	local timer t = GetExpiredTimer()
	local integer H = GetHandleId(t)
	local integer counter = LoadInteger( multiTable , H , 0 ) + 1
	local integer steps = LoadInteger( multiTable , H , 1 )
	local unit whichUnit = LoadUnitHandle( multiTable , H , 2 )
	local unit whichTarget
	local triggerOnAttack whichCallback
	local real x = LoadReal( multiTable , H , 5 )
	local real y = LoadReal( multiTable , H , 6 )

	if counter == steps then
		//=========================================================================================================
		//Execute Attack Action, then disable Interrupt trigger.
		//=========================================================================================================
		set whichTarget = LoadUnitHandle( multiTable , GetHandleId(whichUnit) , 3 )
		set whichCallback = LoadInteger( multiTable , H , 4 )
		call whichCallback.evaluate(whichUnit,whichTarget)
		call FlushChildHashtable( multiTable , H )
		call DisableTrigger( LoadTriggerHandle( multiTable , GetHandleId(whichUnit) , 2 ) )
		call PauseTimer(t)
	elseif x == GetUnitX(whichUnit) and y == GetUnitY(whichUnit) then
		//=========================================================================================================
		//Check if unit has moved. If so, the attack was interrupted.
		//=========================================================================================================
		call SaveInteger( multiTable , H , 0 , counter )
	else
		//=========================================================================================================
		//Unit has moved. Interrupt check.
		//=========================================================================================================
		call FlushChildHashtable( multiTable , H )
		call PauseTimer(t)
	endif
endfunction

private function OnAttack takes nothing returns nothing
	//=========================================================================================================
	//Registered beginning of attack. Wait for attack.
	//=========================================================================================================
	local unit whichUnit = GetTriggerUnit()
	local unit whichTarget = GetEventTargetUnit()
	local trigger trig = GetTriggeringTrigger()
	local integer Ht = GetHandleId(trig)
	local timer t = LoadTimerHandle( multiTable , Ht , 1 )
	local real procChance = LoadReal( multiTable , Ht , 2 )
	local integer steps
	local integer H = GetHandleId(t)
	local integer Hu

	if procChance == 1 or GetRandomReal(0,1) < procChance then
		static if LIBRARY_UnitAttackStats then
			set steps = R2I(GetUnitAttackDelay(whichUnit,0)/TIMESTEP_CHECK) + 1			//Time until projectile is launched.
		else
			set steps = R2I(BlzGetUnitWeaponRealField(whichUnit , UNIT_WEAPON_RF_ATTACK_DAMAGE_POINT , 0)/TIMESTEP_CHECK) + 1
		endif
		set Hu = GetHandleId(whichUnit)
		call EnableTrigger(LoadTriggerHandle( multiTable , Hu , 2 ))			//Interrupt trigger.
		call SaveUnitHandle( multiTable , Hu , 3 , whichTarget )
		call TimerStart( t , TIMESTEP_CHECK , true , function Check )
		call SaveInteger( multiTable , H , 0 , 0 )					//counter
		call SaveInteger( multiTable , H , 1 , steps )
		call SaveUnitHandle( multiTable , H , 2 , whichUnit )
		call SaveInteger( multiTable , H , 4 , LoadInteger( multiTable , Ht , 0 ) )	//callback
		call SaveReal( multiTable , H , 5 , GetUnitX(whichUnit) )
		call SaveReal( multiTable , H , 6 , GetUnitY(whichUnit) )
	endif
endfunction

function AddUnitAttackAction takes unit whichUnit, triggerOnAttack whichCallback, real procChance, boolean isStacking returns nothing
	local trigger trig
	local timer t
	local integer Ht
	local integer Hu = GetHandleId(whichUnit)
	local integer numberOfAttackActions = LoadInteger( multiTable , Hu , 0 )
	local integer A
	local integer L
	local triggerOnAttack thisTriggerCallback

	if not isStacking then
		set A = 4
		set L = numberOfAttackActions + 3
		loop
		exitwhen A > L
			set trig = LoadTriggerHandle( multiTable , Hu , A )
			set Ht = GetHandleId(trig)
			set thisTriggerCallback = LoadInteger( multiTable , Ht , 0 )
			if thisTriggerCallback == whichCallback then
				call SaveInteger( multiTable , Ht , 3 , LoadInteger( multiTable , Ht , 3 ) + 1 )
				set trig = null
				return
			endif
			set A = A + 1
		endloop
	endif

 	set t = CreateTimer()

	//=========================================================================================================
	//Trigger for detecting beginning of attack.
	//=========================================================================================================
	set trig = CreateTrigger()
	set Ht = GetHandleId(trig)

	set numberOfAttackActions = numberOfAttackActions + 1
		
	call TriggerRegisterUnitEvent( trig , whichUnit , EVENT_UNIT_TARGET_IN_RANGE )
	call TriggerAddAction( trig , function OnAttack )
	call SaveInteger( multiTable , Ht , 0 , whichCallback )
	call SaveTimerHandle( multiTable , Ht , 1 , t )
	call SaveReal( multiTable , Ht , 2 , procChance )
	call SaveInteger( multiTable , Ht , 3 , 1 )			//Counter for non-stacking effects.
	call SaveInteger( multiTable , Hu , 0 , numberOfAttackActions )
	call SaveTriggerHandle( multiTable , Hu , numberOfAttackActions + 3 , trig )

	if numberOfAttackActions == 1 then
		//=========================================================================================================
		//If it's the first Attack Action added to that unit, add an OnDeath trigger for cleanup.
		//=========================================================================================================
		static if CLEAN_UP_ON_DEATH then
			if not IsUnitType(whichUnit , UNIT_TYPE_HERO) then
				set trig = CreateTrigger()
				call TriggerRegisterUnitEvent( trig , whichUnit , EVENT_UNIT_DEATH )
				call TriggerAddAction( trig , function OnDeath )
				call SaveTriggerHandle( multiTable , Hu , 1 , trig )
			endif
		endif

		//=========================================================================================================
		//Trigger for detecting the attack being interrupted. The same for all attack actions.
		//=========================================================================================================
		set trig = CreateTrigger()
		call TriggerRegisterUnitEvent( trig , whichUnit , EVENT_UNIT_ISSUED_ORDER )
		call TriggerRegisterUnitEvent( trig , whichUnit , EVENT_UNIT_ISSUED_POINT_ORDER )
		call TriggerRegisterUnitEvent( trig , whichUnit , EVENT_UNIT_ISSUED_TARGET_ORDER )
		call TriggerAddAction( trig , function Interrupt )
		call SaveTriggerHandle( multiTable , Hu , 2 , trig)
		call DisableTrigger(trig)
	endif

	set trig = null
	set t = null
endfunction

function RemoveUnitAttackAction takes unit whichUnit, triggerOnAttack whichCallback returns nothing
	local trigger trig
	local triggerOnAttack thisTriggerCallback
	local integer Hu = GetHandleId(whichUnit)
	local integer Ht
	local integer numberOfAttackActions = LoadInteger( multiTable , Hu , 0 )
	local integer A = 4
	local integer L = numberOfAttackActions + 3
	local integer counter

	//=========================================================================================================
	//Search for attack action that has the effect that should be removed.
	//=========================================================================================================
	loop
	exitwhen A > L
		set trig = LoadTriggerHandle( multiTable , Hu , A )
		set Ht = GetHandleId(trig)
		set counter = LoadInteger( multiTable , Ht , 3)
		if counter > 1 then
			call SaveInteger( multiTable , Ht , 3 , counter - 1 )
			set trig = null
			return
		endif

		set thisTriggerCallback = LoadInteger( multiTable , Ht , 0 )

		if thisTriggerCallback == whichCallback then
			set numberOfAttackActions = numberOfAttackActions - 1
			call SaveInteger( multiTable , Hu , 0 , numberOfAttackActions )
			call DestroyTimer(LoadTimerHandle( multiTable , Ht , 1 ))
			if A < L then
				call SaveTriggerHandle( multiTable , Hu , A , LoadTriggerHandle( multiTable , Hu , L ) )
				call RemoveSavedHandle( multiTable , Hu , L )
			endif
			call FlushChildHashtable( multiTable , Ht )
			call DestroyTrigger(trig)
			exitwhen true
		endif
		set A = A + 1
	endloop

	if numberOfAttackActions == 0 then
		if not IsUnitType(whichUnit , UNIT_TYPE_HERO) then
			call DestroyTrigger(LoadTriggerHandle(multiTable , Hu , 1))	//On death trigger.
		endif
		call DestroyTrigger(LoadTriggerHandle(multiTable , Hu , 2))	//Interrupt trigger.
	endif

	set trig = null
endfunction

function UnitClearAllAttackActions takes unit whichUnit returns nothing
	local integer Hu = GetHandleId(whichUnit)
	local integer numberOfAttackActions = LoadInteger( multiTable , Hu , 0 )
	local trigger trig
	local integer A = 4
	local integer Ht

	loop
	exitwhen A > numberOfAttackActions + 3
		set trig = LoadTriggerHandle( multiTable , Hu , A )
		set Ht = GetHandleId(trig)
		call DestroyTimer(LoadTimerHandle( multiTable , Ht , 1 ))
		call FlushChildHashtable( multiTable , Ht )
		call DestroyTrigger(trig)
		set A = A + 1
	endloop

	call DestroyTrigger(LoadTriggerHandle( multiTable , Hu , 1 ))
	call DestroyTrigger(LoadTriggerHandle( multiTable , Hu , 2 ))
	call FlushChildHashtable( multiTable , Hu )
	set whichUnit = null
	set trig = null
endfunction

endlibrary
Contents

Accurate Attack Stats & Triggers (Map)

Reviews
Wrda
attackModAura's onDestroy method can become faster if you add a new member to the struct, of type integer. It would link the instance to the index of the list. This is worth optimizing, in GetAttackSpeedPercentAuras function. set dist =...

Bribe

Code Moderator
Level 48
Joined
Sep 26, 2009
Messages
9,264
We should collaborate on this, imo.


My solution above will account to 100% accuracy, as long as attack speed does not change mid-attack.

I'm not really sure what you're doing with the target-in-range event, but I think our goals have an overlap.
 
We should collaborate on this, imo.

I would love to and I am happy to share what I've found out, but I'm afraid what you're doing is beyond my expertise. That seems really advanced.

My solution above will account to 100% accuracy, as long as attack speed does not change mid-attack.
That's a source of inaccuracy for my system as well. It shouldn't be too hard to account for buffs being applied and falling off mid-animation - just go into the check loop and calculate a new wait time. But it's really a minor issue.

I'm not really sure what you're doing with the target-in-range event, but I think our goals have an overlap.
Target-in-range is the same as Unit-is-attacked from what I can tell, just in reverse. I find it easier to trigger it this way.
 

Wrda

Spell Reviewer
Level 23
Joined
Nov 18, 2012
Messages
1,741
attackModAura's onDestroy method can become faster if you add a new member to the struct, of type integer. It would link the instance to the index of the list.
JASS:
    static method create takes integer abilityId, integer buffId, integer auraType, unit unitSource, item itemSource returns attackModAura
        local attackModAura ama
        local integer A = 1
        local trigger trig

        set ama = attackModAura.allocate()
        set ama.abilityId = abilityId
        set ama.buffId = buffId
        set ama.auraType = auraType
        set ama.unitSource = unitSource
        set ama.itemSource = itemSource

        call auraBuff.create(buffId,auraType)

        set listSize = listSize + 1
        set ama.data = listSize //new member
        set list[listSize] = ama

        if unitSource != null and not IsUnitType(unitSource , UNIT_TYPE_HERO) then
            set trig = CreateTrigger()
            call TriggerRegisterUnitEvent( trig , unitSource , EVENT_UNIT_DEATH )
            call TriggerAddAction( trig , function attackModAura.OnDeath )
            call SaveInteger( buffTable , GetHandleId(trig) , 0 , ama )
            set trig = null
        endif
        return ama
    endmethod
   
    method onDestroy takes nothing returns nothing
        list[this.data] = list[listSize] //simplified
        set listSize = listSize - 1
    endmethod

This is worth optimizing, in GetAttackSpeedPercentAuras function.
JASS:
set dist = SquareRoot((GetUnitX(ama.unitSource) - GetUnitX(whichUnit))*(GetUnitX(ama.unitSource) - GetUnitX(whichUnit)) + (GetUnitY(ama.unitSource) - GetUnitY(whichUnit))*(GetUnitY(ama.unitSource) - GetUnitY(whichUnit)))

Missing the nulling of the caster variable after the loop to avoid reference leaks, in RegisterCast. Also, there's nothing inside the elseif ams.spellType == THUNDERCLAP then block, is it incomplete?
Missing the nulling of unit and triggers at the end of the functions in Interrupt, Check, OnAttack.

Perhaps you can create a function that would replace the condition of Interrupt function and it would be near the configurable part as to make it easier for the user. You also should mention the range of procChance, which you're using 0.-1. and could have been 0.-100.

There's absolutely no doubt this is a innovative way to detect "on attack" or "projectile launched" event on units, very useful to create all sorts of effects.

Approved
 
Thank you for the approval!

attackModAura's onDestroy method can become faster if you add a new member to the struct, of type integer. It would link the instance to the index of the list.
Hm, I don't really know why I wrote it like this. I usually add the listindex as a member in all my structs that I write in this way. I probably just wrote it real quick to check if this method even works and then forgot about it.

Missing the nulling of the caster variable after the loop to avoid reference leaks, in RegisterCast. Also, there's nothing inside the elseif ams.spellType == THUNDERCLAP then block, is it incomplete?
Hm, I think I expected there to be no way I can write the logic myself so that it encompasses all ways in which the user might modify Thunderclap, so I decided to leave it blank and let the user write the logic regarding who gets affected by Thunderclap, but I forgot to put that in the documentation.

Most of these fixes should be real easy! Will do it sometime next week!
 
I assume this system can instantly get a unit's attack damage (sum of white and red/green damage), is that correct?
And all that is needed is declaring all damage-modifying buffs in advance.
Yes, if you correctly account for all buffs, auras, and item abilities, the attack damage (mean damage) will be instantly 100% accurate, except in some fringe cases with auras.
 
What might those fringe cases be? Would they be related to changing aura value?
If there is a Tauren Chieftain with Level 3 Endurance Aura in range of the unit as well as a hero carrying an Ancient Janggo of Endurance and you change the value of the Janggo to be much higher than those of the Tauren Chieftain's aura, I believe the item aura should overwrite the Tauren Chieftain's aura (correct me if I'm wrong), but my system thinks that the Tauren Chieftain's aura applies because it is a higher level.

There's also the issue that the system checks aura sources within range, but auras persist for 1 or 2 seconds after the source leaves the area of effect.

Modifying an Endurance Aura to affect enemy units will not work, but you can change that easily by changing the line
Code:
					if ama.unitSource != null and UnitAlive(ama.unitSource) and IsUnitAlly(ama.unitSource , GetOwningPlayer(whichUnit)) then
in GetAttackDamagePercentAuras with the correct logic for your auras.

All of these issues could be eliminated by changing the function to cater to your specific needs.

Changing aura values (I assume you mean changing stuff with BlzSetAbilityRealField etc.) should work just fine.
 
Negative values aren't the issue. The issue is that I can't check whether a unit is one of the targets allowed for an aura, so the system just assumes that the auras affect all friendly units in range. But as I said, that can be changed easily by modifying that line and removing
Code:
IsUnitAlly(ama.unitSource , GetOwningPlayer(whichUnit))
from the condition or replacing it with a condition that better suits your map.
 
Well, there could be the situation that you have a Tauren Chieftain with a level 1 Endurance Aura and the opponent has one with level 3, both in range of the unit you're checking. Then the system would think that the level of the aura on your unit is 3.

I should probably make this into an adjustable parameter in the configuration.
 
Well, there could be the situation that you have a Tauren Chieftain with a level 1 Endurance Aura and the opponent has one with level 3, both in range of the unit you're checking. Then the system would think that the level of the aura on your unit is 3.
Would this not be a problem if the 2 Endurance Aura abilities are different from each other and using different buffs?
Essentially, this is only a problem if the aura ability can be acquired by both enemy and ally.
 
Yes, if the buffs of the auras are different, there is no issue.

The sequence is:
-Loop through all the buffs that you have declared and check whether the unit has those buffs.
-If it's an aura buff, loop through all aura sources you declared and check if that aura is associated with that buff.
-(Check if source is allied with the unit and) retrieve the area of effect data of the aura from the source and check whether it is within that distance of the unit.
-If it is, store the level of the ability of the aura.
-After looping through all aura sources, go back to the source that was detected within range with the highest level of the aura ability, then retrieve the attack damage/attack speed bonus data from that source.
 
Made the updates that Wrda requested, although I'm not sure if nulling variables is necessary anymore. Some people are saying that has been fixed with Reforged.

Also added the ability to change whether an aura is affecting allies and/or enemies, added Thunderclap to RegisterCast (you can customize the filter function for your needs), and made the Interrupt condition its own function to make it more easily customized.
 

Wrda

Spell Reviewer
Level 23
Joined
Nov 18, 2012
Messages
1,741
Made the updates that Wrda requested, although I'm not sure if nulling variables is necessary anymore. Some people are saying that has been fixed with Reforged.
If possible, I would like to know where that information come from, since it's better to be updated with the info we have around, either confirming or denying things such as Memory Leaks
Perhaps they meant Lua doesn't suffer this problem?
 
If possible, I would like to know where that information come from, since it's better to be updated with the info we have around, either confirming or denying things such as Memory Leaks
Perhaps they meant Lua doesn't suffer this problem?
I just tested 100k reference leaks per second and after a few minutes the memory usage of wc3 hadn't increased at all. Maybe I'm missing something, but it seems to me that there's no leak.
 

Bribe

Code Moderator
Level 48
Joined
Sep 26, 2009
Messages
9,264
I just tested 100k reference leaks per second and after a few minutes the memory usage of wc3 hadn't increased at all. Maybe I'm missing something, but it seems to me that there's no leak.
The main reason for doing it was for systems which used stuff like H2I(handle) - 0x100000, which was done to lower the index of the handle ID to fit into an array.

If you don't have that stuff in your map, you should be fine. If you use Lua, then you ARE fine.
 
Top