/** This package provides a light-weight damage detection system with priority-ordered listeners. **IMPORTANT** This package **doesn't** provide protection from cyclic damage. Damage from code **must** be declared by the user using DamageEvent.setNextDamageFromCode() before dealing damage. Setting the damage amount to zero or less in the unreduced listeners does **not** skip the reduced listeners: EVENT_PLAYER_UNIT_DAMAGED still fires and they run with an amount of zero. (This corrects an older note here which claimed the opposite. Verified in game by check 10 of package StdlibIngameTests.) if DETECT_NATIVE_ABILITIES is true: > Any damage instance of attacktype ATTACK_TYPE_NORMAL is passed with DamageType SPELL To listen to any damage instance firing: DamageEvent.addListener() -> print(DamageEvent.getSource().getName() + " dealt " + DamageEvent.getAmount().toString() + " damage to " + DamageEvent.getTarget().getName() + ".") If the order of firing of the listeners is important, the user can give a priority to the listener: DamageEvent.addListener(2) -> print("This fires after.") DamageEvent.addListener(0) -> print("This fires before.") If you want to catch the damage instance before it has been reduced by any damage reducing effect (such as armor) you can use the unreduced versions of the listeners: DamageEvent.addUnreducedListener() -> print("this fire before any damage reduction") Each damage instance has a DamageType associated to it. CODE **must** be declared by the user using DamageEvent.setNextDamageFromCode() before dealing damage. ATTACK and SPELL are detected by the system (if DETECT_NATIVE_ABILITIES is false then native abilities will be detected as ATTACK) DamageEvent.addListener() -> switch DamageEvent.getType() case ATTACK print("This damage is from a unit's attack.") case SPELL print("This damage is from a native ability.") case CODE print("This damage is from code.") default Each damage instance can have an id and/or an element.DamageEvent Allowing the user to easily categorize (DamageElement) or/and identify a damage instance (id): DamageEvent.setNextDamageFromCode() DamageEvent.setNextDamageId('A000') // This damage instance is from the spell 'A000' DamageEvent.setNextDamageElement(DAMAGE_ELEMENT_FIRE) // This damage instance will be categorized as a Fire damage instance. UnitDamageTarget(source, target, amount, [...]) Using globally defined priorities for a map can help easily understand and use a damage pipeline: public constant DAMAGE_EVENT_PRIO_START = 0 // Abort damage at this priority public constant DAMAGE_EVENT_PRIO_RELATIVE = 1 // Apply relative changes (multiplication and division) public constant DAMAGE_EVENT_PRIO_ABSOLUTE = 2 // Apply absolute changes (addition and subtraction) public constant DAMAGE_EVENT_PRIO_SHIELD = 4 // Apply reduction from "shield" effects public constant DAMAGE_EVENT_PRIO_FINAL = 5 // Final damage (won't be changed at this stage) */ package DamageEvent import ClosureEvents /* CONFIGURATION */ /** If true, any damage instance of attacktype ATTACK_TYPE_NORMAL is passed with DamageType SPELL */ @configurable constant DETECT_NATIVE_ABILITIES = true /** A damage instance can have an element. DAMAGE_ELEMENT_ATTACK is the default element added to any damage instances of DamageType ATTACK */ @configurable public constant DAMAGE_ELEMENT_ATTACK = new DamageElement("Physical", colorA(223, 59, 33, 255)) /* DAMAGE TYPE */ /** Each damage instance has a DamageType associated to it. ATTACK and SPELL are detected by the system. CODE **must** be declared by the user using DamageEvent.setNextDamageFromCode() before dealing damage */ public enum DamageType ATTACK // From units' attacks or native abilities if DETECT_NATIVE_ABILITIES is false (PHYSICAL) SPELL // From native abilities (NATIVE_ABILITY) CODE // From user code NULLED // For backward compatibility -> has no utility UNKNOWN /* DAMAGE ELEMENT */ /** Each damage instance can have a DamageElement associated to it. Allowing for categories of damage instances: DAMAGE_ELEMENT_FIRE = new DamageElement("Fire", colorA(255, 0, 0, 255)) DAMAGE_ELEMENT_WATER = new DamageElement("Water", colorA(0, 0, 255, 255)) DamageElement can be extended to carry additional per-element settings.*/ public class DamageElement protected string name protected colorA color = COLOR_WHITE construct(string name, colorA color) this.name = name this.color = color function getName() returns string return name function getColor() returns colorA return color /* DAMAGE INSTANCE */ class DamageInstance protected int id protected unit source protected unit target protected real amount protected real originalAmount protected real unreducedAmount protected real unreducedOriginalAmount protected bool unreduced protected attacktype nativeAttackType protected damagetype nativeDamageType protected weapontype nativeWeaponType protected DamageType damageType protected DamageElement damageElement protected static thistype current = null protected static thistype array stack protected static int count = 0 construct(int id, unit source, unit target, real unreducedAmount, attacktype nativeAttackType, damagetype nativeDamageType, weapontype nativeWeaponType, DamageType damageType, DamageElement damageElement) this.id = id this.source = source this.target = target this.amount = unreducedAmount this.originalAmount = unreducedAmount this.unreducedAmount = unreducedAmount this.unreducedOriginalAmount = unreducedAmount this.unreduced = true this.nativeAttackType = nativeAttackType this.nativeDamageType = nativeDamageType this.nativeWeaponType = nativeWeaponType this.damageType = damageType this.damageElement = damageElement count++ stack[count] = this current = this protected function setAmount(real amount) this.amount = amount if unreduced this.unreducedAmount = amount protected function setReducedAmount(real amount) this.amount = amount this.originalAmount = amount this.unreduced = false ondestroy count-- current = stack[count] /* DAMAGE EVENT */ public class DamageEvent // Next damage instance protected static int nextDamageId = 0 protected static DamageType nextDamageType = UNKNOWN protected static DamageElement nextDamageElement = null // Mid damage instance protected static bool abort = false // Damage listeners. Each priority holds a doubly linked list; the tail // pointer keeps same-priority registration order (FIFO) an O(1) append. protected static DamageListener array firstListeners protected static DamageListener array lastListeners protected static int maxPriority = 0 protected static DamageListener array firstUnreducedListeners protected static DamageListener array lastUnreducedListeners protected static int maxUnreducedPriority = 0 /* Cursor of every traversal currently in progress, one slot per nesting level (a listener that deals damage re-enters these loops). A traversal caches the successor of the listener it is about to invoke. If that callback destroys the successor, the cached pointer would dangle, so removeListener repairs every cursor pointing at the node it unlinks. Listeners further down the list need no repair: the cursor is re-read from the live current node on each iteration, and unlinking has already corrected that node's next pointer. */ protected static DamageListener array iterNext protected static int iterDepth = 0 /** Advances any in-flight cursor that is about to step onto this listener. Must run before the listener's own next pointer is cleared. */ protected static function repairCursors(DamageListener removed) for d = 0 to iterDepth - 1 if iterNext[d] == removed iterNext[d] = removed.next /* ON DAMAGE */ protected static function onUnreducedDamage() let amount = GetEventDamage() let attackType = BlzGetEventAttackType() if nextDamageType == DamageType.UNKNOWN// Damage type hasn't been defined -> detect it if DETECT_NATIVE_ABILITIES and attackType == ATTACK_TYPE_NORMAL nextDamageType = SPELL else nextDamageType = ATTACK nextDamageElement = DAMAGE_ELEMENT_ATTACK let dmg = new DamageInstance(nextDamageId, GetEventDamageSource(), GetTriggerUnit(), amount, attackType, BlzGetEventDamageType(), BlzGetEventWeaponType(), nextDamageType, nextDamageElement) nextDamageId = 0 nextDamageType = UNKNOWN nextDamageElement = null let depth = iterDepth iterDepth++ for i = 0 to maxUnreducedPriority var listener = firstUnreducedListeners[i] while listener != null // Publish the successor so removeListener can advance it if the // callback destroys that listener (or this one). iterNext[depth] = listener.next listener.onEvent() if abort dmg.amount = 0 break else listener = iterNext[depth] if abort break iterDepth-- iterNext[depth] = null BlzSetEventAttackType(dmg.nativeAttackType) BlzSetEventDamageType(dmg.nativeDamageType) BlzSetEventWeaponType(dmg.nativeWeaponType) BlzSetEventDamage(dmg.amount) /* Do NOT close the instance out here when the damage ended at <= 0. The package header claims that "if you set damage amount to zero or less in the unreduced listeners, reduced listeners will not fire". That claim is wrong on current game versions: EVENT_PLAYER_UNIT_DAMAGED fires for a zeroed DAMAGING just like any other, and onDamage() below releases the instance normally. Verified in game by check 10 of package StdlibIngameTests. Closing the instance out here made a zeroed *nested* hit pop its own entry, so the following DAMAGED found the enclosing instance as `current` and consumed it with the inner event's damage of 0 - the outer hit then reported 0 damage to its own reduced listeners. Leave the instance on the stack and let its DAMAGED reclaim it. */ protected static function onDamage() let dmg = DamageInstance.current // Every DAMAGED is preceded by a DAMAGING that left its instance on the // stack, including zeroed ones, so `current` is this event's instance. // A null here would mean an unpaired DAMAGED; bail rather than // dereference it. if dmg == null abort = false return if not abort dmg.setReducedAmount(GetEventDamage()) let depth = iterDepth iterDepth++ for i = 0 to maxPriority var listener = firstListeners[i] while listener != null iterNext[depth] = listener.next listener.onEvent() if abort dmg.amount = 0 break else listener = iterNext[depth] if abort break iterDepth-- iterNext[depth] = null BlzSetEventAttackType(dmg.nativeAttackType) BlzSetEventDamageType(dmg.nativeDamageType) BlzSetEventWeaponType(dmg.nativeWeaponType) BlzSetEventDamage(dmg.amount) destroy dmg abort = false /* LISTENERS */ /** Adds a damage event listener at priority 0. If the order of firing is important, use addListener(priority, listener) */ static function addListener(DamageListener listener) returns DamageListener return addListener(0, listener) /** Adds a damage event listener with a given priority. Listeners of different priorities fire from the lowest priority to the highest priority added. Listeners of the same priority fire by order of addition (FIFO) */ static function addListener(int priority, DamageListener listener) returns DamageListener listener.priority = priority listener.unreduced = false listener.registered = true listener.next = null // append at the tail so same-priority listeners fire in registration order if firstListeners[priority] == null listener.prev = null firstListeners[priority] = listener else listener.prev = lastListeners[priority] lastListeners[priority].next = listener lastListeners[priority] = listener if maxPriority < priority maxPriority = priority return listener protected static function removeListener(DamageListener listener) if not listener.registered return // before any pointer on this node is cleared repairCursors(listener) if listener.unreduced removeUnreducedListener(listener) return let prio = listener.priority if listener.prev != null listener.prev.next = listener.next else firstListeners[prio] = listener.next if listener.next != null listener.next.prev = listener.prev else lastListeners[prio] = listener.prev listener.next = null listener.prev = null listener.registered = false if firstListeners[prio] == null and maxPriority == prio var p = prio while p > 0 and firstListeners[p] == null p-- maxPriority = p /** Adds a damage event listener at priority 0 that fires before any damage reduction is applied (such as armor). If the order of firing is important, use addUnreducedListener(priority, listener) */ static function addUnreducedListener(DamageListener listener) returns DamageListener return addUnreducedListener(0, listener) /** Adds a damage event listener with a given priority that fires before any damage reduction is applied (such as armor). Listeners of different priorities fire from the lowest priority to the highest priority added. Listeners of the same priority fire by order of addition (FIFO) */ static function addUnreducedListener(int priority, DamageListener listener) returns DamageListener listener.priority = priority listener.unreduced = true listener.registered = true listener.next = null if firstUnreducedListeners[priority] == null listener.prev = null firstUnreducedListeners[priority] = listener else listener.prev = lastUnreducedListeners[priority] lastUnreducedListeners[priority].next = listener lastUnreducedListeners[priority] = listener if maxUnreducedPriority < priority maxUnreducedPriority = priority return listener protected static function removeUnreducedListener(DamageListener listener) if not listener.registered return // no-op when reached via removeListener, which already repaired repairCursors(listener) let prio = listener.priority if listener.prev != null listener.prev.next = listener.next else firstUnreducedListeners[prio] = listener.next if listener.next != null listener.next.prev = listener.prev else lastUnreducedListeners[prio] = listener.prev listener.next = null listener.prev = null listener.registered = false if firstUnreducedListeners[prio] == null and maxUnreducedPriority == prio var p = prio while p > 0 and firstUnreducedListeners[p] == null p-- maxUnreducedPriority = p /* GETTERS */ /** Returns the id of the damage instance being currently fired */ static function getId() returns int return DamageInstance.current.id /** Returns the source of the damage instance being currently fired */ static function getSource() returns unit return DamageInstance.current.source /** Returns the target of the damage instance being currently fired */ static function getTarget() returns unit return DamageInstance.current.target /** Returns the damage amount of the damage instance being currently fired */ static function getAmount() returns real return DamageInstance.current.amount /** Returns the original reduced damage amount of the damage instance being currently fired */ static function getOriginalAmount() returns real return DamageInstance.current.originalAmount /** Returns the unreduced damage amount of the damage instance being currently fired */ static function getUnreducedAmount() returns real return DamageInstance.current.unreducedAmount /** Returns the original unreduced damage amount of the damage instance being currently fired */ static function getUnreducedOriginalAmount() returns real return DamageInstance.current.unreducedOriginalAmount /** Returns the percent of damage reduced from the *original unreduced amount* by damage reducing effects. If the damage instance is still unreduced, returns zero */ static function getNativeDamageReductionPercent() returns real if DamageInstance.current.unreduced return 0. return 1. - DamageInstance.current.originalAmount / DamageInstance.current.unreducedOriginalAmount /** Returns the attacktype of the damage instance being currently fired */ static function getAttackType() returns attacktype return DamageInstance.current.nativeAttackType /** Returns the damagetype of the damage instance being currently fired */ static function getDamageType() returns damagetype return DamageInstance.current.nativeDamageType /** Returns the weapontype of the damage instance being currently fired */ static function getWeaponType() returns weapontype return DamageInstance.current.nativeWeaponType /** Returns the DamageType of the damage instance being currently fired */ static function getType() returns DamageType return DamageInstance.current.damageType /** Returns the DamageElement of the damage instance being currently fired */ static function getElement() returns DamageElement return DamageInstance.current.damageElement /** Returns true if a damage instance is currently firing */ static function isFiring() returns bool return DamageInstance.count > 0 /* SETTERS */ /** Sets the damage amount of the damage instance being currently fired */ static function setAmount(real amount) DamageInstance.current.setAmount(amount) /** Adds to the damage amount of the damage instance being currently fired */ static function addAmount(real amount) DamageInstance.current.setAmount(DamageInstance.current.amount + amount) /** Subtracts from the damage amount of the damage instance being currently fired */ static function subAmount(real amount) DamageInstance.current.setAmount(DamageInstance.current.amount - amount) /** Sets the attacktype of the damage instance being currently fired */ static function setAttackType(attacktype attackType) DamageInstance.current.nativeAttackType = attackType /** Sets the damagetype of the damage instance being currently fired */ static function setDamageType(damagetype damageType) DamageInstance.current.nativeDamageType = damageType /** Sets the weapontype of the damage instance being currently fired */ static function setWeaponType(weapontype weaponType) DamageInstance.current.nativeWeaponType = weaponType /** Sets the id for the next damage instance */ static function setNextDamageId(int id) nextDamageId = id /** Sets the DamageType to CODE for the next damage instance */ static function setNextDamageFromCode() nextDamageType = DamageType.CODE /** Sets the DamageType for the next damage instance */ static function setNextDamageType(DamageType damageType) nextDamageType = damageType /** Sets the DamageElement for the next damage instance */ static function setNextDamageElement(DamageElement damageElement) nextDamageElement = damageElement /* UTILS */ /** Nulls the damage amount and skip all remaining listeners for the damage instance being currently fired */ static function abortCurrent() abort = true /* LISTENER */ public abstract class DamageListener int priority = 0 thistype prev = null thistype next = null bool unreduced = false /** Guards removeListener against listeners that were never added */ bool registered = false abstract function onEvent() ondestroy DamageEvent.removeListener(this) /* MISC */ /** Returns the name of the given DamageType */ @configurable public function getDamageTypeName(DamageType t) returns string switch t case ATTACK return "Physical" case SPELL return "Ability" case CODE return "Code" default return "Unknown" /** Returns a damage designation given a DamageType and a DamageElement */ @configurable public function getDamageDesignation(DamageType t, DamageElement e) returns string if t != DamageType.UNKNOWN and e != null if t==DamageType.ATTACK and e==DAMAGE_ELEMENT_ATTACK return "Pure Physical Damage" return e.getName() + " " + getDamageTypeName(t) + " Damage" else if t != DamageType.UNKNOWN return getDamageTypeName(t) + " Damage" else if e != null return e.getName() + " Damage" return "Damage" /** Returns the damage designation of the damage instance being currently fired */ function getEventDamageDesignation() returns string return getDamageDesignation(DamageEvent.getType(), DamageEvent.getElement()) /* INIT */ init EventListener.add(EVENT_PLAYER_UNIT_DAMAGING, () -> DamageEvent.onUnreducedDamage()) EventListener.add(EVENT_PLAYER_UNIT_DAMAGED, () -> DamageEvent.onDamage())