[vJASS] [System] Physical Damage Detection

Level 14
Joined
Dec 12, 2012
Messages
1,007
Meaby it's because bj_mapInitialPlayableArea is by default set to null. This and some other "rare cases" occured in my GetClosestWidget too, which I happily fixed :)

I thought so too at the beginning, but the rect had initially the correct dimensions. But after ~10 seconds it just returned zero coordinates. I also checked the mapscript, but he never set the bj_mapInitialPlayableArea variable to any value and never called one of the functions that could modify it (like InitBlizzard, for example). I don't really know what caused this behavior, as it only happend in this one map until now.

However, it is much safer to rely on GetWorldBounds anyway, because bj_mapInitialPlayableArea isn't constant and setting it to some value would break the system anyway. With GetWorldBounds we are on the safe side.
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
Found an actual bug with this system.

Some abilities cause 0-damage events upon being added(tornado slow aura) or casted(holy light).
This would be fine if it weren't for the fact that this system doesn't keep internal and external parts separate.

This part of the code causes the problem:
JASS:
        // Set damage variables  
        set source = GetEventDamageSource()
        set target = GetTriggerUnit()
        set rawAmount = GetEventDamage()
       
        // Determine the damage type
        if damageType == CODE and rawAmount != 0.0 then
            set damageType = CODE
        elseif rawAmount > 0.0 then
            set damageType = PHYSICAL
        elseif rawAmount < 0.0 then
            set damageType = SPELL
        else
            return
        endif

The issue comes from the fact that variables get changed before filtering for pointless events. This means that they can get changed during a damage handler, which is absolutely unacceptable.

Luckily the fix is simple - Use local variables before filtering out garbage events. Then when the filtering is done and before the events are launched you set globals to the appropriate values.

Fixing this part of your code fixed my issue. Thus, I know for sure it was the culprit.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
Update to Version 1.2.0.0
- Fixed a bug that sometimes wront target and source units were asigned
- Made all variables private that should be (damageEventTrigger variable now uses SCOPE_PRIVATE for that).
- System variables like target and source are now summarized in one struct and can be accessed by typing PDDS.target or PDDS.amount to avoid naming conflicts.
 
Level 23
Joined
Apr 16, 2012
Messages
4,041
he has, but prioritizing never works in large scale properly.

Imagine I create handler that I want ot run the very first or the very last, therefore I use -2147483648 or 2147483647. But then, in another initialization trigger I want another handler to be very first or very last, so I use -2147483648 or 2147483647 again, but it will still conflict. Yes extreme case, but may happen.

Afaik this doesnt support it nativelly
 
Not about priorities anymore, but rather phases. Phases are pretty easy to do when you are using the new Trigger library.

You define phases of damage and you define what order you want these phases to run in. Much better than using priorities ;).

