• 🏆 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!

[vJASS] DamagePackage

Level 22
Joined
Feb 6, 2014
Messages
2,466
A modular Damage Detection System.
Contains libraries for your damage detection, distinction and manipulation needs.


JASS:
//! novjass
   
    /*
                             DamagePackage v1.44
                                Documentation
                                  by Flux
           
        Contains libraries for your damage detection, distinction and manipulation needs.
           
        -----------
        DamageEvent
        -----------
            A lightweight damage detection system that 
            detects when a unit takes damage.
            Can distinguish physical and magical damage.
       
        ------------
        DamageModify
        ------------
            An add-on to DamageEvent that allows modification 
            of damage taken before it is applied.
   
           
        -------------
        DamageObjects
        -------------
            Automatically generates required Objects by DamageEvent and
            DamageModify.
   
    CONTENTS:
        - API
        - How to use DamagePackage
        - Important Notes
        - Credits
        - Changelog
           
    //==================================================================//
                                      API:
    //==================================================================//
    */
   
    DamageEvent:
        -----------------------------
              DAMAGE PROPERTIES
        -----------------------------
        Damage.source 
            // Unit that dealt the damage.
        Damage.target 
            // Unit that took the damage.
        Damage.amount 
            // Amount of damage taken.
        Damage.type 
            // The type of damage taken.
            // Values can be DAMAGE_TYPE_PHYSICAL or DAMAGE_TYPE_MAGICAL.
           
        -----------------------------
               DAMAGE CALLBACKS
        -----------------------------
        Damage.register(code)
            // Registers a code that will permanently run when a registered 
            // unit takes damage.
        Damage.registerTrigger(trigger)
            // Registers a trigger that will run when a registered unit takes
            // damage. The trigger can be disabled to avoid an infinite loop.
            // Triggers will execute depending on the order they are registered.
        Damage.unregisterTrigger(trigger)
            // Removes the event in the trigger, causing the trigger to no longer
            // run when a registered unit takes damage.
        Damage.add(unit)
            // For manual registration of units to DamageEvent. You
            // won't use this if AUTO_REGISTER is set to true.
       
        -----------------------------
               MISCELLANEOUS
        -----------------------------
        Damage.enabled
            //Turns on/off the entire DamageEvent.
        Damage.lockAmount()
            //Prevents further modification of damage on this damage instance.
       
    DamageModify
        set Damage.amount = <new amount>
            // Modify the damage taken before it is applied.
       
        Damage.registerModifier(code)
            // Registers a code that will permanently run when a 
            // registered unit takes damage executing before any callbacks 
            // registered via Damage.register(code)/Damage.registerTrigger(trigger).  
       
        Damage.registerModifierTrigger(trigger)
            // Registers a trigger that will run when a registered unit 
            // takes damage executing before any callbacks registered
            // via Damage.register(code)/Damage.registerTrigger(trigger).
            // The trigger can be disabled to avoid an infinite loop.
       
        Damage.unregisterModifierTrigger(trigger)
            // Removes the event in the trigger, it will no longer evaluate
            // and execute when a registered unit takes damage.

    /*
   
    //==================================================================//
                          HOW TO USE DAMAGE PACKAGE:
    //==================================================================//
   
        1. Decide whether you need to use DamageEvent or DamageEvent with DamageModify.
           If you only want to detect the damage and the type of damage, DamageEvent
           will suffice, but if you want to modify the Damage taken, then you need
           DamageModify. Note that DamageEvent without DamageModify is designed to be
           lightweight, therefore it is better not to have DamageModify if you do not 
           need it. Using DamageModify is 90 to 100 microseconds slower.
       
        2. Define Basic configuration of DamageEvent
            */
            private constant integer DAMAGE_TYPE_DETECTOR
                //An ability based on Runed Bracer that is utilized by DamageEvent to distinguish
                //PHYSICAL and MAGICAL damage.
               
            private constant real ETHEREAL_FACTOR
                //Using DamageEvent disables the ethereal factor configured in Gameplay Constants as
                //a side effect of Runed Bracer. However, the system simulates ethereal amplification 
                //and this is the new ethereal factor for magic damage. The configured ethereal factor
                //in Gameplay Constants will be completely ignored.
            .
            private constant boolean AUTO_REGISTER
                //Determines whether units entering the map are automatically registered 
                //to the DamageEvent system
           
            private constant boolean PREPLACE_INIT
                //Auto registers units initially placed in World Editor.
               
            private constant integer COUNT_LIMIT
                //When the number of registered individual unit in the current DamageBucket
                //reaches COUNT_LIMIT, the system will find a new DamageBucket with units less
                //than COUNT_LIMIT and use it as the new current DamageBucket. If none is found, 
                //the system will create a new DamageBucket.
               
            private constant real REFRESH_TIMEOUT
                //Periodic Timeout of Trigger Refresh.
                //Every REFRESH_TIMEOUT, the system will refresh a signle DamageBucket.
           
            private constant integer SET_MAX_LIFE
                //An ability based on Item Life Bonus that is utilized by DamageModify to
                //manipulate damage taken.
        /*
           
           
        3. Register a code or a trigger that will run when a registered unit takes damage. Example:
            */
            //USING CODE PARAMETER
                library L initializer Init
                   
                    private function OnDamage takes nothing returns nothing
                        //This will run whenever a unit registered takes damage.
                        //Do you thing here
                    endfunction
                   
                    private function Init takes nothing returns nothing
                        call Damage.register(function OnDamage)
                    endfunction
                   
                endlibrary
           
            //USING TRIGGER PARAMETER
                library L initializer Init
                   
                    globals
                        private trigger trg = CreateTrigger()
                    endglobals
                   
                    private function OnDamage takes nothing returns boolean
                        //This will run whenever a unit registered takes damage.
                        return false
                    endfunction
                   
                    private function Init takes nothing returns nothing
                        call Damage.registerTrigger(trg)
                        call TriggerAddCondition(trg, Condition(function OnDamage))
                    endfunction
                   
                endlibrary
               
                // You would want to use Damage.registerTrigger when avoiding recursion loop
                // because the trigger can be disabled unlike code.
           
            /*
       
        4. If you want to modify the damage taken, you need the DamageModify library.
           Simply change Damage.amount to whatever you want the new damage value to be.
           Example:
           */
                library L initializer Init
                   
                    private function OnDamage takes nothing returns nothing
                        //All damage taken will be amplied by two
                        set Damage.amount = 2*Damage.amount
                    endfunction
                   
                    private function Init takes nothing returns nothing
                        call Damage.registerModifier(function OnDamage)
                    endfunction
                   
                endlibrary
            /*
            DamageModify callbacks and triggers runs first before DamageEvent callbacks
            and triggers. Example:
            */
                library L initializer Init
                   
                    private function OnDamageModifier takes nothing returns nothing
                        set Damage.amount = 0   //This will cause all damage taken to be zero
                    endfunction
                   
                    private function OnDamage takes nothing returns nothing
                        call BJDebugMsg(GetUnitName(Damage.target) " takes " + R2S(Damage.amount) + " damage")
                        //Will print:
                        //"<Target Name> takes 0 damage"
                    endfunction
                   
                    private function Init takes nothing returns nothing
                        call Damage.registerModifier(function OnDamageModifier)
                        call Damage.register(function OnDamageModifier)
                    endfunction
                   
                endlibrary
            /*
   
        5. If you want to deal damage inside onDamage callback without causing infinite loops, 
           you can do so using Damage.registerTrigger(trigger) and disabling the trigger before
           the new damage is applied then enabling it again after damage is applied. Example:
           */
                library L initializer Init
                   
                    globals
                        private trigger trg = CreateTrigger()
                    endglobals
                   
                    private function OnDamage takes nothing returns boolean
                        call BJDebugMsg(GetUnitName(Damage.target) " takes " + R2S(Damage.amount) + " damage")
                        call DisableTrigger(thistype.trg)
                        call UnitDamageTarget(Damage.source, Damage.target, 42.0, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, null)
                        call EnableTrigger(thistype.trg)
                        call BJDebugMsg(GetUnitName(Damage.target) " takes an extra 42 damage.")
                        return false
                    endfunction
                   
                    private function Init takes nothing returns nothing
                        call Damage.registerTrigger(trg)
                        call TriggerAddCondition(trg, Condition(function OnDamage))
                    endfunction
                   
                endlibrary
            /*
   
   
    //==================================================================//
                               IMPORTANT NOTES:
    //==================================================================//
       
        - Life Drain will not work with this system.
       
        - Locust Swarm abilities will still work, but the "Data - Damage Return Factor"
          defined in Object Editor must be multiplied to -1. Example, to fix the
          default Locust Swarm ability, change the value from 0.75 to -0.75
       
        - Runed Bracer items and abilities will not work with this system. But one
          can easily make a trigger for that.
       
        - Mana Shield works normally.
         
        - Artillery attacks that causes unit to explode on death works normally.
       
        - Finger of Death works normally.
       
        - Spirit Link will not work with this system. However, it is possible to
          recreate a triggered version of Spirit Link using this system.
       
        - Magic Attacks are detected as DAMAGE_TYPE_PHYSICAL while Spells Attack
          are detected as DAMAGE_TYPE_MAGICAL.
   
   
    //==================================================================//
                                   CREDITS:
    //==================================================================//
   
        looking_for_help 
            - for the Runed Bracer trick allowing this system to distinguish PHYSICAL
              and MAGICAL damage.
            - for Physical Damage Detection System which was used as a reference for 
              creating this system.
       
        Bribe
            - for the optional Table
       
        Cokemonkey11 and PurplePoot
            - for the bucket-based damage detection systems for less processes per refresh.

        Aniki, Quilnez and Wietlol
            - for finding bugs, giving feedbacks and suggestions.
           
           
   
    //==================================================================//
                                   CHANGELOG:
    //==================================================================//
        v1.00 - [3 Aug 2016]
         - Initial Release
       
        v1.10 - [7 Aug 2016]
         - Fixed unremoved and unintentional BJDebug Messages.
         - Fixed unremoved group in preplace.
         - Fixed uncleaned Table/hashtable timer handle id.
         - Fixed "Nonrecursive Damage bug".
         - Fixed HP Bar flickering bug.
         - Fixed a bug where revived units are not registered.
         - Optimized the script, now only uses 1 static timer.
         - Replaced UnitAlive by GetUnitTypeId as the condition for removal.
         - Removed optional requirements TimerUtils and TimerUtilsEx.
         - Implemented a periodic refresh mechanism.
         - Implemented the bucket technique to limit the number of units per refresh.

        v1.11 - [7 Aug 2016]
         - Fixed a bug where it does not auto-register preplaced units when using Table.
         - Fixed some functions compiling to trigger evaluation due to the order.
         - Added a filter when using AUTO_REGISTER.
       
        v1.12 - [8 August 2016]
         - Fixed a bug when all DamageBuckets are removed.
         - Fixed a bug where currentBucket points to a destroyed DamageBucket
           when it is destroyed.
         - Fixed unremoved saved boolean in Table/hashtable.
         - AutoRegisterFilter is now also applied to preplaced units.
       
        v1.20 - [17 August 2016]
         - Fixed recursion bug.
         - Added Damage.enabled to control whether DamageEvent callbacks are ON/OFF.
         - Added Damage.registerPermanent(code).
         - Renamed Damage.registerFirst(code) to Damage.registerModifier(code).
         - Damage.registerModifier(code) only comes within DamageModify.
         - SET_MAX_LIFE of DamageModify is now preloaded.
       
        v1.30 - [15 October 2016]
         - Added more detailed documentation with examples.
         - Now uses life change event instead of a timer avoiding several bugs.
         - Fixed recursion damage workaround.
         - Optimized and shortened the code.
       
        v1.40 - [29 March 2017]
         - Implemented a stack to make Damage.source, Damage.target, Damage.amount and Damage.type behave like local variables in the callback.
         - Fixed Damage.source and Damage.target changing unit value withing callback due to recursion.
         - Fixed Damage.amount and Damage.type changing value within callback due to recursion.
         - Improved documentation on how it affects default Warcraft 3 abilities.
       
        v1.41 - [24 May 2017]
         - Added Damage.lockAmount() feature.
         - Fixed bug occuring when units with very high hp takes damage.
         - Changing Damage.amount will no longer work on non-modifier codes/triggers.
       
        v1.42 - [25 May 2017]
         - Fixed bug occuring when units with very high hp takes very small damage.
         
        v1.43 - [29 May 2017]
         - Fixed bug when magic damage amount is between 0.125 to 0.2.
         
        v1.44 - [3 June 2017]
         - Fixed bug when magic damage exceeds target's max health.
       
    */
   
   
