[vJASS] DamageEvent

Since there are now events for changing damage without requiring a cheat death ability (which also makes importing less annoying) I figured I'd make this DDS since no one else (except Bribe, whose system is more GUI-friendly) seems to have done it. We don't quite have a spell detection boolean, however, so this still uses the Runed Bracers trick.

Requires Warcraft III version 1.29 or newer.

PS: If you have another idea for a name for this system, I'd love to hear it, if only to avoid confusion with Nestharus' own DamageEvent.

JASS:
library DamageEvent   /* v1.02.1
  
    by Spellbound
  
  
    DESCRIPTION
    ¯¯¯¯¯¯¯¯¯¯¯
    DamageEvent is a damage detection system that uses the new native functions that were added with
    patch 1.29. Detecting damage and even changing it is now easier than before.
  
  
    REQUIREMENTS  
    ¯¯¯¯¯¯¯¯¯¯¯¯
*/  uses  /*

*/  UnitDex             /* Needed to setup units for damage detection and cleanup when they are removed.
                        // https://www.hiveworkshop.com/threads/system-unitdex-unit-indexer.248209/    
                      
*/  optional            /*

*/  RegisterNativeEvent // Recommended if you wish to have finer control over your damage event triggers
                        // https://www.hiveworkshop.com/threads/snippet-registerevent-pack.250266/

/*
    INSTALLATION
    ¯¯¯¯¯¯¯¯¯¯¯¯
    Import UnitDex into your map and then copy the ability Spell Damage Detection (DamageEvent). If
    able, change the spell id to SPDD. Otherwise, chance the value of SPELL_DMG_DETECT in the globals
    block below to that of Spell Damage Detection (DamageEvent). Ctrl + D allows you to view object
    ids in the object editor.
  
    Copy this library into your map and you are ready to start using it.
  
  
    NOTES
    ¯¯¯¯¯
    1) Spell damage detection works by giving all units a modified Runed Bracer ability that reduces
    all spell damage by 200%, thus taking it into the negative. When DamageEvent detects a negative
    number it assumes it's magical and sets DmgEvent.class to DAMAGE_CLASS_SPELL and then returns the
    damage to a positive number. If you intend to heal using a modified damage-based spell (eg Storm
    Bolt), the damage will become a positive number and will instead be detected as physical damage.
    Any additional spell-damage reduction like Elune's Grace will still affect the Storm Bolt's
    damage but the system will believe it's phyiscal. Healing through negative spell damage won't
    work - you will have to trigger it.
  
    2) I have substituted the word 'type' to 'class' to avoid confusion with the native variable type
    called damagetype. This is because DamageEvent comes with a custom function to damage targets
    that takes a DamageClass instead of a weapontype. An extended version exists that allows you to
    input a weapontype as well, should you find the need for it. With this custom damage function,
    your DamageClass is preset and when an onDamage event fires, DmgEvent.class will return that
    instead of just DAMAGE_CLASS_PHYSICAL or DAMAGE_CLASS_SPELL. You may also use that to force the
    system to detect a phyiscal damage as spell by setting it's class to DAMAGE_CLASS_SPELL,
    although it will still not be subject to vanilla effects that reduce spell damage.
  
    3) Runed Bracers and Elune's Grace will not work. They have to be triggered.
  
    4) Mana Shield will fail to trigger a damage event. Set all damage reduction values to zero, and
    trigger the effects or use the fix provided in onDamage.
  
    5) Life Drain will not do anything. Trigger it or use the fix provided.
  
    6) Damage Return Factor on Locust Swarm must be set to a negative number, or else the Locusts
    will damage the Crypt Lord rather than heal him when they return to him.
  
    7) Anti-Magic Shell with a shield life will not work and must be triggered.
  
  
    API
    ¯¯¯
        EVENT
      
        if using RegisterNativeEvent...
      
            call RegisterNativeEvent(EVENT_PLAYER_UNIT_DAMAGED, function whichFunction)
                ^ Registers a DamageEvent event that will run whichFunction whenever a unit takes
                damage. If a unit is not indexed, it will not fire the event when it takes damage.
          
        if NOT using RegisterNativeEvent...
      
            call DamageEvent.register(function whichFunction)
                ^ Registers a DamageEvent event that will run whichFunction whenever a unit takes
                damage. If a unit is not indexed, it will not fire the event when it takes damage.
      
        EVENT RESPONSE
      
        /* IMPORTANT: /* Always use a LOCAL DamageEvent variable */*/
      
        eg:
      
        local DamageEvent Damage = DmgEvent
      
        you can then proceed to modify its value as you wish:
      
        Damage.amount is the amount inflicted after reduction via armour type and rating. This
            variable can be modified to change damage inflicted.
        Damage.class is the damage type - either Physical or Spell, but new ones can be added.
        Damage.source is the one inflicting the damage
        Damage.target is the unit being damaged
        Damage.default is a boolean that informs the system that DamageClassOverride is not zero.
            This will allow you to check is a damage was inflicted normally (via attack or spell)
            and not via triggers, specifically, through UnitApplyDamage() or UnitApplyDamageEx().
            UnitDamageTarget() will still be treated as default physical damage.
      
        UTILITY
      
        set YOUR_CUSTOM_DAMAGE_CLASS = DamageClass.create()
            ^ You can create custom damage classes this way. This system only comes with PHYSICAL and
            SPELL, but should you need to, you can make new ones that have their own interactions. An
            example would be a system of elemental damage.
          
            Ref NOTES 2) for an explanation of what a damage 'class' is.
      
        call UnitApplyDamage(whichSource, whichTarget, whichDamage, isAttack, isRanged, attackType, damageType, damageClass)
            ^ Use this instead of UnitDamageTarget. It takes the same arguments, except the last
            one is used for your custom DamageClass.
          
        call UnitApplyDamageEx(whichSource, whichTarget, whichDamage, isAttack, isRanged, attackType, damageType, weapontype, damageClass)
            ^ Use this instead of UnitDamageTarget. It takes the same arguments in addition of a
            DamageClass argument.