I was telling people that other systems should use phases too, but everyone said no : (. I put phases in pretty much all my new event stuff.

This DDS should probably support phases.

Sorry for long delay on input o-o, don't pay much attention anymore.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
LFH, thanks for making this system. I'm gonna use it in my map. It is great that you are willing to keep this system simple, readable and independent (does not require other resources). :)

Hi, thanks and sorry for the late reply:

I got a question: if I do not use a triggered version of Finger of Death(FoD) when using this DDS, the only problem with the default FoD is that the unit killed by it does not explode?

Yes, thats right. If you don't care about FoD exploding units, you don't have to care about that and can just use it normal.

Sorry for long delay on input o-o, don't pay much attention anymore.

Hm, not sure to which input you are refering?

About the phases, I won't add this to the system for the same reason why I didn't add priorities. IMO their benefit is neglectable and nobody ever asked me about them.
 
For some reason, I cant increase the target's HP;
JASS:
private function ArcLands takes nothing returns nothing
    local integer id
    if heal.has(GetHandleId(PDDS.target)) then
        set id = GetHandleId(PDDS.target)
        call BJDebugMsg(R2S(heal.real[id])) //THIS SHOWS CORRECTLY
        call SetUnitLife(PDDS.target, GetUnitLife(PDDS.target)+heal.real[id]) //DOESNT WORK
    endif    
endfunction

On the other hand if I edit the customized globals to;

private constant integer DAMAGE_TYPE_DETECTOR = 0
private constant integer SET_MAX_LIFE = 0

IT WORKED PERFECT!


Is this a bug or just normal?
 
Level 10
Joined
Sep 19, 2011
Messages
527
update to handle life correctly (all good as far as i can see):

JASS:
library DamageEvent/* v 1.2.0.0
**********************************************************************************
*
*   Physical Damage Detection Engine
*   --------------------------------
*   By looking_for_help aka eey
*
*   This system is able to detect, modify and deal damage. It can differentiate 
*   between physical and spell damage, as well as block, reduce or increase the
*   applied damage to a desired value. You can also allocate damage events from
*   running damage events.
*
**********************************************************************************
*
*   Implementation
*   --------------
*   1.  Copy this trigger to your map. With the AddDamageHandler function you can 
*       add as many handlers as you like (compare the OnDamage scope).
*   2.  Copy the two custom abilities to your map and make sure they have the
*       correct ID in the globals variable block.
*   3.  Go to the locust swarm ability and invert its damage return portion
*       from (default) 0.75 to -0.75.
*   4.  Remove the spell damage reduction ability from the spell damage reduction
*       items you use (runed bracers). You can configure the resistance of this
*       item in the globals block, modifying BRACERS_SPELL_DAMAGE_REDUCTION.
*
**********************************************************************************
*
*   Important Notes
*   ---------------
*   1.  Life Drain does not work with this system, so you should use a triggered 
*       version of this spell if you want to use it.
*   2.  Same for Finger of Death, if you want to use this spell bug free with this
*       system, you should use a triggered version of it.
*   3.  If you use damage modifiers by setting the damage amount variable, you have
*       to use GetUnitLife, GetUnitMaxLife and SetUnitLife instead of GetWidgetLife,
*       GetUnitState for UNIT_STATE_MAX_LIFE and SetWidgetLife in your whole map to
*       ensure there occure no bugs.
*   4.  The boolean USE_SPELL_RESISTANCE_AUTO_DETECT is only neccessary set to true
*       if you want to use a customized damage table with spell damage resistance
*       above 100%. If this is not the case, it should be set to false, as the 
*       system is faster then and still works correct.
*   5.  As already mentioned you can't use the spell reduction ability when using this
*       system (runed bracers and elunes grace). If you want to use them, you can 
*       trigger them by using the damage modifiers. Runed bracers is already considered
*       in this system, so you don't have to code it.
*
**********************************************************************************
*   
*   System API
*   ----------
*   unit PDDS.target
*       - In this global unit variable, the damaged unit is saved. Don't write any-
*         thing into this variable, use it as readonly.
*
*   unit PDDS.source
*       - In this global unit variable, the damage source is saved. Don't write any-
*         thing into this variable, use it as readonly.
*
*   real PDDS.amount
*       - In this global real variable, the amount of damage is saved. This amount
*         can be modified by simply setting it to the desired amount that should be
*         applied to the target. Set the amount to 0.0 to block the damage completly.
*         Negative values will result in heal.
*
*   integer PDDS.damageType
*       - In this global integer variable, the damage type of the current damage is
*         saved. Use it to differentiate between physical, spell and code damage. The
*         variable can takes the values PHYSICAL == 0, SPELL == 1 and CODE == 2. Don't 
*         write anything into this variable, use it as readonly. 
*
*   function UnitDamageTargetEx takes unit localSource, unit localTarget, real localAmount, 
*                                     boolean attack, boolean ranged, attacktype localAttackType, 
*                                     damagetype localDamageType, weapontype localWeaponType 
*                                     returns boolean
*       - Use this function to allocate attacks from a running damage event. You can use
*         it in exactly the same way as the standard native UnitDamageTarget. The function
*         is designed recursive, so you can allocate as many attacks as you want. Do not 
*         use this function outside of a damage event.
*
*   function GetUnitLife takes unit u returns real
*       - Use this function instead of the GetWidgetLife native. It ensures that you
*         get the correct health value, even when damage modifiers are applied. If
*         you don't use damage modifiers at all, you don't need this function.
*
*   function GetUnitMaxLife takes unit u returns real
*       - Use this function instead of the GetUnitState(u, UNIT_STATE_MAX_LIFE) native.
*         It will return the correct value, even when damage modifiers are applied. If
*         you don't use damage modifiers at all, you don't need this function.
*
*   function SetUnitLife takes unit u, real newLife returns nothing
*       - Use this function instead of the SetWidgetLife(u, newLife) native if you use
*         damage modifiers in your map. Same rules as for the GetUnitMaxLife and the
*         GetUnitMaxLife functions.
*
*   function AddDamageHandler takes code damageHandler returns nothing
*       - Allows you to add a damage handler function to the system. Compare the
*         OnDamage scope for an example usage.
*
*   function RemoveDamageHandler takes code damageHandler returns nothing
*       - Allows you to remove a damage handler function from the system dynamic-
*         ally. If you added the same handler function multiple times to the sys-
*         tem, this function will remove all of these equal functions.
*
*********************************************************************************/
    
    globals
        /*************************************************************************
        *   Customizable globals
        *************************************************************************/
        
        private constant integer DAMAGE_TYPE_DETECTOR = 'A000'
        private constant integer SET_MAX_LIFE = 'A001'
        private constant integer SPELL_DAMAGE_REDUCTION_ITEM = 'brac'
        
        private constant boolean USE_SPELL_RESISTANCE_AUTO_DETECT = false
        private constant real ETHEREAL_DAMAGE_FACTOR = 1.66
        private constant real BRACERS_SPELL_DAMAGE_REDUCTION = 0.33
        private constant real TRIGGER_CLEANUP_PERIOD = 60.0
        
        /*************************************************************************
        *   End of Customizable globals
        *************************************************************************/
        
        constant integer PHYSICAL = 0
        constant integer SPELL = 1
        constant integer CODE = 2
        constant real UNIT_MIN_LIFE = 0.406
        
        private constant attacktype ATTACK_TYPE_UNIVERSAL = ConvertAttackType(7)
        private hashtable h
        private trigger damageEvent
        private trigger damageHandler
        private trigger runAllocatedAttacks
        private integer allocatedAttacks 
        private integer totalAllocs
        private integer allocCounter
        private real damageEventTrigger        
    endglobals
    
    struct PDDS extends array
        static unit source
        static unit target
        static real amount
        static integer damageType
    endstruct
    
    /******************************************************************************
    *   User functions
    ******************************************************************************/
    
    function AddDamageHandler takes code func returns nothing
        local integer id = GetHandleId(Condition(func))
        local integer index = 0
        
        // Loop to manage equal damage handlers
        loop
            exitwhen ( LoadTriggerConditionHandle(h, id, index) == null )
            set index = index + 1
        endloop
        
        // Store the desired damage handler function
        call SaveTriggerConditionHandle(h, id, index, TriggerAddCondition(damageHandler, Filter(func)))
    endfunction
    
    function RemoveDamageHandler takes code func returns nothing
        local integer id = GetHandleId(Condition(func))
        local integer index = 0
        
        // Loop through all equal damage handlers
        loop
            exitwhen ( LoadTriggerConditionHandle(h, id, index) == null )
            call TriggerRemoveCondition(damageHandler, LoadTriggerConditionHandle(h, id, index))
            set index = index + 1
        endloop
        
        // Clean things up
        call FlushChildHashtable(h, id)
    endfunction
    
    function UnitDamageTargetEx takes unit localSource, unit localTarget, real localAmount, boolean attack, /*
                                    */boolean ranged, attacktype localAttackType, damagetype localDamageType, /*
                                    */weapontype localWeaponType returns boolean            
        // Avoid infinite loop due to recursion
        if PDDS.damageType == CODE then
            return false
        endif
        
        // Avoid allocating attacks on units that are about to be killed
        if ( localTarget == PDDS.target and GetWidgetLife(localTarget) - PDDS.amount < UNIT_MIN_LIFE ) then
            return false
        endif
        
        // Store all damage parameters determined by the user
        set allocatedAttacks = allocatedAttacks + 1
        call SaveUnitHandle(h, allocatedAttacks, 0, localSource)
        call SaveUnitHandle(h, allocatedAttacks, 1, localTarget)
        call SaveReal(h, allocatedAttacks, 2, localAmount)
        call SaveBoolean(h, allocatedAttacks, 3, attack)
        call SaveBoolean(h, allocatedAttacks, 4, ranged)
        call SaveInteger(h, allocatedAttacks, 5, GetHandleId(localAttackType))
        call SaveInteger(h, allocatedAttacks, 6, GetHandleId(localDamageType))
        call SaveInteger(h, allocatedAttacks, 7, GetHandleId(localWeaponType))

        // Return true if the damage was allocated
        return true
    endfunction
    
    
    /******************************************************************************
    *   Sub functions
    ******************************************************************************/

    private function GetUnitSpellResistance takes unit u returns real
        local real originalHP
        local real beforeHP
        local real afterHP
        local real resistance
        local real DUMMY_DAMAGE = 100
        local real DUMMY_FACTOR = 0.01
        
        // Deal spell damage in order to get the units resistance
        call UnitRemoveAbility(u, DAMAGE_TYPE_DETECTOR)
        set originalHP = GetWidgetLife(u)
        call UnitAddAbility(u, SET_MAX_LIFE)
        call SetWidgetLife(u, 20000.0)
        set beforeHP = GetWidgetLife(u)
        call DisableTrigger(damageEvent)
        call UnitDamageTarget(PDDS.source, u, DUMMY_DAMAGE, true, false, null, DAMAGE_TYPE_UNIVERSAL, null)
        call EnableTrigger(damageEvent)
        set afterHP = GetWidgetLife(u)
        call UnitRemoveAbility(u, SET_MAX_LIFE)
        call SetWidgetLife(u, originalHP)
        call UnitAddAbility(u, DAMAGE_TYPE_DETECTOR)
        call UnitMakeAbilityPermanent(u, true, DAMAGE_TYPE_DETECTOR)

        // Calculate this resistance
        set resistance = DUMMY_FACTOR*(beforeHP - afterHP)

        // If the resistance was greater than 100%, return it
        if resistance > 1.0 then
            return resistance
        else
            return 1.0
        endif
    endfunction

    private function UnitHasItemOfType takes unit u, integer itemId returns integer
        local integer index = 0
        local item indexItem

        // Check if the target has a spell damage reducing item
        loop
            set indexItem = UnitItemInSlot(u, index)
            if ( indexItem != null ) and ( GetItemTypeId(indexItem) == itemId ) then
                return index + 1
            endif

            set index = index + 1
            exitwhen index >= bj_MAX_INVENTORY
        endloop
        return 0
    endfunction
    

    /******************************************************************************
    *   Damage Engine
    ******************************************************************************/
    
    /******************************************************************************
    *   Start unit protection
    *   Handling correctly life since 2015
    *
    *   Credits to Dalvengyr
    ******************************************************************************/
    private function FinishUnitProtection takes nothing returns boolean
        local trigger lifeTrigger = GetTriggeringTrigger()
        local unit protectedUnit = GetTriggerUnit()
        local real life = LoadReal(h, GetHandleId(lifeTrigger), 0)
        
        if (GetUnitAbilityLevel(protectedUnit, SET_MAX_LIFE) > 0) then
            call UnitRemoveAbility(protectedUnit, SET_MAX_LIFE)
        endif
        
        call SetWidgetLife(protectedUnit, life)
        
        call FlushChildHashtable(h, GetHandleId(lifeTrigger))
        call DestroyTrigger(lifeTrigger)
        
        set lifeTrigger = null
        set protectedUnit = null
        
        return false
    endfunction
    
    private function ProtectUnit takes unit whichUnit, real damage returns nothing
        local trigger lifeTrigger = CreateTrigger()
        local real life = GetWidgetLife(whichUnit)
        local real maxLife = GetUnitState(whichUnit, UNIT_STATE_MAX_LIFE)
        local boolean magical = damage < 0
        
        call SaveReal(h, GetHandleId(lifeTrigger), 0, life)
        
        if (life + RAbsBJ(damage) > maxLife) then
            call UnitAddAbility(whichUnit, SET_MAX_LIFE)
        endif
        
        if (magical) then
            call SetWidgetLife(whichUnit, life)
            call TriggerRegisterUnitStateEvent(lifeTrigger, whichUnit, UNIT_STATE_LIFE, GREATER_THAN, life - damage / 2)
        else
            call SetWidgetLife(whichUnit, life + damage)
            call TriggerRegisterUnitStateEvent(lifeTrigger, whichUnit, UNIT_STATE_LIFE, LESS_THAN, life + damage / 2)
        endif
        
        call TriggerAddCondition(lifeTrigger, Condition(function FinishUnitProtection))
        
        set lifeTrigger = null
    endfunction
    /******************************************************************************
    *   End unit protection
    ******************************************************************************/
    
    private function DamageEngine takes nothing returns nothing
        local real rawAmount
        local real originalHealth
        local real oldAmount
        local real life

        // Set damage variables  
        set rawAmount = GetEventDamage()
        
        if rawAmount == 0.0 then
            return
        endif
        
        set PDDS.source = GetEventDamageSource()
        set PDDS.target = GetTriggerUnit()
        
        // Determine the damage type
        if rawAmount > 0.0 then
            if PDDS.damageType != CODE then
                set PDDS.damageType = PHYSICAL
            endif
            set PDDS.amount = rawAmount
        else
            if PDDS.damageType != CODE then
                set PDDS.damageType = SPELL
            endif
            set PDDS.amount = -rawAmount
        endif
        
        // Register spell reduction above 100%
        static if USE_SPELL_RESISTANCE_AUTO_DETECT then
            set PDDS.amount = GetUnitSpellResistance(PDDS.target)*PDDS.amount
        else
            if ( IsUnitType(PDDS.target, UNIT_TYPE_ETHEREAL) and IsUnitEnemy(PDDS.target, GetOwningPlayer(PDDS.source)) and rawAmount < 0.0 ) then
                set PDDS.amount = ETHEREAL_DAMAGE_FACTOR*PDDS.amount
            endif
        endif
        
        // Register spell damage reducing items like runed bracers
        if ( IsUnitType(PDDS.target, UNIT_TYPE_HERO) and UnitHasItemOfType(PDDS.target, SPELL_DAMAGE_REDUCTION_ITEM) > 0 ) and rawAmount < 0.0 then
            set PDDS.amount = (1 - BRACERS_SPELL_DAMAGE_REDUCTION)*PDDS.amount
        endif
        
        set originalHealth = GetWidgetLife(PDDS.target)
        
        // Call damage handlers
        set damageEventTrigger = 1.0
        set damageEventTrigger = 0.0
        
        set life = originalHealth - PDDS.amount
        
        if (life > UNIT_MIN_LIFE) then
            call SetWidgetLife(PDDS.target, life)
            call ProtectUnit(PDDS.target, rawAmount)
        else
            // So units can explode correctly
            if (PDDS.damageType == PHYSICAL) then
                call SetWidgetLife(PDDS.target, UNIT_MIN_LIFE)
            else
                call DisableTrigger(damageEvent)
                call UnitDamageTarget(PDDS.source, PDDS.target, 100000, true, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNIVERSAL, null)
                call EnableTrigger(damageEvent)
            endif
        endif

        // Handle allocated attacks from UnitDamageTargetEx
        if totalAllocs == 0 then
            set totalAllocs = allocatedAttacks
        endif
        
        if allocatedAttacks > 0 then
            set allocatedAttacks = allocatedAttacks - 1
            set allocCounter = allocCounter + 1
            
            call TriggerEvaluate(runAllocatedAttacks)
        endif

        // Reset all required variables
        set totalAllocs = 0
        set allocCounter = -1
        set PDDS.damageType = -1
    endfunction
    
    
    /******************************************************************************
    *   Initialization
    ******************************************************************************/
    
    private function RestoreTriggers takes nothing returns nothing
        local unit enumUnit = GetEnumUnit()
    
        // Re-register units that are alive
        if GetWidgetLife(enumUnit) >= UNIT_MIN_LIFE then
            call TriggerRegisterUnitEvent(damageEvent, enumUnit, EVENT_UNIT_DAMAGED)
        endif
        set enumUnit = null
    endfunction

    private function ClearMemory_Actions takes nothing returns nothing
        local group g = CreateGroup()
        local code c = function DamageEngine
        
        // Reset the damage event
        call GroupEnumUnitsInRect(g, GetWorldBounds(), null)
        call ResetTrigger(damageEvent)
        call DestroyTrigger(damageEvent)
        set damageEvent = null
       
        // Rebuild it then
        set damageEvent = CreateTrigger()
        call TriggerAddCondition(damageEvent, Filter(c))
        call ForGroup(g, function RestoreTriggers)
        
        // Clean things up
        call DestroyGroup(g)
        set g = null
        set c = null
    endfunction
    
    private function MapInit takes nothing returns nothing
        local unit enumUnit = GetEnumUnit()
    
        // Register units on map initialization
        call UnitAddAbility(enumUnit, DAMAGE_TYPE_DETECTOR)
        call UnitMakeAbilityPermanent(enumUnit, true, DAMAGE_TYPE_DETECTOR)
        call TriggerRegisterUnitEvent(damageEvent, enumUnit, EVENT_UNIT_DAMAGED)
        set enumUnit = null
    endfunction
    
    private function UnitEntersMap takes nothing returns nothing
        local unit triggerUnit = GetTriggerUnit()
    
        // Register units that enter the map
        if ( GetUnitAbilityLevel(triggerUnit, DAMAGE_TYPE_DETECTOR) < 1 ) then
            call UnitAddAbility(triggerUnit, DAMAGE_TYPE_DETECTOR)
            call UnitMakeAbilityPermanent(triggerUnit, true, DAMAGE_TYPE_DETECTOR)
            call TriggerRegisterUnitEvent(damageEvent, triggerUnit, EVENT_UNIT_DAMAGED)
        endif
        set triggerUnit = null
    endfunction
    
    private function RunAllocatedAttacks takes nothing returns nothing
        local integer localAllocAttacks = allocatedAttacks + 1

        // Calculate the correct sequence of allocated attacks
        set localAllocAttacks = localAllocAttacks - totalAllocs + 1 + 2*allocCounter
        
        // Deal code damage if the unit isn't exploding
        set PDDS.damageType = CODE
        if GetWidgetLife(LoadUnitHandle(h, localAllocAttacks, 1)) >= UNIT_MIN_LIFE then
            call UnitDamageTarget(LoadUnitHandle(h, localAllocAttacks, 0), LoadUnitHandle(h, localAllocAttacks, 1), /*
                                */LoadReal(h, localAllocAttacks, 2), LoadBoolean(h, localAllocAttacks, 3), /*
                                */LoadBoolean(h, localAllocAttacks, 4), ConvertAttackType(LoadInteger(h, /*
                                */localAllocAttacks, 5)), ConvertDamageType(LoadInteger(h, localAllocAttacks, 6)), /*
                                */ConvertWeaponType(LoadInteger(h, localAllocAttacks, 7)))
        else
            call FlushChildHashtable(h, localAllocAttacks - 1)
        endif
        
        // Clean things up
        call FlushChildHashtable(h, localAllocAttacks)
    endfunction
    
    private module Inits
        private static method onInit takes nothing returns nothing
            local group g = CreateGroup()
            local region r = CreateRegion()
            local trigger UnitEnters = CreateTrigger()
            local trigger ClearMemory = CreateTrigger()
            local code cDamageEngine = function DamageEngine
            local code cUnitEnters = function UnitEntersMap
            local code cClearMemory = function ClearMemory_Actions
            local code cRunAllocatedAttacks = function RunAllocatedAttacks
            
            // Initialize variables
            set h = InitHashtable()
            set damageEvent = CreateTrigger()
            set damageHandler = CreateTrigger()
            set PDDS.damageType = -1
            set allocatedAttacks = 0
            set runAllocatedAttacks = CreateTrigger()
            set totalAllocs = 0
            set allocCounter = -1
            
            // Register units on map initialization
            call TriggerRegisterVariableEvent(damageHandler, SCOPE_PRIVATE + "damageEventTrigger", EQUAL, 1.0)
            call TriggerAddCondition(damageEvent, Filter(cDamageEngine))   
            call GroupEnumUnitsInRect(g, GetWorldBounds(), null)
            call ForGroup(g, function MapInit)
            
            // Register units that enter the map
            call RegionAddRect(r, GetWorldBounds())
            call TriggerRegisterEnterRegion(UnitEnters, r, null)
            call TriggerAddCondition(UnitEnters, Filter(cUnitEnters))
                    
            // Register trigger for allocated attacks
            call TriggerAddCondition(runAllocatedAttacks, Filter(cRunAllocatedAttacks))
            
            // Clear memory leaks
            call TriggerRegisterTimerEvent(ClearMemory, TRIGGER_CLEANUP_PERIOD, true)
            call TriggerAddCondition(ClearMemory, Filter(cClearMemory))
        
            // Clean things up
            call DestroyGroup(g)
            set UnitEnters = null
            set ClearMemory = null
            set cDamageEngine = null
            set cUnitEnters = null
            set cClearMemory = null
            set cRunAllocatedAttacks = null
            set g = null
            set r = null
        endmethod
    endmodule
    
    private struct Init extends array
        implement Inits
    endstruct
endlibrary

edit: bug, units do not explode.
edit: fixed
 
I am still studying the revolution that is this resource. So spell shield at 200% doesn't block heals? Also, does this have the flickering health bar problem that mine has? And, if so, how did you fix that?

Also, minor optimization: when you loop through your hashtable, you do a lot of:

exitwhen ( LoadTriggerConditionHandle(h, id, index) == null )

You could, instead, do:

exitwhen not HaveSavedHandle(h, id, index)
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
I am still studying the revolution that is this resource. So spell shield at 200% doesn't block heals?

No, heals are not blocked. Luckily bracers play quite well with this mechanism.

Also, does this have the flickering health bar problem that mine has? And, if so, how did you fix that?

No, healthbar flickering is avoided with setting the health value to an appropriate amount before releasing the damage event. This way health bars never flicker.

There is still the (very small) chance of health value flickering (the numeric value), but this can't be avoided. There was the effort (espcially by Nestharus) to overcome this problem too by using unit state events instead of timers (like the one Ruke posted), but Nestharus came to the conclusion that this can't be used reliable because of inaccuracies of the event.

Also, minor optimization:

Will take a look, thx.
 
unit state events can be used reliably. Always use a unit state event except when the unit state event won't register. In these cases, use a timer. The value will be so small that you will not have to use a custom unit life thing. You'll only need to add like 1 hp to the unit. There will be no healthbar flickering and the life will be within like .03 of the actual value.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
unit state events can be used reliably. Always use a unit state event except when the unit state event won't register. In these cases, use a timer. The value will be so small that you will not have to use a custom unit life thing. You'll only need to add like 1 hp to the unit. There will be no healthbar flickering and the life will be within like .03 of the actual value.

Is there a working implementation of this mechanism available? Because your latest update in that thread was that DDS is still buggy (and some of its required libraries too). If they can be used reliable that would be great.
 
I just haven't done it yet. The principle is simple. Figure out the lowest amount of change that unit state event can detect and determine if this is messed with by floating point accuracy or not. Just set the unit's health with an event using very small numbers on a timer or something and throw a message when a value isn't registered. Start at like 1 and decrease by .001 each time or something. Try this with low max health (like 1 or .1), medium max health (like 100), and high max health (like 1,000,000,000). Once you find the value, then do a simple if-statement when you are preparing to prepare the unit for saving. If the damage is > the value, then just use the trigger. If it's less than the value, use the timer.

I dunno when I'll do this myself. I start working for HP tomorrow, so I'll have less free time. I just haven't been motivated. The steps themselves are pretty easy if you want to figure out the values and stuff for yourselves. If the values are different based on max unit health, it makes it a little bit more complicated, but it should still be pretty easy : ).
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
I dunno when I'll do this myself. I start working for HP tomorrow, so I'll have less free time.

Nice, congrats :)