//! endnovjass


JASS:
library DamageEvent /*
            ----------------------------------
                    DamageEvent v1.44
                        by Flux
            ----------------------------------

        A lightweight damage detection system that
        detects when a unit takes damage.
        Can distinguish physical and magical damage.

    */ requires /*
      (nothing)

    */ optional Table /*
        If not found, DamageEvent will create 2 hashtables. Hashtables are limited to 255 per map.

    */

    //Basic Configuration
    //See documentation for details
    globals
        private constant integer DAMAGE_TYPE_DETECTOR = 'ADMG'
        private constant real ETHEREAL_FACTOR = 1.6666
    endglobals

    //Advanced Configuration
    //Default values are recommended, edit only if you understand how the system works (See documentation).
    globals
        private constant boolean AUTO_REGISTER = true
        private constant boolean PREPLACE_INIT = true
        private constant integer COUNT_LIMIT = 50
        private constant real REFRESH_TIMEOUT = 30.0
    endglobals

    static if not AUTO_REGISTER and not PREPLACE_INIT then
    //Equivalent to AUTO_REGISTER or PREPLACE_INIT
    else
        //Autoregister Filter
        //If it returns true, it will be registered automatically
        private function AutoRegisterFilter takes unit u returns boolean
            local integer id = GetUnitTypeId(u)
            return id != 'dumi'
        endfunction
    endif

    //Globals not meant to be edited.
    globals
        constant integer DAMAGE_TYPE_PHYSICAL = 1
        constant integer DAMAGE_TYPE_MAGICAL = 2
        private constant real MIN_LIFE = 0.406
        private DamageBucket pickedBucket = 0
        private DamageBucket currentBucket = 0
    endglobals

    struct DamageBucket

        readonly integer count
        readonly trigger trg
        readonly group grp
        readonly thistype next
        readonly thistype prev

        private static timer t = CreateTimer()

        method destroy takes nothing returns nothing
            set this.next.prev = this.prev
            set this.prev.next = this.next
            if thistype(0).next == 0 then
                call PauseTimer(thistype.t)
            endif
            if this == currentBucket then
                set currentBucket = thistype(0).next
            endif
            call DestroyTrigger(this.trg)
            call DestroyGroup(this.grp)
            set this.trg = null
            set this.grp = null
            call this.deallocate()
        endmethod

        //Returns the DamageBucket where unit u belongs.
        static method get takes unit u returns thistype
            static if LIBRARY_Table then
                return Damage.tb[GetHandleId(u)]
            else
                return LoadInteger(Damage.hash, GetHandleId(u), 0)
            endif
        endmethod

        method remove takes unit u returns nothing
            call GroupRemoveUnit(this.grp, u)
            static if LIBRARY_Table then
                call Damage.tb.remove(GetHandleId(u))
            else
                call RemoveSavedInteger(Damage.hash, GetHandleId(u), 0)
            endif
        endmethod

        //Add unit u to this DamageBucket.
        method add takes unit u returns nothing
            call TriggerRegisterUnitEvent(this.trg, u, EVENT_UNIT_DAMAGED)
            call GroupAddUnit(this.grp, u)
            set this.count = this.count + 1
            static if LIBRARY_Table then
                set Damage.tb[GetHandleId(u)] = this
            else
                call SaveInteger(Damage.hash, GetHandleId(u), 0, this)
            endif
        endmethod

        private static thistype temp

        //Enumerate DamageBucket units, removing it if it is removed from the game
        private static method cleanGroup takes nothing returns nothing
            local unit u = GetEnumUnit()
            local thistype this = temp
            if GetUnitTypeId(u) != 0 then
                call TriggerRegisterUnitEvent(this.trg, u, EVENT_UNIT_DAMAGED)
                set this.count = this.count + 1
            else
                call GroupRemoveUnit(this.grp, u)
                static if LIBRARY_Table then
                    call Damage.tb.remove(GetHandleId(u))
                else
                    call RemoveSavedInteger(Damage.hash, GetHandleId(u), 0)
                endif
            endif
            set u = null
        endmethod

        //Refreshes this DamageBucket
        method refresh takes nothing returns nothing
            local unit u
            call DestroyTrigger(this.trg)
            set this.trg = CreateTrigger()
            call TriggerAddCondition(this.trg, Filter(function Damage.core))
            set this.count = 0
            set thistype.temp = this
            call ForGroup(this.grp, function thistype.cleanGroup)
            if this.count == 0 then
                call this.destroy()
            endif
        endmethod

        static method create takes nothing returns thistype
            local thistype this = thistype.allocate()
            set this.count = 0
            set this.trg = CreateTrigger()
            set this.grp = CreateGroup()
            call TriggerAddCondition(this.trg, Filter(function Damage.core))
            set this.next = thistype(0)
            set this.prev = thistype(0).prev
            set this.next.prev = this
            set this.prev.next = this
            if this.prev == 0 then
                call TimerStart(thistype.t, REFRESH_TIMEOUT, true, function Damage.refresh)
            endif
            return this
        endmethod

    endstruct

    struct DamageTrigger

        private trigger trg
        private thistype next
        private thistype prev

        method destroy takes nothing returns nothing
            set this.next.prev = this.prev
            set this.prev.next = this.next
            static if LIBRARY_Table then
                call Damage.tb.remove(GetHandleId(this.trg))
            else
                call RemoveSavedInteger(Damage.hash, GetHandleId(this.trg), 0)
            endif
            set this.trg = null
            call this.deallocate()
        endmethod

        static method unregister takes trigger t returns nothing
            local integer id = GetHandleId(t)
            static if LIBRARY_Table then
                if Damage.tb.has(id) then
                    call thistype(Damage.tb[id]).destroy()
                endif
            else
                if HaveSavedInteger(Damage.hash, id, 0) then
                    call thistype(LoadInteger(Damage.hash, id, 0)).destroy()
                endif
            endif
        endmethod

        static method register takes trigger t returns nothing
            local thistype this = thistype.allocate()
            set this.trg = t
            set this.next = thistype(0)
            set this.prev = thistype(0).prev
            set this.next.prev = this
            set this.prev.next = this
            static if LIBRARY_Table then
                set Damage.tb[GetHandleId(t)] = this
            else
                call SaveInteger(Damage.hash, GetHandleId(t), 0, this)
            endif
        endmethod

        static method executeAll takes nothing returns nothing
            local thistype this = thistype(0).next
            loop
                exitwhen this == 0
                if IsTriggerEnabled(this.trg) then
                    if TriggerEvaluate(this.trg) then
                        call TriggerExecute(this.trg)
                    endif
                endif
                set this = this.next
            endloop
        endmethod

    endstruct


    struct Damage extends array

        private static thistype stackTop = 0
        private static thistype global
        private static integer array allocator
        private unit stackSource
        private unit stackTarget
        private real stackAmount
        private integer stackType
        private thistype stackNext

        private static real hp

        static if LIBRARY_Table then
            readonly static Table tb
        else
            readonly static hashtable hash = InitHashtable()
        endif

        //Allows the DamageModify module to access the configuration
        static if LIBRARY_DamageModify then
            private static constant real S_ETHEREAL_FACTOR = ETHEREAL_FACTOR
            private static constant real S_MIN_LIFE = MIN_LIFE
        endif

        static method remove takes unit u returns nothing
            call DamageBucket.get(u).remove(u)
        endmethod

        //Add unit u to the current DamageBucket
        static method add takes unit u returns nothing
            local DamageBucket temp
            local DamageBucket b
            //If unit does not belong to any DamageBucket yet
            if DamageBucket.get(u) == 0 then
                call UnitAddAbility(u, DAMAGE_TYPE_DETECTOR)
                call UnitMakeAbilityPermanent(u, true, DAMAGE_TYPE_DETECTOR)
                if currentBucket != 0 then
                    //When the current DamageBucket exceeds the limit
                    if currentBucket.count >= COUNT_LIMIT then
                        set temp = DamageBucket(0).next
                        loop
                            exitwhen temp == 0
                            //Find a DamageBucket with few units
                            if temp.count < COUNT_LIMIT then
                                exitwhen true
                            endif
                            set temp = temp.next
                        endloop
                        if temp == 0 then //If none is found
                            set currentBucket = DamageBucket.create()
                        else             //If a DamageBucket is found, use it
                            set currentBucket = temp
                        endif
                    endif
                else
                    set currentBucket = DamageBucket.create()
                    set pickedBucket = currentBucket
                endif
                call currentBucket.add(u)
            endif
        endmethod

        //Periodic Refresh only refreshing one DamageBucket per REFRESH_TIMEOUT
        //to avoid lag spike.
        static method refresh takes nothing returns nothing
            call pickedBucket.refresh()
            loop
                set pickedBucket = pickedBucket.next
                exitwhen pickedBucket != 0
            endloop
        endmethod

        static method operator amount takes nothing returns real
            return thistype.stackTop.stackAmount
        endmethod

        static method operator type takes nothing returns integer
            return thistype.stackTop.stackType
        endmethod

        static method operator target takes nothing returns unit
            return thistype.stackTop.stackTarget
        endmethod

        static method operator source takes nothing returns unit
            return thistype.stackTop.stackSource
        endmethod

        private static boolean prevEnable = true

        static method operator enabled takes nothing returns boolean
            return thistype.prevEnable
        endmethod

        static method operator enabled= takes boolean b returns nothing
            local DamageBucket bucket = DamageBucket(0).next
            if b != thistype.prevEnable then
                loop
                    exitwhen bucket == 0
                    if b then
                        call EnableTrigger(bucket.trg)
                    else
                        call DisableTrigger(bucket.trg)
                    endif
                    set bucket = bucket.next
                endloop
            endif
            set thistype.prevEnable = b
        endmethod

        //All registered codes will go to this trigger
        private static trigger registered

        static method register takes code c returns boolean
            call TriggerAddCondition(thistype.registered, Condition(c))
            return false    //Prevents inlining
        endmethod

        static method registerTrigger takes trigger trig returns nothing
            call DamageTrigger.register(trig)
        endmethod

        static method unregisterTrigger takes trigger trig returns nothing
            call DamageTrigger.unregister(trig)
        endmethod

        implement optional DamageModify

        static if not LIBRARY_DamageModify then

            private static method afterDamage takes nothing returns boolean
                call SetWidgetLife(thistype.stackTop.stackTarget, thistype.hp - thistype.stackTop.stackAmount)
                call DestroyTrigger(GetTriggeringTrigger())
                if thistype.global > 0 then
                    set thistype.allocator[thistype.global] = thistype.allocator[0]
                    set thistype.allocator[0] = thistype.global
                    set thistype.stackTop = thistype.stackTop.stackNext
                endif
                return false
            endmethod

            static method core takes nothing returns boolean
                local real amount = GetEventDamage()
                local thistype this
                local real newHp
                local trigger trg

                if amount == 0.0 then
                    return false
                endif

                set this = thistype.allocator[0]
                if (thistype.allocator[this] == 0) then
                    set thistype.allocator[0] = this + 1
                else
                    set thistype.allocator[0] = thistype.allocator[this]
                endif
                set this.stackSource = GetEventDamageSource()
                set this.stackTarget = GetTriggerUnit()
                set this.stackNext = thistype.stackTop
                set thistype.stackTop = this

                if amount > 0.0 then
                    set this.stackType = DAMAGE_TYPE_PHYSICAL
                    set this.stackAmount = amount
                    call DamageTrigger.executeAll()
                    set thistype.allocator[this] = thistype.allocator[0]
                    set thistype.allocator[0] = this
                    set thistype.stackTop = thistype.stackTop.stackNext

                elseif amount < 0.0 then
                    set this.stackType = DAMAGE_TYPE_MAGICAL
                    if IsUnitType(this.stackTarget, UNIT_TYPE_ETHEREAL) then
                        set amount = amount*ETHEREAL_FACTOR
                    endif
                    set this.stackAmount = -amount
                    call DamageTrigger.executeAll()

                    set thistype.hp = GetWidgetLife(this.stackTarget)
                    set newHp = thistype.hp + amount
                    if newHp < MIN_LIFE then
                        set newHp = MIN_LIFE
                    endif
                    call SetWidgetLife(this.stackTarget, newHp)

                    set trg = CreateTrigger()
                    if amount < -1.0 then
                        call TriggerRegisterUnitStateEvent(trg, this.stackTarget, UNIT_STATE_LIFE, GREATER_THAN, newHp + 1.0)
                    elseif amount < -0.125 then
                        call TriggerRegisterUnitStateEvent(trg, this.stackTarget, UNIT_STATE_LIFE, GREATER_THAN, newHp + 0.125)
                    else
                        call TriggerRegisterUnitStateEvent(trg, this.stackTarget, UNIT_STATE_LIFE, GREATER_THAN, newHp + 0.01)
                    endif
                    call TriggerAddCondition(trg, Condition(function thistype.afterDamage))
                    set trg = null
                    set thistype.global = this
                endif

                return false
            endmethod
        endif

        static if PREPLACE_INIT then
            private static method preplace takes nothing returns nothing
                local group g = CreateGroup()
                local unit u
                call GroupEnumUnitsInRect(g, bj_mapInitialPlayableArea, null)
                loop
                    set u = FirstOfGroup(g)
                    exitwhen u == null
                    call GroupRemoveUnit(g, u)
                    if AutoRegisterFilter(u) then
                        call thistype.add(u)
                    endif
                endloop
                call DestroyGroup(g)
                call DestroyTimer(GetExpiredTimer())
                set g = null
            endmethod
        endif

        static if AUTO_REGISTER then
            private static method entered takes nothing returns boolean
                local unit u = GetTriggerUnit()
                if AutoRegisterFilter(u) then
                    call thistype.add(u)
                endif
                set u = null
                return false
            endmethod
        endif

        implement DamageInit

    endstruct

    module DamageInit
        private static method onInit takes nothing returns nothing
            static if AUTO_REGISTER then
                local trigger t = CreateTrigger()
                local region reg = CreateRegion()
                call RegionAddRect(reg, bj_mapInitialPlayableArea)
                call TriggerRegisterEnterRegion(t, reg, null)
                call TriggerAddCondition(t, function thistype.entered)
            endif
            static if LIBRARY_Table then
                set thistype.tb = Table.create()
            endif
            static if PREPLACE_INIT then
                call TimerStart(CreateTimer(), 0.0000, false, function thistype.preplace)
            endif
            set thistype.registered = CreateTrigger()
            call DamageTrigger.register(thistype.registered)
            set thistype.allocator[0] = 1
            set thistype(0).stackSource = null
            set thistype(0).stackTarget = null
            set thistype(0).stackAmount = 0.0
            set thistype(0).stackType = 0
        endmethod

    endmodule

