• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

[Lua] How to make items receive Damage Taken events like units?

Level 3
Joined
Feb 8, 2025
Messages
26
Hello, I'm trying to output only the effect targeting the item, and I'm trying to run it with the Damage Taken event, so please point out the problem.

For reference, the Issued an order targeting an objects event has an error where the effect output timing is checked even when you are under attack speed-related buffs or debuffs, or the effect is output even when the attack is canceled while attacking an item, so I plan to use Damage Taken.

This is an existing trigger
  • Events
    • Unit - A unit Is issued an order targeting an object
  • Conditions
    • (Unit-type of (Ordered unit)) Equal to Coatl
    • (Issued order) Equal to (Order(attack))
    • ((Ordered unit) is alive) Equal to True
  • Actions
    • Wait until ((Distance between (Position of (Ordered unit)) and (Position of (Target item of issued order))) Less than or equal to 460.00), checking every 0.10 seconds
    • Wait 0.33 seconds
    • Special Effect - Create a special effect at (Position of (Target item of issued order)) using Abilities\Spells\Other\Monsoon\MonsoonBoltTarget.mdl
    • Special Effect - Destroy (Last created special effect)
This is the trigger that applies the Damage Taken event.
Lua:
function CoatlHitsItem()
    local trig = CreateTrigger()
  
    TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DAMAGED)
  
    TriggerAddCondition(trig, Condition(function()
        local attacker = GetEventDamageSource()
        local target = GetTriggerUnit()
      
        return GetUnitTypeId(attacker) == FourCC('nwgs') and IsItem(target)
    end))
  
    TriggerAddAction(trig, function()
        local target = GetTriggerUnit()
        local x = GetUnitX(target)
        local y = GetUnitY(target)
      
        local effect = AddSpecialEffect("Abilities\\Spells\\Other\\Monsoon\\MonsoonBoltTarget.mdl", x, y)
      
        TimerStart(CreateTimer(), 1.0, false, function()
            DestroyEffect(effect)
            DestroyTimer(GetExpiredTimer())
        end)
    end)
end

function IsItem(unit)
    return GetItemTypeId(unit) ~= 0
end

function InitTrig_CoatlHitsItem()
    CoatlHitsItem()
end

  • Events
    • Map initialization
  • Conditions
  • Actions
    • Custom script: InitTrig_CoatlHitsItem()
 
Last edited by a moderator:
Level 3
Joined
Feb 8, 2025
Messages
26
Items are not Units so that Event won't work. There's no good way of doing this besides using a Unit to represent the Item.
Then, by applying the Unit - A unit is issued an order targeting an object event, can't the effect timing be adjusted according to the attack speed when a buff or debuff is applied, or the effect output be limited when an attack order is issued and then canceled?
 

Uncle

Warcraft Moderator
Level 73
Joined
Aug 10, 2018
Messages
7,875
That'll be pretty difficult to do and I can't see it working 100% of the time.

How about this idea:

Give your Unit a hidden Channel ability. Then whenever it issues an Attack order on an Item, instead, Order it to cast the Ability targeting the Item. Then create a trigger that runs when you cast that ability. This trigger will run an effect similar to Magic Leash - dealing damage every X seconds, where X is the unit's "attack cooldown".

1) Replacing issued Orders with a new one requires a 1 frame delay. Alternatively, you can Stun/Unstun the unit then Issue the new order. (BlzPauseUnitEx)
2) Channel should be hidden using the X: 0, Y: -11 trick, Options: Visible, Targeting: Units (items), and have a 99999 Follow Through Time.
3) Silence effects will cancel the ability, I forget if you can make the ability immune to it.
 
Last edited:
Level 3
Joined
Feb 8, 2025
Messages
26
That'll be pretty difficult to do and I can't see it working 100% of the time.

How about this idea:

Give your Unit a hidden Channel ability. Then whenever it issues an Attack order on an Item, instead, Order it to cast the Ability targeting the Item. Then create a trigger that runs when you cast that ability. This trigger will run an effect similar to Magic Leash - dealing damage every X seconds, where X is the unit's "attack cooldown".