If the values are different based on max unit health, it makes it a little bit more complicated, but it should still be pretty easy

Hm ok, that sounds like there still has to be put some research into this... I'm also concered about event ordering because unit state events fire synchronous and timers asynchronous, so mixing those two might compicate things further and introduce new problems...
 
Hm ok, that sounds like there still has to be put some research into this... I'm also concered about event ordering because unit state events fire synchronous and timers asynchronous, so mixing those two might compicate things further and introduce new problems...

It'll work just fine if you do it like I did it back when I was using timers.

Nice, congrats :)

Working as a programmer.
 
Level 14
Joined
Dec 12, 2012
Messages
1,007
Actually, the system is a little complex. If you process a damage event on a health event and there are events on timers, it should all run together and timer stuff should be cleared. The timer damage already ran : ). If timer gies through, clear batch of jobs.

Yes hm, sounds reasonable, but getting all this perfectly right will be tricky... Especially since floating point arithmetic is really prone to rounding errors. Blizzard should just have used integers for this stuff ^^
 
Level 24
Joined
Aug 1, 2013
Messages
4,658
Figure out the lowest amount of change that unit state event can detect
I am afraid that it detects the change from a round number from the unit's state itself.

So when a unit has 123.456 hp, then the minimum change that it can detect is 0.457 lower or 0.545 higher.
(This is just a wild guess cause I am too lazy to just try it :D)