endlibrary


JASS:
library DamageModify uses DamageEvent/*
            ---------------------------------
                    DamageModify v1.44
                        by Flux
            ---------------------------------

        An add-on to DamageEvent that allows modification
        of damage taken before it is applied.
    */

    globals
        private constant integer SET_MAX_LIFE = 'ASML'
    endglobals

    struct DamageTrigger2

        private trigger trg
        private thistype next
        private thistype prev

        method destroy takes nothing returns nothing
            set this.next.prev = this.prev
            set this.prev.next = this.next
            static if LIBRARY_Table then
                call Damage.tb.remove(GetHandleId(this.trg))
            else
                call RemoveSavedInteger(Damage.hash, GetHandleId(this.trg), 0)
            endif
            set this.trg = null
            call this.deallocate()
        endmethod

        static method unregister takes trigger t returns nothing
            local integer id = GetHandleId(t)
            static if LIBRARY_Table then
                if Damage.tb.has(id) then
                    call thistype(Damage.tb[id]).destroy()
                endif
            else
                if HaveSavedInteger(Damage.hash, id, 0) then
                    call thistype(LoadInteger(Damage.hash, id, 0)).destroy()
                endif
            endif
        endmethod

        static method register takes trigger t returns nothing
            local thistype this = thistype.allocate()
            set this.trg = t
            set this.next = thistype(0)
            set this.prev = thistype(0).prev
            set this.next.prev = this
            set this.prev.next = this
            static if LIBRARY_Table then
                set Damage.tb[GetHandleId(t)] = this
            else
                call SaveInteger(Damage.hash, GetHandleId(t), 0, this)
            endif
        endmethod

        static method executeAll takes nothing returns nothing
            local thistype this = thistype(0).next
            loop
                exitwhen this == 0
                if IsTriggerEnabled(this.trg) then
                    if TriggerEvaluate(this.trg) then
                        call TriggerExecute(this.trg)
                    endif
                endif
                set this = this.next
            endloop
        endmethod

    endstruct

    module DamageModify

        private static boolean changed = false
        private static trigger registered2 = CreateTrigger()
        private static boolean locked = false
       
        static method registerModifier takes code c returns boolean
            call TriggerAddCondition(thistype.registered2, Condition(c))
            return false    //Prevents inlining
        endmethod

        static method registerModifierTrigger takes trigger trg returns nothing
            call DamageTrigger2.register(trg)
        endmethod

        static method unregisterModifierTrigger takes trigger trg returns nothing
            call DamageTrigger2.unregister(trg)
        endmethod

        static method lockAmount takes nothing returns nothing
            set thistype.locked = true
        endmethod

        private static method afterDamage takes nothing returns boolean
            if GetUnitAbilityLevel(thistype.stackTop.stackTarget, SET_MAX_LIFE) > 0 then
                call UnitRemoveAbility(thistype.stackTop.stackTarget, SET_MAX_LIFE)
            endif
            call SetWidgetLife(thistype.stackTop.stackTarget, thistype.hp - thistype.stackTop.stackAmount)
            call DestroyTrigger(GetTriggeringTrigger())
            if thistype.global > 0 then
                set thistype.allocator[thistype.global] = thistype.allocator[0]
                set thistype.allocator[0] = thistype.global
                set thistype.stackTop = thistype.stackTop.stackNext
            endif
            return false
        endmethod

        static method core takes nothing returns boolean
            local real amount = GetEventDamage()
            local boolean changed = false
            local thistype this
            local trigger trg
            local real newHp

            if amount == 0.0 then
                return false
            endif

            set this = thistype.allocator[0]
            if (thistype.allocator[this] == 0) then
                set thistype.allocator[0] = this + 1
            else
                set thistype.allocator[0] = thistype.allocator[this]
            endif
            set this.stackSource = GetEventDamageSource()
            set this.stackTarget = GetTriggerUnit()
            set this.stackNext = thistype.stackTop
            set thistype.stackTop = this

            if amount > 0.0 then
                set this.stackType = DAMAGE_TYPE_PHYSICAL
                set this.stackAmount = amount
                call DamageTrigger2.executeAll()
                set changed = thistype.changed
                if changed then
                    set thistype.changed = false
                endif
                set thistype.locked = true
                call DamageTrigger.executeAll()
                set thistype.locked = false

            elseif amount < 0.0 then
                set this.stackType = DAMAGE_TYPE_MAGICAL
                if IsUnitType(this.stackTarget, UNIT_TYPE_ETHEREAL) then
                    set amount = amount*S_ETHEREAL_FACTOR
                endif
                set this.stackAmount = -amount
                call DamageTrigger2.executeAll()
                set changed = thistype.changed
                if changed then
                    set thistype.changed = false
                endif
                set thistype.locked = true
                call DamageTrigger.executeAll()
                set thistype.locked = false
            endif

            if amount < 0.0 or (changed and amount > 0.125) then
                set thistype.hp = GetWidgetLife(this.stackTarget)
                set trg = CreateTrigger()
                if amount > 0.0 then
                    set newHp = thistype.hp + amount
                    if newHp > GetUnitState(this.stackTarget, UNIT_STATE_MAX_LIFE) then
                        call UnitAddAbility(this.stackTarget, SET_MAX_LIFE)
                    endif

                    call SetWidgetLife(this.stackTarget, newHp)
                    if amount > 1.0 then
                        call TriggerRegisterUnitStateEvent(trg, this.stackTarget, UNIT_STATE_LIFE, LESS_THAN, newHp - 1.0)
                    elseif amount > 0.125 then
                        call TriggerRegisterUnitStateEvent(trg, this.stackTarget, UNIT_STATE_LIFE, LESS_THAN, newHp - 0.125)
                    endif
                else
                    set newHp = thistype.hp + amount
                    if newHp < S_MIN_LIFE then
                        set newHp = S_MIN_LIFE
                    endif
                    call SetWidgetLife(this.stackTarget, newHp)
                    if amount < -1.0 then
                        call TriggerRegisterUnitStateEvent(trg, this.stackTarget, UNIT_STATE_LIFE, GREATER_THAN, newHp + 1.0)
                    elseif amount < -0.125 then
                        call TriggerRegisterUnitStateEvent(trg, this.stackTarget, UNIT_STATE_LIFE, GREATER_THAN, newHp + 0.125)
                    else
                        call TriggerRegisterUnitStateEvent(trg, this.stackTarget, UNIT_STATE_LIFE, GREATER_THAN, newHp + 0.01)
                    endif
                endif
                call TriggerAddCondition(trg, Condition(function thistype.afterDamage))
                set trg = null
                set thistype.global = this

            else
                set thistype.allocator[this] = thistype.allocator[0]
                set thistype.allocator[0] = this
                set thistype.stackTop = thistype.stackTop.stackNext

            endif

            return false
        endmethod

        static method operator amount= takes real r returns nothing
            if not thistype.locked then
                set thistype.stackTop.stackAmount = r
                set thistype.changed = true
            endif
        endmethod

        private static method onInit takes nothing returns nothing
            local unit u = CreateUnit(Player(14), 'hfoo', 0, 0, 0)
            call UnitAddAbility(u, SET_MAX_LIFE)
            call RemoveUnit(u)
            set thistype.registered2 = CreateTrigger()
            call DamageTrigger2.register(thistype.registered2)
            set u = null
        endmethod
    endmodule