*/

globals
  
    private constant integer    SPELL_DMG_DETECT    = 'SPDD'
    private constant real       ETHEREAL_FACTOR     = 1.6666
    private constant real       REFRESH_PERIOD      = 30.
  
    // Do not edit those EVER or everything will break. You have been warned.
    DamageClass  DAMAGE_CLASS_PHYSICAL = 0
    DamageClass  DAMAGE_CLASS_SPELL = 0
  
    private DamageClass  DamageClassOverride = 0
    DamageEvent DmgEvent = 0
  
    private integer Count = 0
    private trigger RegTrig = CreateTrigger()
    private trigger RunTrig = CreateTrigger()
    private timer Clock = CreateTimer()
  
    integer EVENT_PLAYER_UNIT_DAMAGED = 0
  
endglobals

struct DamageClass
  
    static method create takes nothing returns thistype
        return allocate()
    endmethod
endstruct

struct DamageEvent
  
    readonly unit source
    readonly unit target
    readonly DamageClass class
    readonly boolean default
    real amount
  
    private method destroy takes nothing returns nothing
        set this.source = null
        set this.target = null
        set this.default = false
        call this.deallocate()
    endmethod
  
    static method onDamage takes nothing returns boolean
        local real amt = GetEventDamage()
        local thistype this
      
        static if LIBRARY_RegisterNativeEvent then
            local integer playerId
        endif

        if amt == 0. then
            return false
        endif
      
        set this = allocate()
        set DmgEvent = this
      
        set this.source = GetEventDamageSource()
        set this.target = GetTriggerUnit()
      
        if amt > 0. then
            set this.class = DAMAGE_CLASS_PHYSICAL

        elseif amt < 0. then
            set this.class = DAMAGE_CLASS_SPELL
            if IsUnitType(this.target, UNIT_TYPE_ETHEREAL) then
                set amt = amt * ETHEREAL_FACTOR
            endif
            set amt = -amt
          
        endif
      
        // if a custom damage class was assigned...
        if DamageClassOverride != 0 then
            set this.class = DamageClassOverride
            set this.default = false
        else
            set this.default = true
        endif
      
        set this.amount = amt
      
        static if LIBRARY_RegisterNativeEvent then
            set playerId = GetPlayerId(GetOwningPlayer(this.target))
            call TriggerEvaluate(GetNativeEventTrigger(EVENT_PLAYER_UNIT_DAMAGED))
            if IsNativeEventRegistered(playerId, EVENT_PLAYER_UNIT_DAMAGED) then
                call TriggerEvaluate(GetIndexNativeEventTrigger(playerId, EVENT_PLAYER_UNIT_DAMAGED))
            endif
        else
            if IsTriggerEnabled(RunTrig) then
                call TriggerEvaluate(RunTrig)
            endif
        endif
      
        call BlzSetEventDamage(this.amount)
      
        set DmgEvent = 0
      
        call this.destroy()
      
        return false
    endmethod
  
    static method register takes code c returns nothing
        call TriggerAddCondition(RunTrig, Condition(c))
    endmethod
      