However there is a bright side... damages have a miminum of 1, which means that the unit will take at least 1 damage.
Which means that the minimum doesnt have to be that low.
Remember that we are still talking about the original damage that was dealt and NOT the damage that we apply :D

Perfect. If anyone needs Nestharus, it's HP. First day on the job: bros you're coding this all wrong! No don't couple those scripts! This printer queue could be 600x faster. Are you seriously preinstalling bloatware on these machines!?!? It's like I'm working for a zoo!

Dont underestimate Nes...
However, I am sure that none of Nes' employees actually understood what he told them to do, so it is only natural that nothing went right :D

On the other hand...
JASS:
function ADE_Remove_Standard_Damage takes nothing returns boolean
    local unit whichUnit
    local integer unitId
    
    set whichUnit = GetTriggerUnit()
    set unitId = GetUnitUserData(whichUnit)
    call UnitRemoveAbility(whichUnit, udg_ADE_ABILITY_CHEAT_DEATH)
    call SetWidgetLife(whichUnit, udg_ADE_HealthArray[unitId])
    set udg_ADE_HealthArray[unitId] = 0.
    call DestroyTrigger(GetTriggeringTrigger())
    
    set whichUnit = null
    return false
endfunction

...

        //instead of start timer:
        set udg_ADE_HealthArray[targetId] = GetWidgetLife(target)
        call UnitAddAbility(target, udg_ADE_ABILITY_CHEAT_DEATH)
        call SetWidgetLife(target, GetWidgetLife(target) + amount)
        
        set trig = CreateTrigger()
        if amount > 0. then
            call TriggerRegisterUnitStateEvent(trig, target, UNIT_STATE_LIFE, LESS_THAN, GetWidgetLife(target) -1)
        else
            call TriggerRegisterUnitStateEvent(trig, target, UNIT_STATE_LIFE, GREATER_THAN, GetWidgetLife(target) +1)
        endif
        call TriggerAddCondition(trig, udg_ADE_FilterFunc)
        set trig = null