endlibrary


JASS:
library L initializer Init
          
    private function OnDamage takes nothing returns nothing
        //This will run whenever a unit registered takes damage.
        //Do you thing here
    endfunction
          
    private function Init takes nothing returns nothing
        call Damage.register(function OnDamage)
    endfunction
        
endlibrary


Changelog
v1.00 - [3 Aug 2016]
- Initial Release

v1.10 - [7 Aug 2016]
- Fixed unremoved BJDebug Messages.
- Fixed unremoved group in preplace.
- Fixed uncleaned Table/hashtable timer handle id.
- Fixed "Nonrecursive Damage bug".
- Fixed HP Bar flickering bug.
- Fixed a bug where revived units are not registered.
- Optimized the script, now only uses 1 static timer.
- Replaced UnitAlive by GetUnitTypeId as the condition for removal.
- Removed optional requirements TimerUtils and TimerUtilsEx.
- Implemented a periodic refresh mechanism.
- Implemented the bucket technique to limit the number of units per refresh.

v1.11 - [7 Aug 2016]
- Fixed a bug where it does not auto-register preplaced units when using Table.
- Fixed some functions compiling to trigger evaluation due to the order.
- Added a filter when using AUTO_REGISTER.

v1.12 - [8 Aug 2016]
- Fixed a bug when all DamageBuckets are removed.
- Fixed a bug where currentBucket points to a destroyed DamageBucket when it is destroyed.
- Fixed unremoved saved boolean in Table/hashtable.
- AutoRegisterFilter is now also applied to preplaced units.

