• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Accurate Attack Stats & Triggers (now with GUI)

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)

Accurate Attack Stats & Triggers GUI (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 50
Joined
Sep 26, 2009
Messages
9,456
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 25
Joined
Nov 18, 2012
Messages
1,864
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 25
Joined
Nov 18, 2012
Messages
1,864
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 50
Joined
Sep 26, 2009
Messages
9,456
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.
 
A GUI interface would certainly be possible, but I'm not good at doing them. I don't know what GUI users expect. I'd need some help from someone or you show me a similar resource with a GUI interface whose approach that I can copy.
I think you can take a look at Bribe's GUI Spell System and GUI Damage Event as a reference.
What I usually see in GUI-friendly systems is that variable declarations are made at map initialization.
If a variable has to be registered multiple times to different values, globals are used, followed by Trigger - Run... for each set of variables.
 
Top