This means that this:
  • TEST
    • Events
      • Player - Player 1 (Red) skips a cinematic sequence
    • Conditions
    • Actions
      • Unit - Create 1 Footman for Player 1 (Red) at (Center of (Playable map area)) facing Default building facing degrees
      • Game - Display to (All players) the text: (unit hp = + (String((Life of (Last created unit)))))
      • Unit - Cause (Last created unit) to damage (Last created unit), dealing 123.00 damage of attack type Normal and damage type Normal
      • Game - Display to (All players) the text: (unit hp = + (String((Life of (Last created unit)))))
is now (except for leaks) a perfect trigger.
It shows 420 and then 310.192.
So... I will definately be using this method... and I suggest you to just try it too.
(I just have to find out if the boolexpr leaks... I dont think so any more :/)
 
Level 24
Joined
Aug 1, 2013
Messages
4,658
It doesnt matter if it detects when a unit dies.
It matters if it detects when the damage has been taken.

The problem about a DDS is that the original damage has to be replaced by a different damage created and applied by the DDS.
However there is no way how you can remove that original damage or reduce it so we give the target a cheat death ability and increase his current health to make him survive that damage.
We do have to remove that ability and set the life of that unit once that damage has been taken.

We used to do a 0s timer that would expire after the damage has been taken.
However that means that any other actions that are done before the timer phase of the game runs and after the damage has been taken is basically in a danger zone because GetLife(), SetLife(), GetMaxLife(), etc, etc, etc are then invalid.
Therefor we either use custom functions or hooks to set the maximum health back earlier than the timer.