1) Replacing issued Orders with a new one requires a 1 frame delay. Alternatively, you can Stun/Unstun the unit then Issue the new order. (BlzPauseUnitEx)
2) Channel should be hidden using the X: 0, Y: -11 trick, Options: Visible, Targeting: Units (items), and have a 99999 Follow Through Time.
3) Silence effects will cancel the ability, I forget if you can make the ability immune to it.
Even if I attack an item, the effect still doesn't come out. What's the problem?

  • Events
    • Unit - A unit Is issued an order targeting an object
  • Conditions
    • (Unit-type of (Ordered unit)) Equal to coatl
    • (Issued order) Equal to (Order(attack))
    • ((Ordered unit) is alive) Equal to true
    • (Target item of issued order) Not equal to No item
  • Actions
    • Unit Group - Order (Units of type coatl) to Neutral Dark Ranger - Life Drain (Target unit of issued order)
 

Attachments

  • 제목 없음.jpg
    제목 없음.jpg
    295.7 KB · Views: 6

Uncle

Warcraft Moderator
Level 73
Joined
Aug 10, 2018
Messages
7,875
You're targeting an ITEM not a UNIT.

Try something like this:
  • Actions
    • Custom script: call BlzPauseUnitEx( GetTriggerUnit(), true )
    • Custom script: call BlzPauseUnitEx( GetTriggerUnit(), false )
    • Custom script: call IssueTargetItemOrder( GetTriggerUnit(), "lifedrain", GetOrderTargetItem() )
You need to interrupt the first Order and replace it with a new Order to use the Ability. But I just realized that GUI doesn't let you specify the Order when targeting an Item so you have to use Custom Script. Also, I doubt that Life Drain can even target Items.
 
Last edited:
Level 3
Joined
Feb 8, 2025
Messages
26
You're targeting an ITEM not a UNIT.

Try something like this:
  • Actions
    • Custom script: call BlzPauseUnitEx( GetTriggerUnit(), true )
    • Custom script: call BlzPauseUnitEx( GetTriggerUnit(), false )
    • Custom script: call IssueTargetItemOrder( GetTriggerUnit(), "lifedrain", GetOrderTargetItem() )
You need to interrupt the first Order and replace it with a new Order to use the Ability. But I just realized that GUI doesn't let you specify the Order when targeting an Item so you have to use Custom Script. Also, I doubt that Life Drain can even target Items.
Even if an attack order is given, it does not attack.
  • Coatl Wait for Attack Range Copy
    • Events
      • Unit - A unit Is issued an order targeting an object
    • Conditions
      • (Unit-type of (Ordered unit)) Equal to coatl
      • (Issued order) Equal to (Order(attack))
    • Actions
      • Custom script: local u = GetTriggerUnit()
      • Custom script: BlzPauseUnitEx(u, true)
      • Custom script: BlzPauseUnitEx(u, false)
      • Custom script: IssueTargetItemOrder(u, "lifedrain", GetOrderTargetItem())
 
Level 3
Joined
Feb 8, 2025
Messages
26
You're targeting an ITEM not a UNIT.

Try something like this:
  • Actions
    • Custom script: call BlzPauseUnitEx( GetTriggerUnit(), true )
    • Custom script: call BlzPauseUnitEx( GetTriggerUnit(), false )
    • Custom script: call IssueTargetItemOrder( GetTriggerUnit(), "lifedrain", GetOrderTargetItem() )
You need to interrupt the first Order and replace it with a new Order to use the Ability. But I just realized that GUI doesn't let you specify the Order when targeting an Item so you have to use Custom Script. Also, I doubt that Life Drain can even target Items.
Considering that not only life drain but most of the remaining skills do not attack items, it seems that there is no answer unless the monsoon effect is modified like the Chimaera attack effect.
 
Level 3
Joined
Feb 8, 2025
Messages
26
Here's the idea, although I needed two abilities because the "Targeting" ability would automatically stop channeling for some odd reason.
The motion is not smooth, the most important effects are not visible, and it is not clear whether the effect output timing is faster or slower depending on the attack speed buff and debuff.
 

Uncle

Warcraft Moderator
Level 73
Joined
Aug 10, 2018
Messages
7,875
The motion is not smooth, the most important effects are not visible, and it is not clear whether the effect output timing is faster or slower depending on the attack speed buff and debuff.
You can edit it to launch a missile, change when the attack animation plays, etc.

It's not going to be very easy though, because the game doesn't have the functions necessary to make it easy. But what I showed you is a good start.
 
Level 3
Joined
Feb 8, 2025
Messages
26
You can edit it to launch a missile, change when the attack animation plays, etc.