endstruct

private struct Link
    unit u
    thistype prev
    thistype next
    method destroy takes nothing returns nothing
        set this.u = null
        call this.deallocate()
    endmethod
    static method create takes unit u returns thistype
        local thistype this = allocate()
        set this.u = u
        set this.prev = 0
        set this.next = 0
        return this
    endmethod
endstruct

private module DamageEventInit
    private static method onInit takes nothing returns nothing
        call TriggerAddCondition(RegTrig, Filter(function DamageEvent.onDamage))
        call DisableTrigger(RegTrig)
      
        // If you use a different Unit Indexer, change those two lines below
        call OnUnitIndex(function thistype.onIndex)
        call OnUnitDeindex(function thistype.onRemove)
      
        set DAMAGE_CLASS_PHYSICAL = DamageClass.create()
        set DAMAGE_CLASS_SPELL = DamageClass.create()
      
        static if LIBRARY_RegisterNativeEvent then
            set EVENT_PLAYER_UNIT_DAMAGED = CreateNativeEvent()
        endif
    endmethod
endmodule

private struct DamageEventRegister
  
    private static Link First
    private static Link Last
    private static Link array LinkId
  
    private static method remove takes unit u returns nothing
        local Link link = LinkId[GetUnitId(u)]
      
        if link == First then
            set First = link.next
            set link.next.prev = 0
        elseif link == Last then
            set Last = link.prev
            set link.prev.next = 0
        else
            set link.prev.next = link.next
            set link.next.prev = link.prev
        endif
      
        set Count = Count - 1
        if Count == 0 then
            call PauseTimer(Clock)
            call DisableTrigger(RegTrig)
        endif
    endmethod
  
    private static method refresh takes nothing returns nothing
        local Link link = First
      
        call DestroyTrigger(RegTrig)
        set RegTrig = CreateTrigger()
      
        loop
            exitwhen link == 0
            call TriggerRegisterUnitEvent(RegTrig, link.u, EVENT_UNIT_DAMAGED)
            set link = link.next
        endloop
        call TriggerAddCondition(RegTrig, Filter(function DamageEvent.onDamage))
    endmethod
  
    private static method add takes unit u returns Link
        local Link link = Link.create(u)
      
        set Count = Count + 1
        if Count == 1 then
            set First = link
            set Last = link
            call TimerStart(Clock, REFRESH_PERIOD, true, function thistype.refresh)
            call EnableTrigger(RegTrig)
        else
            set link.prev = Last
            set Last.next = link
        endif
      
        set Last = link
        set link.next = 0
      
        call UnitAddAbility(u, SPELL_DMG_DETECT)
        call UnitMakeAbilityPermanent(u, true, SPELL_DMG_DETECT)
      
        return link
    endmethod
  
    private static method onIndex takes nothing returns nothing
        local unit u = GetIndexedUnit()
        call TriggerRegisterUnitEvent(RegTrig, u, EVENT_UNIT_DAMAGED)
        set LinkId[GetIndexedUnitId()] = thistype.add(u)
        set u = null
    endmethod
  
    private static method onRemove takes nothing returns nothing
        local integer id = GetIndexedUnitId()
        call thistype.remove(GetIndexedUnit())
        call LinkId[id].destroy()
        set LinkId[id] = 0
    endmethod
  
    implement DamageEventInit
  
endstruct

function UnitApplyDamage takes unit source, unit target, real damage, boolean attack, boolean ranged, attacktype atk, damagetype dmg, DamageClass dc returns nothing
    set DamageClassOverride = dc
    call UnitDamageTarget(source, target, damage, attack, ranged, atk, dmg, null)
    set DamageClassOverride = 0
endfunction

function UnitApplyDamageEx takes unit source, unit target, real damage, boolean attack, boolean ranged, attacktype atk, damagetype dmg, weapontype wpt, DamageClass dc returns nothing
    set DamageClassOverride = dc
    call UnitDamageTarget(source, target, damage, attack, ranged, atk, dmg, wpt)
    set DamageClassOverride = 0
endfunction

endlibrary

Demo:
JASS:
scope onDamage