But because hooks arent really that nice and custom functions isnt that lovely either, having an event that detects when the life of a unit has changed does give us a certain moment that the damage has been applied.
However this event is (like "a unit takes damage") not able to be a generic event nor able to be of generic values.
The life of that unit had to be higher than the given value to the event and becomes lower or must be lower and becomes higher.

I will do some tests dealing 1 damage to a unit by both spell and normal damage to find out if everyting works.
But if that 1 is good enough, then we (I because you wont use my system) have a DDS that doest break all those GetWidgetLife(), GetUnitState(), etc, etc.
 
I had a regular unit hitting a hero with 500 hp and a TON of armor and I bugged the unit state event detection. I turned it off and I was doing a small fraction of damage. After awhile, the hero health went down to 499. This was actually my test case : ). DDS ignores damage events of 0, and the thing went through. I also observed how much damage was done and it was a very small number. It was not 0 : 0.
 
Level 24
Joined
Aug 1, 2013
Messages
4,658
That second display message shows you that that unit still has the cheat death ability.
When you use unit events instead of a 0s timer, that means that you remove that ability instantly when the damage is applied.

I have done some tests and it seems that the lowest amount of health change detected is 0.0625. (1/16)
So if your spell damage or triggered damage is lower than 0.0626, then it fails... and to be honest, that amount is unable to reach with spells and really stupid to have in triggers.