v1.20 - [17 August 2016]
- Fixed recursion bug.
- Added Damage.enabled to control whether DamageEvent callbacks are ON/OFF.
- Added Damage.registerPermanent(code).
- Renamed Damage.registerFirst(code) to Damage.registerModifier(code).
- Damage.registerModifier(code) only comes within DamageModify.
- SET_MAX_LIFE of DamageModify is now preloaded.

v1.30 - [15 October 2016]
- Added more detailed documentation with examples.
- Now uses life change event instead of a timer avoiding several bugs.
- Fixed recursion damage workaround.
- Optimized and shortened the code.

v1.40 - [29 March 2017]
- Implemented a stack to make Damage.source, Damage.target, Damage.amount and Damage.type behave like local variables in the callback.
- Fixed Damage.source and Damage.target changing unit value withing callback due to recursion.
- Fixed Damage.amount and Damage.type changing value within callback due to recursion.
- Improved documentation on how it affects default Warcraft 3 abilities.

v1.41 - [24 May 2017]
- Added Damage.lockAmount() feature.
- Fixed bug occuring when units with very high hp takes damage.
- Changing Damage.amount will no longer work on non-modifier codes/triggers.

v1.42 - [25 May 2017]
- Fixed bug occuring when units with very high hp takes very small damage.