struct onDamage
   
    private static method onDamage takes nothing returns nothing
        local DamageEvent Damage = DmgEvent
   
        local unit source = Damage.source
        local unit target = Damage.target
        local real damage = Damage.amount
        local DamageClass class = Damage.class
       
        // Life Drain
        local real healAmount
       
        // Mana Shield
        local real mana
        local real effectiveMana
        local real manaMult
        local real dmgReduction
        local real dmgToMana
        local real dmgToHP
        local real dmgResult
        local integer manaShieldHero
       
        // Mana Shield fix - based of Bribe's fix from Damage Engine
        if GetUnitAbilityLevel(target, 'BNms') > 0 then
           
            set mana = GetUnitState(target, UNIT_STATE_MANA)
           
            set manaShieldHero = GetUnitAbilityLevel(target, 'ANms')
           
            // Hero Mana Sheild (ANms)
            if manaShieldHero > 0 then
                // find the mana multiplier. If manaMult == 4. then it reduces 1 hp for every 4 points of mana.
                set manaMult = .5 + .5 * manaShieldHero
                // This will mimic the Damage Absorbed field.
                set dmgReduction = 1.
               
            // Unit Mana Shield (ACmf)
            else
                set manaMult = 2.
                set dmgReduction = 1.
           
            endif
           
            set dmgToMana = damage * dmgReduction
            set dmgToHP = damage - dmgToMana
           
            set damage = dmgToHP
           
            set effectiveMana = mana * manaMult
           
            if dmgToMana <= effectiveMana then
                set effectiveMana = effectiveMana - dmgToMana
                    set dmgResult = 0.
                set mana = effectiveMana / manaMult
            else
                set dmgResult = dmgToMana - effectiveMana
                call IssueImmediateOrderById(target, 852590) // order manashieldoff
                set mana = 0.
            endif
           
            call SetUnitState(target, UNIT_STATE_MANA, mana)
           
            set damage = damage + dmgResult
            set Damage.amount = damage
           
        endif
       
        // if PHYSICAL damage
        if Damage.class == DAMAGE_CLASS_PHYSICAL then
            call PoppingText.create(I2S(R2I(damage)), GetUnitX(target), GetUnitY(target), 120., 10., .15, COLOUR_PHYSICAL_DMG, GetOwningPlayer(source), VISIBILITY_ALL)
           
        // if SPELL damage
        elseif Damage.class == DAMAGE_CLASS_SPELL then
            call PoppingText.create(I2S(R2I(damage)), GetUnitX(target), GetUnitY(target), 120., 10., .2, COLOUR_MAGICAL_DMG, GetOwningPlayer(source), VISIBILITY_ALL)
           
            // put your spell damage reduction here
           
            // Life Drain fix - vanilla values
            if GetUnitAbilityLevel(source, 'Bdcl') > 0 and GetUnitAbilityLevel(target, 'Bdtl') > 0 then
                if GetUnitAbilityLevel(source, 'ANdr') > 0 then // hero life drain ability check
                    set healAmount = 10. + 15. * GetUnitAbilityLevel(source, 'ANdr')
                else
                    set healAmount = 55.
                endif
                call SetWidgetLife(source, GetWidgetLife(source) + healAmount)
            endif
       
        // if CUSTOM damage class
        else
       
        endif
       
        set source = null
        set target = null
    endmethod
   
    private static method onInit takes nothing returns nothing
        static if LIBRARY_RegisterNativeEvent then
            call RegisterNativeEvent(EVENT_PLAYER_UNIT_DAMAGED, function thistype.onDamage)
        else
            call DamageEvent.register(function thistype.onDamage)
        endif
    endmethod
   
endstruct

endscope

- v1.02.1 updated documentation to explain how to properly use DamageEvent (use a local variable). DamageEvent variable renamed from Damage to DmgEvent. Also fixed a couple of typos.
- v1.02 Fixed an issue where the links of the list holding all instanced units was not being removed when a unit was deindexed.
- v1.01 Fixed an issue where the wrong node in the global unit list was being destroyed.
- v1.00 initial release.


Hopefully I haven't missed anything!

Merry Christmas :)
 

Attachments

  • DamageEvent v1.02.1.w3x
    39 KB · Views: 52
Last edited:
Level 39
Joined
Jun 23, 2007
Messages
4,060
I feel it may be better to get @looking_for_help to update his library with a 1.29+ version unless this works different in some fundamental way which provides a benefit. As far as I know it would be a fairly simple update for his library to adapt to the newer patches.

Aside from that your library looks good. Although I have a feeling Blizzard may give us a simple event to detect damage in the future.
 
Well, like I said, no one updated their DDS aside from Bribe, so I just posted mine here in case someone needed something up to date. This library doesn't require any Cheat Death abilities or tricks to nullify damage, it used BlzSetEventDamage() . While we wait for patch 1.31, that all we're getting for the time being.

I'm not familiar with how looking_for_help's DDS works, so I can't really say how different or similar this library is from his.
 
Top