@Xonok
I do hope not.
At least the normal Unit takes damage event fires even when 0.07 is used (smallest amount higher than 0.0625 that I can write in that GUI trigger)
So I assume that lfh's DDS wouldnt have problems recognizing it either.

But still basic attacks do have a minimum of 1 damage.
I had a unit with 2 damage hitting my hero with 505 armor. (97% reduction)
Which means that the hero should take only 0.06 damage.
However the amount showed b y GetEventDamage() is still 1.
So I dont know what you did there Nes, but it certainly didnt do what was supposed to happen.
 
Level 23
Joined
Apr 16, 2012
Messages
4,041
not stupid. Imagine I have map where units have low hp, like 10. And then I have some fire that deals 1 dmg/sec but I want it nice and contiguous, so I do 1/30 dmg, and that is a lot lower than 1/16.

Yes, not necessarily damage event per se, but could be applied by UnitDamageTarget, then it is :D

It is long known that the lowest damage unit can strike for is 1 despite armor reduction
 
LFH, that runed bracers trick is a godsend. What a great find!

I have updated GUI Damage Engine to use this trick and the results are great. While studying your resource I have found some improvements that I want to share with you:

GetWorldBounds() leaks each time you call it. Just set it to a variable during init for reference.

You don't need to trigger the runed bracers ability. In object editor, set the runed bracers ability value to 1.67 and that's all you need.