v1.43 - [29 May 2017]
- Fixed bug when magic damage amount is between 0.125 to 0.2.

v1.44 - [3 June 2017]
- Fixed bug when magic damage exceeds target's max health.

 

Attachments

  • DamagePackage v1.44.w3x
    47.1 KB · Views: 279
Last edited:
Level 22
Joined
Feb 6, 2014
Messages
2,466
Before anyone asks, What lead me to create DamagePackage?

While modding, I realized I need a vJASS DDS that can distinguish Physical and Magical Damage. Physical Damage Detection System (PDDS) by looking_for_help is the only candidate so I initially used it. However, there are things I don't like about PDDS:
(No offense to lfh)

- Not modular enough
When a user doesn't need the damage modification feature, PDDS still uses extra computation for it.

- Has room for microoptimization
Can be improved such as only using 1 hashtable read and write with a struct instead of putting source, target, amount, etc to each of their own hashtable space.

- Looks outdated
Doesn't use UnitAlive and uses a trigger with variable changes value event to trigger registered function calls.
Also, lfh wasn't as good as he is now when he created PDDS (PDDS.source/target/damageType is not readonly).
Also, DamagePackage utilizes other systems such as TimerUtils and Table when found/present.

- Looks bloated
There are things like function UnitHasItemOfType and other wrapper functions which seems unnecessary to me.
When I look at function DamageEngine, it seems like a huge overhead.
Mine (when Damage is not modified) will only call 5 native functions everytime a unit is physically damaged compared to PDDS which calls >15 natives and a few custom functions.

- I hate the idea of trigger refresh periodically.
DamagePackage uses a different approach, it will only refresh once a certain number of units is registered.


- Doesn't allow full control who gets registered.
Imagine a map where I have full control of units appearing in the map via CreateUnit. Then instead of using a trigger with event "Unit enters Region",
I can just add (or hook) Damage.add(unit) after CreateUnit saving more performance.

- Slightly a pain to import.
Requires to copy two abilities. DamagePackage will use ObjectMerger in the future.
Latest versions already has an ObjectMerger.



By the way, I'm expecting this still has a lot of bugs, but for now my tests went flawlessly.

Things I will consider in the future: (Tell me if I miss something)
- Exploding unit
- Locust Swarm returning to caster
- At first I saw flickering hp, but now I can't even though I didn't change anything in the code (Need help on this).
- Coke's bucket technique.
- Priority Queue? (not sure)
 
Last edited:
Level 13
Joined
Nov 7, 2014
Messages
571
It seems that a Damage.is_killing_blow flag could be useful, rather than registering *_DEATH event.

It can be computed from within the callback but it's tedious
JASS:
if GetWidgetLife(Damage.target) - Damage.amount < 0.405 then
    // Damage.target died, although UnitAlive reports that it's alive, it will be dead ("next tick"?)
endif

The "Nonrecursive Damage" example doesn't work, the damage dealt is always 20 (regardless of the attacker), and the "flickering hp" occured.

PS: the two abilities in ObjectMerger syntax:

JASS:
// ADMG:
//
//! external ObjectMerger w3a AIsr ADMG isr2 1 2 aart "" anam "Damage.DAMAGE_TYPE_DETECTOR"
// or
//! externalblock extension=lua ObjectMerger $FILENAME$
    //! i setobjecttype("abilities")

    //! i createobject("AIsr", "ADMG")
        //! i makechange(current, "isr2", "1" , "2")
        //! i makechange(current, "aart", "")
        //! i makechange(current, "anam", "Damage.DAMAGE_TYPE_DETECTOR")
//! endexternalblock

// ASML:
//
//! external ObjectMerger w3a AIl1 ASML Ilif 1 1000000 ansf "" anam "Damage.SET_MAX_LIFE"
// or
//! externalblock extension=lua ObjectMerger $FILENAME$
    //! i setobjecttype("abilities")

    //! i createobject("AIl1", "ASML")
        //! i makechange(current, "Ilif", "1" , "1000000")
        //! i makechange(current, "ansf", "")
        //! i makechange(current, "anam", "Damage.SET_MAX_LIFE")
//! endexternalblock
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
It seems that a Damage.is_killing_blow flag could be useful, rather than registering *_DEATH event.

It can be computed from within the callback but it's tedious
JASS:
if GetWidgetLife(Damage.target) - Damage.amount < 0.405 then
    // Damage.target died, although UnitAlive reports that it's alive, it will be dead ("next tick"?)
endif
Why Damage.is_killing_blow (hate that underscore) over registering DEATH event? What's the advantage/purpose?

The "Nonrecursive Damage" example doesn't work, the damage dealt is always 20 (regardless of the attacker), and the "flickering hp" occured.
Good find, added you to the credit lists. I was able to track down the cause and found a solution. It turns out that the constant 10 damage is applied twice (hence 20 damage). It is fixed now in the version on my hard drive.
The flickering hp is also fixed at least from what I can see. I'll upload the next version as soon as I fully tested and checked it unlike the Initial Release.

PS: the two abilities in ObjectMerger syntax:

JASS:
// ADMG:
//
//! external ObjectMerger w3a AIsr ADMG isr2 1 2 aart "" anam "Damage.DAMAGE_TYPE_DETECTOR"
// or
//! externalblock extension=lua ObjectMerger $FILENAME$
    //! i setobjecttype("abilities")

    //! i createobject("AIsr", "ADMG")
        //! i makechange(current, "isr2", "1" , "2")
        //! i makechange(current, "aart", "")
        //! i makechange(current, "anam", "Damage.DAMAGE_TYPE_DETECTOR")
//! endexternalblock

// ASML:
//
//! external ObjectMerger w3a AIl1 ASML Ilif 1 1000000 ansf "" anam "Damage.SET_MAX_LIFE"
// or
//! externalblock extension=lua ObjectMerger $FILENAME$
    //! i setobjecttype("abilities")

    //! i createobject("AIl1", "ASML")
        //! i makechange(current, "Ilif", "1" , "1000000")
        //! i makechange(current, "ansf", "")
        //! i makechange(current, "anam", "Damage.SET_MAX_LIFE")