It's not going to be very easy though, because the game doesn't have the functions necessary to make it easy. But what I showed you is a good start.
Then, can't we make it again based on this trigger? The effect comes out smoother than the previous map. Of course, there is an error where the effect is output when you enter the range even if you cancel the attack command. Also, I think it will be easier to set the trigger if you change the waiting time of Coatl (Animation Damage Point: 0.33, Cooldown time: 2.00) flexibly according to the attack speed buff and debuff.
  • Events
    • Unit - A unit Is issued an order targeting an object
  • Conditions
    • (Unit-type of (Ordered unit)) Equal to Coatl
    • (Issued order) Equal to (Order(attack))
    • ((Ordered unit) is alive) Equal to True
  • Actions
    • Wait until ((Distance between (Position of (Ordered unit)) and (Position of (Target item of issued order))) Less than or equal to 460.00), checking every 0.10 seconds
    • Wait 0.33 seconds
    • Special Effect - Create a special effect at (Position of (Target item of issued order)) using Abilities\Spells\Other\Monsoon\MonsoonBoltTarget.mdl
    • Special Effect - Destroy (Last created special effect)
 
Level 45
Joined
Feb 27, 2007
Messages
5,578
At the end of the day all you want to do is display a special effect when an item takes damage from an attack, right? Here's a stupid, inefficient alternative solution for that:
  • Run a trigger with very high frequency (0.10 period? maybe faster or slower is acceptable) that searches the playable map area for items on the ground and then loops over those items.
  • Check the current life of each item you encounter (GetWidgetLife works IDK if there's a GUI alternative), and then save that value in a hashtable using the item itself as one of the keys.
  • Compare the current life of the item to the last known hp value of the item. If the numbers are different then the item took damage between the most recent timer tick and 'now' so you should create the special effect.
  • Clearing the hashtable of unnecessary data belonging to destroyed items is a little bit more difficult since, as Uncle said, there's no "item dies" or "item is removed from map" or "item is fully consumed" events you can use.
This will not allow you to figure out which unit dealt the damage to the item, just that damage was dealt. Even a trigger changing item health would read as damage to the item with this method.

I think another alternative would be to give items 1 hp, and then alter the death animation of the item box so that it plays the correct effect as part of the death animation directly. That doesn't allow items to survive multiple hits and play the effect on each hit, though.
 
Level 3
Joined
Feb 8, 2025
Messages
26
I think another alternative would be to give items 1 hp, and then alter the death animation of the item box so that it plays the correct effect as part of the death animation directly. That doesn't allow items to survive multiple hits and play the effect on each hit, though.
"I think another alternative would be to give items 1 hp, and then alter the death animation of the item box so that it plays the correct effect as part of the death animation directly. That doesn't allow items to survive multiple hits and play the effect on each hit, though."

this parts Isn't this supposed to work when items are destroyed by other units as well as certain units?
 
Level 3
Joined
Feb 8, 2025
Messages
26
At the end of the day all you want to do is display a special effect when an item takes damage from an attack, right? Here's a stupid, inefficient alternative solution for that:
  • Run a trigger with very high frequency (0.10 period? maybe faster or slower is acceptable) that searches the playable map area for items on the ground and then loops over those items.
  • Check the current life of each item you encounter (GetWidgetLife works IDK if there's a GUI alternative), and then save that value in a hashtable using the item itself as one of the keys.
  • Compare the current life of the item to the last known hp value of the item. If the numbers are different then the item took damage between the most recent timer tick and 'now' so you should create the special effect.
  • Clearing the hashtable of unnecessary data belonging to destroyed items is a little bit more difficult since, as Uncle said, there's no "item dies" or "item is removed from map" or "item is fully consumed" events you can use.
This will not allow you to figure out which unit dealt the damage to the item, just that damage was dealt. Even a trigger changing item health would read as damage to the item with this method.

I think another alternative would be to give items 1 hp, and then alter the death animation of the item box so that it plays the correct effect as part of the death animation directly. That doesn't allow items to survive multiple hits and play the effect on each hit, though.
And this doesn't show any effect whether you destroy it in one shot or in multiple shots.

  • Events
    • Time - Every 0.10 seconds of game time
  • Conditions
    • (Unit-type of (Ordered unit)) Equal to Coatl
    • (Issued order) Equal to (Order(attack))
    • ((Ordered unit) is alive) Equal to True
  • Actions
    • Item - Pick every item in (Playable map area) and do (Actions)
      • Loop - Actions
        • Set VariableSet Temp_Item = (Picked item)
        • Custom script: Temp_HP = GetWidgetLife(Temp_Item)
        • Custom script: Last_HP = itemHealthTable[Temp_Item] or 0
        • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
          • If - Conditions
            • Last_HP Greater than or equal to 0.00
            • Temp_HP Less than Last_HP
          • Then - Actions
            • Special Effect - Create a special effect at (Position of Temp_Item) using Abilities\Spells\Other\Monsoon\MonsoonBoltTarget.mdl
            • Special Effect - Destroy (Last created special effect)
          • Else - Actions
    • Custom script: itemHealthTable[Temp_Item] = Temp_HP
 
Level 45
Joined
Feb 27, 2007
Messages
5,578
this parts Isn't this supposed to work when items are destroyed by other units as well as certain units?
Changing the item model means it would play that altered death animation no matter what causes the item to die. That includes triggers destroying the item.
And this doesn't show any effect whether you destroy it in one shot or in multiple shots.
As Chaosium explained, the conditions you’ve added to the trigger are erroneous and are preventing the trigger from successfully executing.

What you want to do (“play an effect on an item only when a specific unit attacks the item, but the unit is ranged so it’s possible to cancel the attack and also it has projectile speed so the effect must be timed properly”) is not supported by the editor. There is no event you can use for items taking damage (items and units are different and the unit events/responses will not work if you throw them into a trigger like you did). Since there’s no event there’s also no way to know which unit damaged the item. Since you can’t know which unit damaged the item, you also can’t choose to only show this effect when a particular unit/unit-type does the attack/damage.

There is no solution that will satisfy all of what you want; only some of your requirements may be satisfied. You must use one or multiple of any number of alternative solutions that are inherently flawed. You must decide which parts of your desired outcome/behavior are most important for you to achieve and then move forward with a solution that respects those things.

I have offered a couple such alternative solutions and there are others in this thread. I think none of us really know why you want to do this or what matters most about it so we can’t really tell you what you should do. Can you describe why you want it to work this specific way only for one unit type? Attacking items is a really uncommon thing to build gameplay around in wc3.
 
+1 to the above^, it is hard to find a solution that satisfies everything (at least without a lot of drawbacks).

I'm a bit late to the party, but I was curious if there was an elegant solution to this problem. I tried a couple of things:
  1. Periodically checking item health--but this had a number of caveats: you can't easily detect who attacked the item, two simultaneous attacks may be registered as one event, can't react exactly when the damage was dealt etc.
  2. Adding an invisible dummy beneath the item, and then giving the item attacker a splash damage ability. Sadly, this didn't work because the splash damage abilities (cleaving attack, orb of fire, orb of annihilation) don't deal splash damage to units if you attack an item.
But after more experimentation, I think I found something that works reasonably well and I thought I'd share it. It is somewhat similar to @Uncle 's initial idea (replacing the order), but with some adjustments:
  1. When a unit is issued an order to attack an item, spawn a dummy on top of the item and order the attacker to attack the dummy instead. This way, their attack animations/projectile/behavior is all preserved (as far as the player is concerned).
  2. Detect damage dealt to the dummy, and then apply that damage to the item.
  3. Fire an event with the information from the dummy damage event, and you're good to go. 😎
This works quite well in my testing, and is pretty efficient since the system only needs to monitor things so long as there are any active item-attack orders (instead of having to constantly monitor all the items on the map).


There are some caveats though, and I had to resort to some trickery to make the dummy have no health bar/selection but still be attackable (by adding/removing locust and then adding chaos to transform it). But overall I'd say it would probably work for most maps if you really needed it:
JASS:
library ItemDamageDetection initializer Init

    /**
     * ItemDamageDetection v1.0
     *     by Purgeandfire
     *
     * Description:
     *   A system that allows you to detect when a unit deals damage to an item.
     *   It works by intercepting item attack orders and instead having the attacker
     *   attack an invisible dummy spawned at the item location. When the dummy
     *   takes damage, the item will receive damage and the event will be fired.
     *
     *   This system supports retrieving the damaged item, the attacker, and the
     *   damage amount.
     *
     * Caveats:
     *     - Unlike most dummies, this dummy does not have locust (so we can attack it).
     *       Using a locust/chaos trick, we can still remove its health bar and selectability,
     *       but it will still be treated as a normal unit. So when you're enumerating units
     *       (e.g. EnumUnitsInRange...), beware not to inadvertently kill/remove this unit.
     *     - The locust/chaos trick may still have side-effects, such as still being able to
     *       target the unit in certain conditions. But since the item is on top of it, it isn't
     *       really a problem (at least, I haven't been able to select it/attack it independently).
     *     - This system will issue its own orders, trigger some unit damage events (when the dummy
     *       is attacked). So just beware of your own triggers that react to these events.
     *     - If a unit has a splash-enabled weapon type (e.g. Missile (Splash)) and the "Area of Effect
     *       Targets Allowed" includes "Item", then this system will fire any events for items damaged
     *       by the splash attack. By default, though, no unit has a splash attack that affects items.
     *     - Since the dummy unit is targetable, it may receive buffs/debuffs that allow neutral targets.
     *     - Since the attacker is technically attacking a unit, it will use auto-cast bonus damage
     *       abilities like searing arrows, cold arrows, etc. (normally against items those aren't used) 
     *     - This only detects damage from weapon attacks against items. You could technically detect
     *       it from abilities as well, but that requires more knowledge about the map and its abilities,
     *       so if that is needed, I'd recommend just adding events for those specific spells.
     *
     * Importing:
     *     - From the object editor, copy the "Dummy (Item Damage Detection System)" unit into your map
     *     - Update the TRANSFORM_DUMMY_ID to match the rawcode of the dummy in your map
     *     - From the object editor, copy the "Chaos (Item Damage Detection System)" ability into your map
     *     - Make sure the field, "Data - New Unit Type" is correctly set to the dummy you copied above
     *     - Update the CHAOS_TRANSFORM variable with that ability rawcode
     *
     * API:
     *   function RegisterAnyItemDamageEvent takes code actionCallback returns nothing
     *     - Use this to call a function when an item is damaged.
     *
     *   function GetEventDamagedItem takes nothing returns item
     *     - Event response usable in the callback for `RegisterAnyItemDamageEvent`. 
     *       Returns the item that was damaged.
     *
     *   function GetEventItemAttacker takes nothing returns unit
     *     - Event response usable in the callback for `RegisterAnyItemDamageEvent`. 
     *       Returns the unit that dealt damage to the item.
     *
     *   function GetEventDamageDealtToItem takes nothing returns real
     *     - Event response usable in the callback for `RegisterAnyItemDamageEvent`. 
     *       Returns the amount of damage dealt to the item.
     */

    globals

        /**
         *
         * Configuration
         *
         */        

        /**
         * Controls how often we check if an item has been killed or that the
         * unit is no longer issued an attacking order. Feel free to change this,
         * but generally it won't be too expensive to keep this as-is since the timer
         * only runs whenever there are active item attack orders.
         */
        private constant real SCAN_INTERVAL = 0.03125

        /**
         * When a unit is issued an attack order, the system will monitor the orders
         * for that unit and try to remove it from the system if it is no longer attacking
         * the item (mainly for performance). 
         * 
         * However, there is an edge case where a unit can fire a ranged attack and then move away
         * while the projectile is still mid-air. We still want to detect that the item was
         * damaged once the projectile lands, so instead of removing such units from the system
         * immediately, we add a short grace period to remove them after X seconds.
         *
         * 5 seconds should be sufficient for most maps. But if you have a map where you expect
         * some projectiles to take longer than 5 seconds to reach the target, you may want to
         * increase this. 
         */
        private constant real REMOVAL_GRACE_PERIOD = 5.0

        /**
         * The dummy is spawned using this code (sheep), before adding/removing locust and transforming
         * it. The locust and transform trick is to remove the health bar, but to keep it attackable.
         */
        private constant integer ORIGINAL_DUMMY_ID = 'nshe'

        /**
         * The custom dummy for this system, defined in the object editor.
         * Units issued an order to attack will instead attack this dummy.
         */ 
        private constant integer TRANSFORM_DUMMY_ID = 'h000'

        /**
         * The custom chaos transformation ability for this system, defined in the object editor.
         */
        private constant integer CHAOS_TRANSFORM = 'S000'

        /**
         *
         * Implementation Globals
         *
         */

        private constant integer ORDER_ID_ATTACK = 851983
        private constant integer ORDER_ID_STOP = 851972

        private trigger orderDetectionTrigger = CreateTrigger()
        private trigger unitDamageDetectionTrigger = CreateTrigger()
        private hashtable hash = null
        private timer periodicScan = null

        private trigger eventTrigger = CreateTrigger()
        private item eventItem = null
        private unit eventUnit = null
        private real eventDamage = 0.0
    endglobals

    /**
     *
     * Public API
     *
     */

    function GetEventDamagedItem takes nothing returns item
        return eventItem
    endfunction

    function GetEventItemAttacker takes nothing returns unit
        return eventUnit
    endfunction

    function GetEventDamageDealtToItem takes nothing returns real
        return eventDamage
    endfunction

    function RegisterAnyItemDamageEvent takes code actionCallback returns nothing
        call TriggerAddAction(eventTrigger, actionCallback)
    endfunction

    /**
     *
     * Implementation
     *
     */

    private struct UnitOrderData
        unit orderedUnit
        item targetItem
        integer unitKey
        real lifetime

        thistype next
        thistype prev

        method destroy takes nothing returns nothing
            // stop the unit from attacking the dummy anymore
            if this.lifetime == -1 and GetUnitCurrentOrder(orderedUnit) == ORDER_ID_ATTACK then
                call DisableTrigger(orderDetectionTrigger)
                call IssueImmediateOrderById(orderedUnit, ORDER_ID_STOP)
                call EnableTrigger(orderDetectionTrigger)
            endif

            set this.next.prev = this.prev
            set this.prev.next = this.next

            call FlushChildHashtable(hash, unitKey)
            call this.deallocate()
        endmethod

        method redirectAttackOrderTo takes unit target returns nothing
            call DisableTrigger(orderDetectionTrigger)
            call IssueTargetOrderById(orderedUnit, ORDER_ID_ATTACK, target)
            call EnableTrigger(orderDetectionTrigger)
        endmethod

        method refresh takes unit targetDummy returns nothing
            // called when a unit is re-issued an order to the item
            // we'll just reset any state (e.g. its lifetime)
            set this.lifetime = -1
            call this.redirectAttackOrderTo(targetDummy)
        endmethod
        
        static method create takes unit orderedUnit, item targetItem, unit targetDummy returns thistype
            local thistype this = thistype.allocate()

            set this.orderedUnit = orderedUnit
            set this.targetItem = targetItem
            set this.unitKey = GetHandleId(orderedUnit)
            set this.next = 0
            set this.prev = 0
            set this.lifetime = -1 // -1 represents infinite lifetime

            call SaveInteger(hash, unitKey, 2, this)

            call this.redirectAttackOrderTo(targetDummy)

            return this
        endmethod
    endstruct

    private struct ItemData
        unit targetDummy
        item it
        UnitOrderData orderHead
        integer itemKey

        thistype next
        thistype prev

        method fireDamageEvent takes unit attacker, real damage returns nothing
            local unit tempUnit = eventUnit
            local item tempItem = eventItem
            local real tempDamage = eventDamage

            set eventUnit = attacker
            set eventItem = it
            set eventDamage = damage
            call TriggerExecute(eventTrigger)
            set eventUnit = tempUnit
            set eventItem = tempItem
            set eventDamage = tempDamage

            set tempUnit = null
            set tempItem = null
        endmethod

        method destroy takes nothing returns nothing
            local UnitOrderData currentOrder = orderHead
            local UnitOrderData nextOrder = 0
            loop
                exitwhen currentOrder == 0
                set nextOrder = currentOrder.next
                call currentOrder.destroy()
                set currentOrder = nextOrder
            endloop

            set this.next.prev = this.prev
            set this.prev.next = this.next

            if thistype(0).next == 0 then
                call PauseTimer(periodicScan)
            endif

            call FlushChildHashtable(hash, itemKey)
            call FlushChildHashtable(hash, GetHandleId(targetDummy))
            call RemoveUnit(targetDummy)
            call this.deallocate()
        endmethod

        method removeOrderImmediately takes UnitOrderData orderData returns nothing
            local UnitOrderData currentOrder = orderHead
            local UnitOrderData nextOrder = 0
            loop
                exitwhen currentOrder == 0
                set nextOrder = currentOrder.next

                if currentOrder == orderData then
                    if orderData == orderHead then
                        set orderHead = orderHead.next // assign a new head
                    endif

                    call currentOrder.destroy()
                endif

                set currentOrder = nextOrder
            endloop

            // no more orders remaining
            if orderHead == 0 then
                call this.destroy()
            endif
        endmethod

        method removeOrderWithGracePeriod takes UnitOrderData orderData returns nothing
            if orderData.lifetime < 0 then
                set orderData.lifetime = REMOVAL_GRACE_PERIOD
            endif
        endmethod

        method handleUnitAttackStates takes nothing returns nothing
            local UnitOrderData currentOrder = orderHead
            local UnitOrderData nextOrder = 0
            loop
                exitwhen currentOrder == 0
                set nextOrder = currentOrder.next

                if GetUnitCurrentOrder(currentOrder.orderedUnit) != ORDER_ID_ATTACK then
                    call this.removeOrderWithGracePeriod(currentOrder)                  
                endif

                set currentOrder = nextOrder
            endloop
        endmethod 

        method addOrder takes unit orderedUnit, item targetItem returns nothing
            local UnitOrderData newOrderData = UnitOrderData.create(orderedUnit, targetItem, targetDummy)

            set orderHead.next.prev = newOrderData
            set newOrderData.next = orderHead.next
            set orderHead.next = newOrderData
            set newOrderData.prev = orderHead
        endmethod

        method decrementOrderLifetimes takes nothing returns nothing
            local UnitOrderData currentOrder = orderHead
            local UnitOrderData nextOrder = 0
            loop
                exitwhen currentOrder == 0
                set nextOrder = currentOrder.next

                if currentOrder.lifetime > 0 then
                    set currentOrder.lifetime = currentOrder.lifetime - SCAN_INTERVAL
                    
                    // if the lifetime transitions below 0, then remove it
                    if currentOrder.lifetime <= 0 then
                        call this.removeOrderImmediately(currentOrder)
                    endif
                endif

                set currentOrder = nextOrder
            endloop
        endmethod

        static method scan takes nothing returns nothing
            local thistype currentData = thistype(0).next
            local thistype nextData = 0

            loop
                exitwhen currentData == 0
                set nextData = currentData.next

                // If the item is dead, destroy this instance
                if GetWidgetLife(currentData.it) <= 0 then
                    call currentData.destroy()
                else
                    // Remove any units that are no longer attacking (e.g. if the item died or some weird order interruption happened)
                    call currentData.handleUnitAttackStates()

                    // Update state for any units that are pending removal from the system
                    call currentData.decrementOrderLifetimes()
                endif

                set currentData = nextData
            endloop
        endmethod

        static method create takes unit orderedUnit, item targetItem returns thistype
            local thistype this = thistype.allocate()
            set this.it = targetItem
            set this.itemKey = GetHandleId(targetItem)

            // create a neutral dummy, adding/removing locust and then adding chaos
            // this will remove the health bar but still allow it to be attacked
            set this.targetDummy = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), ORIGINAL_DUMMY_ID, GetItemX(it), GetItemY(it), 0)
            call UnitAddAbility(targetDummy, 'Aloc')
            call UnitRemoveAbility(targetDummy, 'Aloc')
            call UnitAddAbility(targetDummy, CHAOS_TRANSFORM)

            set this.orderHead = UnitOrderData.create(orderedUnit, targetItem, targetDummy)

            if thistype(0).next == 0 then
                call TimerStart(periodicScan, SCAN_INTERVAL, true, function thistype.scan)
            endif

            set thistype(0).next.prev = this
            set this.next = thistype(0).next
            set thistype(0).next = this
            set this.prev = 0

            call SaveInteger(hash, itemKey, 0, this)
            call SaveInteger(hash, GetHandleId(targetDummy), 1, this)
            return this
        endmethod
    endstruct

    private function TrackUnitOrder takes unit orderedUnit, item targetItem returns nothing
        local UnitOrderData orderData = LoadInteger(hash, GetHandleId(orderedUnit), 2)
        local ItemData itemData = LoadInteger(hash, GetHandleId(targetItem), 0)
        local ItemData existingItemData = 0

        // check for any existing orders from this unit
        if orderData != 0 then
            if orderData.targetItem == targetItem then
                // if this order is already tracking this item, just refresh it
                call orderData.refresh(itemData.targetDummy)
                return
            else
                // otherwise remove this order and replace it with the new one
                set existingItemData = LoadInteger(hash, GetHandleId(targetItem), 0)
                if existingItemData != 0 then
                    call existingItemData.removeOrderWithGracePeriod(orderData)
                endif                
            endif
        endif

        if itemData == 0 then
            set itemData = ItemData.create(orderedUnit, targetItem)  
        else
            call itemData.addOrder(orderedUnit, targetItem)
        endif
    endfunction

    private function UntrackUnit takes unit orderedUnit returns nothing
        local UnitOrderData orderData = LoadInteger(hash, GetHandleId(orderedUnit), 2)
        local ItemData itemData = 0
        if orderData == 0 or orderData.targetItem == null then
            return
        endif

        set itemData = LoadInteger(hash, GetHandleId(orderData.targetItem), 0)  
        if itemData == 0 then
            return
        endif

        call itemData.removeOrderWithGracePeriod(orderData)
    endfunction

    private function OnOrder takes nothing returns nothing
        local item targetItem = GetOrderTargetItem()
        if targetItem != null and GetWidgetLife(targetItem) > 0 and GetIssuedOrderId() == ORDER_ID_ATTACK then
            call TrackUnitOrder(GetTriggerUnit(), targetItem)
        else
            call UntrackUnit(GetTriggerUnit())
        endif

        set targetItem = null
    endfunction

    private function OnItemManipulated takes nothing returns nothing
        local ItemData itemData = LoadInteger(hash, GetHandleId(GetManipulatedItem()), 0)
        if itemData != 0 then
            call itemData.destroy()
        endif
    endfunction

    private function OnUnitDamaged takes nothing returns nothing
        local unit damageSource = GetEventDamageSource() 
        local unit damageTarget = BlzGetEventDamageTarget()
        local real damage = GetEventDamage()
        local ItemData itemData = LoadInteger(hash, GetHandleId(damageTarget), 1)
        local UnitOrderData orderData = LoadInteger(hash, GetHandleId(damageSource), 2)

        // negate the damage dealt to the dummy
        call BlzSetEventDamage(0)

        // if order data does not exist for the item or the attacker, ignore this
        if itemData == 0 or orderData == 0 and itemData.it == orderData.targetItem then
            set damageSource = null
            set damageTarget = null
            return
        endif

        // if this is not a normal attack (e.g. orb of venom poison), ignore it
        if BlzGetEventDamageType() != DAMAGE_TYPE_NORMAL then
            set damageSource = null
            set damageTarget = null
            return
        endif

        // redirect the actual damage to the item
        call SetWidgetLife(itemData.it, RMaxBJ(GetWidgetLife(itemData.it) - damage, 0.0))
        call itemData.fireDamageEvent(damageSource, damage)

        set damageSource = null
        set damageTarget = null
    endfunction

    private function Init takes nothing returns nothing
        local trigger itemManipulateTrigger = CreateTrigger()
        set hash = InitHashtable()
        set periodicScan = CreateTimer()

        call TriggerRegisterAnyUnitEventBJ(orderDetectionTrigger, EVENT_PLAYER_UNIT_ISSUED_ORDER)
        call TriggerRegisterAnyUnitEventBJ(orderDetectionTrigger, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER)
        call TriggerRegisterAnyUnitEventBJ(orderDetectionTrigger, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER) 
        call TriggerAddAction(orderDetectionTrigger, function OnOrder)

        call TriggerRegisterAnyUnitEventBJ(itemManipulateTrigger, EVENT_PLAYER_UNIT_USE_ITEM)
        call TriggerRegisterAnyUnitEventBJ(itemManipulateTrigger, EVENT_PLAYER_UNIT_PICKUP_ITEM)
        call TriggerAddAction(itemManipulateTrigger, function OnItemManipulated)

        call TriggerRegisterAnyUnitEventBJ(unitDamageDetectionTrigger, EVENT_PLAYER_UNIT_DAMAGING)
        call TriggerAddAction(unitDamageDetectionTrigger, function OnUnitDamaged)
    endfunction

endlibrary

There are probably some more corner cases I haven't really thought of, but hopefully it helps @Tteodori Usemap or any others if they are trying to find a solution to this. :thumbs_up:
 

Attachments

  • ItemDamageDetectionSample.w3m
    25.2 KB · Views: 2
Top