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