//! endexternalblock
This is a huge help, thanks! Can't give you +rep, it says "5 different users must be given reputation before hitting the same person again."
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
"Why Damage.is_killing_blow (hate that underscore) over registering DEATH event? What's the advantage/purpose?"
onDeath has a few disadvantages.
In my Damage Engine, I have a boolean "isFatalDamage" and an event "onFatalDamage".
The difference is for example with buffs, one can not read buffs from an already dead unit (and the unit is dead on the onDeath event).
Or if you want to prevent death, you also have to be before the damage has been taken.

Also... you dont need to create a new timer every time a unit takes damage, the duration is supposed to be minimal (0), so one timer fixing all the units should do... next to which, you dont need timers at all... you just need a trigger with a life change event.

Also... I might have read it wrong, but if I am not mistaken, your system doesnt properly handle nesting damages.
Have a trigger running on the "thistype.registered" event and have the actions of that trigger deal damage to a unit.

Also... I think the event functionality should be in a separate system.
Just how I feel that it makes sense.

Also... ... why do I even care?
Im out.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
In my Damage Engine, I have a boolean "isFatalDamage" and an event "onFatalDamage".
The difference is for example with buffs, one can not read buffs from an already dead unit (and the unit is dead on the onDeath event).
Or if you want to prevent death, you also have to be before the damage has been taken.
If that's the case, users can just check the difference between current life and amount of damage and if it is less than 0.405, the damage is fatal.

Also... you dont need to create a new timer every time a unit takes damage, the duration is supposed to be minimal (0), so one timer fixing all the units should do... next to which, you dont need timers at all... you just need a trigger with a life change event.
Using a single static timer would result to a significant improve of performance, however it will bugged when multiple units are damaged at the same time (e.g. Flamestrike from Bloodmage). I'll try to research the life change event implementation if its better.

Also... I might have read it wrong, but if I am not mistaken, your system doesnt properly handle nesting damages.
Have a trigger running on the "thistype.registered" event and have the actions of that trigger deal damage to a unit.
Yes it was bugged, but I fixed it now in the version in my hard drive (which is not published yet).

Also... I think the event functionality should be in a separate system.
Just how I feel that it makes sense.
What event functionality?

Also... ... why do I even care?
Im out.
Because you're an active member?? Because I will give anyone +rep for feedback??
EDIT: Damn it, same reason "5 different users must be given reputation before hitting the same person again."
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Using a single static timer would result to a significant improve of performance, however it will bugged when multiple units are damaged at the same time (e.g. Flamestrike from Bloodmage).
Not true.
What you have to do is start the timer and add the damaged unit to a stack of units.
When the timer expires, the stack will be iterated over and the units will be reset to normal life.
(Just dont forget to empty the stack ;))

What event functionality?
The event register and stuff.
JASS:
struct Damage
 
    readonly static Event onDamage
 
endstruct

struct Event
 
    public method run takes nothing returns nothing
    public method register takes code func returns nothing
    public method unregister takes code func returns nothing
 
endstruct

Nes and Bribe already have the life change event.
It is pretty neat because you can do:

call UnitDamageTarget(u, u, blablabla)
call BJDebugMsg("HP: " + R2S(GetWidgetLife(u)))

And it will show the correct value... not a +500k value.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
Not true.
What you have to do is start the timer and add the damaged unit to a stack of units.
When the timer expires, the stack will be iterated over and the units will be reset to normal life.
(Just dont forget to empty the stack ;))
Yeah, but you weren't specific at first so I had to "guess" what you mean.
Anyways, I'll ditch the timer stuff and use Life Event instead.

The event register and stuff.
JASS:
struct Damage

    readonly static Event onDamage

endstruct

struct Event

    public method run takes nothing returns nothing
    public method register takes code func returns nothing
    public method unregister takes code func returns nothing

endstruct
I don't see any advantage of doing so.

Nes and Bribe already have the life change event.
It is pretty neat because you can do:

call UnitDamageTarget(u, u, blablabla)
call BJDebugMsg("HP: " + R2S(GetWidgetLife(u)))

And it will show the correct value... not a +500k value.
Got it.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
"I don't see any advantage of doing so."
There is no real advantage... except you separate different things.
As mentioned before, it is no big deal, but it would be nice.
Especially when you have different events (beforeDamage, afterDamage, etc).
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
Updated to v1.10.
I ditched the Life Event because of the bug and because that approach means I have to create a trigger every onDamage. Using GetWidgetLife returns the hp before damage is applied and I was not able to make GetWidgetLife return a value of 500k+ in my tests which is good.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
None of my tests failed, but if it is really a problem, you could potentially make 2 events, one with the smallest amount possible, and one with half the damage amount.

If the damage would be 0.01 (which is hardly anywhere near possible death, or reasonable damage), then it would fail.
(Dunno what the exact minimum was though gotta check that out.)
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
In theory, it doesnt matter, a unit has to deal damage to the target unit, and the damage is quite a lot, just to kill the unit.
However, if you for some reason change the damage type bonus from normal to 0 against whatever armor the target is wearing, it wont work.

Therefor, it is safer to use ConvertAttackType(7) which is not present in the gameplay constants.
All attack types above that go to null which goes to the standard... I think ATTACK_TYPE_NORMAL.
(On a side note, ConvertAttackType(7) deals increased damage to all armor types I used in my tests, I didnt really check the values, but it was a serious factor.)

Also... (before some people start to complain about it).
Siege weapons (aka units with Artillery as weapon type), explode units when they die.
Triggered damage doesnt do that.
If the target should die, and the original damage is positive, you should set the current life to the minimum amount and let the original damage kill the target.
Then Artillery damage will explode the unit.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
If I may ask, why ATTACK_TYPE_NORMAL and DAMAGE_TYPE_UNIVERSAL?
Using that combination damages the unit whether it is ethereal or magic immune.

Therefor, it is safer to use
ConvertAttackType(7)
which is not present in the gameplay constants.
Unfortunately, ConvertAttackType(7) cannot damage ethereal units that's why I'm not using it.

Also... (before some people start to complain about it).
Siege weapons (aka units with Artillery as weapon type), explode units when they die.
My system still explodes units when attacked by a Meat Wagon unless the Damage was modified. I'm not sure if there is any solution to that. My DamageModify algorithm cancels the amount of damage taken then apply the Damage.amount in the after damage.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Wut?
But then the Damage.amount is delayed... that is horrible.
When using life events, it wont matter that much, but still.

Look at Bribe's DDS, if the damage is positive, then the health is set to the minimum amount so the original damage will end up doing the final blow.
Ofcourse, if your system supports changing the source, you also have to check if the source is the same.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
Wut?
But then the Damage.amount is delayed... that is horrible.
Delayed by 0.0000 second. And why is that horrible? PDDS by lfh uses the same algorithm yet it is still approved in the JASS Section.
The problem with life events is the bug mentioned by Nestharus and the creation of a new trigger every onDamage.

Look at Bribe's DDS, if the damage is positive, then the health is set to the minimum amount so the original damage will end up doing the final blow.
But what if the damage is magical (negative)?
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
If you don't use unit life change event and use timer instead, you might need to have GetWidgetLifeEx like what lfh did. (I believe you understand why)

Which is not very neat imo. If I have to deal with that bug, I prefer to check whether or not, the damage amount is lower/equal to the smallest number life change event can detect. If it's lower/equal, fire the custom damage event directly. Else, create new trigger with life change event.