I have mostly remedied Life Drain, but I am not sure if I overlooked something. Unlike for locust swarm and runed bracers, Object Editor cannot be used to correct life drain.

You only need one unitdamagetarget, regardless of if it was ethereal or if it has the spell detect ability. Just use UnitDamageTarget(s, t, -999, false, false, null, DAMAGE_TYPE_UNIVERSAL, null)

There are more I want to share, but I'm out of time for now.
 
Level 24
Joined
Aug 1, 2013
Messages
4,658
You don't need to trigger the runed bracers ability. In object editor, set the runed bracers ability value to 1.67 and that's all you need.
That is not entirely true.
What WC3 does is it sets a value (magic resist) to the value of the item gained (the runed bracers).
This OVERWRITES the old value so using several runed bracers has no effect whatsoever.
However when you remove the runed bracers custom ability (by the dds), then the old one works again.

So changing the settings of the regular abilities doesnt work, but readding the DDS spell damage reduction whenever a unit gained any other Spell Damage Reduction ability would work. Which is what I try.
LFH, simply removes that and triggers it.

You only need one unitdamagetarget, regardless of if it was ethereal or if it has the spell detect ability. Just use UnitDamageTarget(s, t, -999, false, false, null, DAMAGE_TYPE_UNIVERSAL, null)
DamageType could be null as well iirc.
 
Top