/**************************************************************************************************
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