But you need to keep the order of the fired events. As if not, in some cases, that mini damage event might be fired before the normal damage event. Okay, I don't have good wordings for it so let me give you an example:
1. You deal 100 damage to a unit > native damage event fires > damage is bigger than the minimum number > create new trigger with life change event > custom damage event is delayed waiting for the life change event
2. You deal 0.0000001 damage to a unit (assuming life change event can't detect it) > the damage amount is lower than minimum number the event can detect > fire the custom damage event directly
That way, the second damage event will fire before the first one. So, you have to make a queue for it.

So it's something like this: when the first 100 damage is detected > queue the custom event > create the life change trigger. Next, the low damage is detected > queue the event instead of firing it directly. Then when the first life change event is detected, fire that event (pop it from queue and move to next node/event), and so on...
To queue the event, you can store the event data instead in the node. Like damage source, target, amount, type, etc.

The point is, I will prefer life change event than timer no matter what work around I will have to get through. Because, even 0 interval timer is too slow compared to the life change event. And using timer will require you to have that extra function. I'm typing this in mobile sorry if there's typo. Maybe I also missed something, I need to think about it more broadly later at home...

Edit:
First problem I found about the queue method is deciding when to stop popping the queue. If you keep popping nodes from it some events can be fired even when life change event hasn't been detected yet. Well, that's a nasty one. Proving that the queue thing might not be a solution.

Maybe Nestharus has done more experiments and has more alternative solutions than me ^^

Edit 2:
The solution I can think of is by creating some sort of "event sequence". Each sequence has it's own queue. New sequence is created whenever a life change trigger is created. Basically, the queue is only used for keeping the event order. So in 99% of all cases all of these queues will only have one node which is quite inefficient. Because, let's get back to reality, no one will ever deal a damage just right after dealing another damage. That's plain dumb. I mean, in what real case somebody want to do that? And in what real case somebody want to deal that small amount of damage? What for?
I will say, it's a bug yes but is unnecessary to be fixed. Especially if it costs some neatness.
 
Last edited:
Level 24
Joined
Aug 1, 2013
Messages
4,657
Because if you use a timer, then it doesnt work fine.
Because of the timer, the cheat death ability remove is delayed, so immediately after the onDamage, the unit has massive health.
The GetWidgetLifeEx() checks if the target has the cheat death ability and removes it.

For GUI support, this is horrible, as "Life/Max Life of <unit>" also doesnt work.
With life change events, it does work.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
Because if you use a timer, then it doesnt work fine.
Because of the timer, the cheat death ability remove is delayed, so immediately after the onDamage, the unit has massive health.

Not this one. The onDamage callbacks are executed first before the cheat death ability is added. So if a user uses GetWidgetLife inside an onDamage callback, it will return the correct hp. The only situation (as far as I know) that it will return false HP is if you do modify the damage amount then uses GetWidgetLife inside a timer callback of a 0.000 second timer. But I can't think of a situation why anyone would do that. Example:

JASS:
private static method expire takes nothing returns nothing
    local real hp = GetWidgetLife(globalUnit) //Returns wrong hp
endmethod

private static method onDamage takes nothing returns nothing
    set globalUnit = Damage.target
    set Damage.amount = 0
    call TimerStart(CreateTimer(), 0.0, false, function thistype.expire)
endmethod
 

Kazeon

Hosted Project: EC
Level 33
Joined
Oct 12, 2011
Messages
3,449
JASS:
private static method onDamage takes nothing returns nothing
    set globalUnit = Damage.target
    set Damage.amount = 0
    call TimerStart(CreateTimer(), 0.0, false, function thistype.expire)
endmethod
It's still bugged. Low HP unit will be dead before the timer expires if you do nothing about its HP. And if you do anything about the HP, GetWidgetLife will be bugged.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
It is the entire reason why GetWidgetLifeEx() exists.
So I'll take the bet as a yes?? What's your bet?

EDIT: Sorry didn't saw
JASS:
private static method onDamage takes nothing returns nothing
    set globalUnit = Damage.target
    set Damage.amount = 0
    call TimerStart(CreateTimer(), 0.0, false, function thistype.expire)
endmethod
It's still bugged. Low HP unit will be dead before the timer expires if you do nothing about its HP. And if you do anything about the HP, GetWidgetLife will be bugged.
Nope, I tested it blocking 500 damage with hp = 1. Damage still blocked.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
I bet it bugs out, but I also bet that your first attempt in setting up the situation will not bug that script which will make you think that it is not bugged...
Your 2nd bet is exactly correct because the only time it will bug is when there is a trigger that will modify the damage and if the damage is physical. Otherwise, it will not bug.
The chances of that happening is not high (that's why I missed it), I mean where will you ever need to measure the hp after dealing physical damage to a unit that modifies the damage. But since this is a public resource, it is not acceptable.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
I dont really understand why the damage has to be physical...

But in any case, if you have a spell that deals damage to a unit and after that, heals the caster for x% of the targets current health, then it bugs out.
Ofcourse assuming that the damage changes, which is not that unlikely as you have implemented a DDS in the first place.
In my spells, I have numberless effects that run on damage from both spells and basic attacks.
Luckily, in my case, there arent many things that could go wrong, but if it has minor problems like "under these circumstances, with this setup, with these units and this maximum range between the heroes, this specific spell will bug out if it is cast on this unit" which is unlikely to happen, then it still has to be fixed.

"You really should make sure first before facepalming yourself."
Ow yea, the facepalm was about "after" the onDamage.
So right after that function that runs on "<unit> takes damage", that unit is bugged until the timer runs out.
Seeing you trying to do something during the onDamage, it really made me cry.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
Luckily, in my case, there arent many things that could go wrong, but if it has minor problems like "under these circumstances, with this setup, with these units and this maximum range between the heroes, this specific spell will bug out if it is cast on this unit" which is unlikely to happen, then it still has to be fixed.
Yeah it still needs to be fixed because this is a public resource. Fortunately, as I've stated in the first or second post, I just needed a good DDS for the map I'm developing. Since it is good enough for my map (didn't encounter any bugs) and I don't have time to implement the life change event, I think I'll just ditch this one to Graveyard.
 
Hey guys. I have not followed all discussions in the DDS threads so I'm not really good informed at the moment.
It's quite a lot of work, so I would wanna know if this is still the current opinion:
I think I'll just ditch this one to Graveyard.
so it is because of this bug, but which is covered by other dds systems?
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
Hey guys. I have not followed all discussions in the DDS threads so I'm not really good informed at the moment.
It's quite a lot of work, so I would wanna know if this is still the current opinion:

so it is because of this bug, but which is covered by other dds systems?
Yes, basically the problem with this is the damage is delay by a 0.000 second asynchronous timer, hence using GetWidgetLife after applying damage could return the wrong value (in my case, if the damage is physical and damage is modified).
Other DDS such as PDDS by lfh force users to use a GetWidgetLife wrapper to fix this.
 
Level 22
Joined
Feb 6, 2014
Messages
2,466
Well, you can use the unit life changed event to avoid the GetWidgetLife issue, but that has its own problems.
Exactly, plus I don't like the extra trigger creation everytime a unit is damaged.
I personally don't have problems with 0 second delay damage because there's workarounds in preventing them. Also, it is rare to encounter it, like I said, the bug mentioned will only occur on physically modified damage.
 
Top