- Joined
- Dec 12, 2012
- Messages
- 1,007
Detection of Physical and Spell Damage
> GUI Version available here <
1. Introduction and motivation
I remember the problem of how to detect physical damage when I stopped mapping years ago. Back then there was no real way to achieve this, and so it is still said today. Although I found some real awesome systems for damage detection after starting mapping again, none of them is really able to detect if the damage was physical or dealt by a spell. There have been attempts with the frost-orb ability which causes unfortunately some bugs, especially with the attack ground command. There was also an attempt with a giant dummy unit which gets damaged by splash (so you have to make all physical damage to splash), but the system also had quite much problems and was never really finished.
But I really need such a system for the things that I want to accomplish on my new map and as I want to use all off the standard melee spells, retriggering all spells is no option. But after weeks of thinking, trying, failing and programming, I think I found an universal solution to the problem.
2. Basic Idea
The idea of the system is quite simple. Every unit on the map gets a modified ability of runed bracers with 200% damage reduction from spells. Because the damage reduction is bigger than 100%, all spell damage is negativ. Now by simply checking the sign of the damage, it is possible to identify if the damage was physical of not.
More complicated was to actually deal the damage on the target again, so that the heal of the negative damage gets denied. This is the opposite problem of damage-blocking: The unit has to get damaged first and later gets healed automatically. This was problematic if the damage would be mortal. Anyway I solved it, so the damage gets allways dealt correctly. For more details, see section 5.
3. Implementation
The implementation is quite easy, just follow these steps:
Remarks:
4. Screenshots
Enclosed are two screenshots of the testmap. You can see the damage detection registering the damage events and identifying the correct damage type by coloring the displayed amount of damage either red (physical) or blue (spell).
5. Code
The following code shows the Implementation of the system on my test map. To achieve an after damage event, which is needed here, a 0.0 second timer is used.
In this global unit variable, the damaged unit is saved. Don't write anything into this variable, use it as readonly.
In this global unit variable, the damage source is saved. Don't write anything into this variable, use it as readonly.
In this global real variable, the amount of damage is saved. This amount can be modified by simply setting it to the desired amount that should be applied to the target. Set the amount to 0.0 to block the damage completly. Negative values will result in heal.
In this global integer variable, the damage type of the current damage is saved. Use it to differentiate between physical, spell and code damage. The variable can takes the values PHYSICAL == 0, SPELL == 1 and CODE == 2. Don't write anything into this variable, use it as readonly.
Use this function to allocate attacks from a running damage event. You can use it in exactly the same way as the standard native UnitDamageTarget. The function is designed recursive, so you can allocate as many attacks as you want. Do not use this function outside of a damage event.
Use this function instead of the GetWidgetLife native. It ensures that you get the correct health value, even when damage modifiers are applied. If you don't use damage modifiers at all, you don't need this function.
Use this function instead of the GetUnitState(u, UNIT_STATE_MAX_LIFE) native. It will return the correct value, even when damage modifiers are applied. If you don't use damage modifiers at all, you don't need this function.
Use this function instead of the SetWidgetLife(u, newLife) native if you use damage modifiers in your map. Same rules as for the GetUnitMaxLife and the GetUnitMaxLife functions.
Allows you to add a damage handler function to the system. Compare the OnDamage scope for an example usage.
Allows you to remove a damage handler function from the system dynamically. If you added the same handler function multiple times to the system, this function will remove all of these equal functions.
Have fun and feel free to test/comment!
Greetings,
looking_for help
UPDATE Version 0.9:
- Fixed a bug with the carrion swarm ability
- Updated the documentation due to this
- Replaced some BJs (still have to learn quite much about WE though^^)
- Added a fixed testmap
UPDATE Version 0.9a:
- Fixed bugs with damage tables above 100% spell damage
- Fixed bugs with ethereal units
- Implemented a custom version of Life Drain to fix LifeDrain Bug
- Merged everything into one trigger and to vJASS
- Replaced all BJs with natives
- Updated the documentation
UPDATE Version 0.9.9.1:
- Implemented damage modifiers that allow to block, reduce, increase or invert damage
- Fixed bugs with attacks that share the same instance
- Fixed rounding errors with very high spell damages
- Removed all memory leaks
- Fixed a bug with locust swarm ability
- Added additional support for customized damage tables with spell damage above 100%
- Updated the documentation and added a new testmap
UPDATE Version 1.0.0.0:
- Completly reworked the whole system
- Increased performance dramatically
- Added a GUI Version of the system
- Implemented a function to allocate damage events from already running damage events
- Allocating damage events is implemented recursive, so users can allocate as many events as they want
- Implemented a function to dynamically remove damage handlers from the system
- Implemented a function to get a units life always correct, even when damage modifiers are used
- Implemented a function to get a units max life always correct, even when damage modifiers are used
- Implemented a function to set a units life always correct, even when damage modifiers are used
- Changed the way events are triggered and fixed a bug with this
- Moved from trigger actions to trigger conditions
- Added optional support for the TriggerRefresh library by Nestharus
- Reworked the system API, users can now set the damage amount directly without a function call
- Added documentation for all user-relevant functions
- Narrowed the window for the rescue ability to get applied
- Fixed bugs with negative damage events that share the same instance
- Fixed a bug with mortal spell and physical damage that run in the same instance
- Fixed a bug that could occure with zero damage events
- Proved bug-freeness for parallel damage events by brute forcing through all possibilities
- Updated the documentation and added a new testmap
UPDATE Version 1.0.0.1:
- Fixed a minor bug with allocated attacks
UPDATE Version 1.0.0.2:
- Fixed a minor bug with the in-built bracer item
- Now using a module initializer to fix bugs with damage on map init
UPDATE Version 1.1.0.0:
- Fixed a bug that made functions like GetWidgetLife return invalid values after killing a unit with spell damage
UPDATE Version 1.1.1.0:
- Removed two unused local variables
- Removed optional support for TriggerRefresh
UPDATE to Version 1.2.0.0
- Fixed a bug that sometimes wront target and source units were asigned
- Made all variables private that should be (damageEventTrigger variable now uses
- System variables like target and source are now summarized in one struct and can be accessed by typing
UPDATE to Version 1.3.0.0
- Fixed a bug that revived units were not re-registered by the system under certain circumstances
- Added the possibility to deal damage in an onDamage event before the original damage executes
- Fixed a minor memory leak
> GUI Version available here <
1. Introduction and motivation
I remember the problem of how to detect physical damage when I stopped mapping years ago. Back then there was no real way to achieve this, and so it is still said today. Although I found some real awesome systems for damage detection after starting mapping again, none of them is really able to detect if the damage was physical or dealt by a spell. There have been attempts with the frost-orb ability which causes unfortunately some bugs, especially with the attack ground command. There was also an attempt with a giant dummy unit which gets damaged by splash (so you have to make all physical damage to splash), but the system also had quite much problems and was never really finished.
But I really need such a system for the things that I want to accomplish on my new map and as I want to use all off the standard melee spells, retriggering all spells is no option. But after weeks of thinking, trying, failing and programming, I think I found an universal solution to the problem.
2. Basic Idea
The idea of the system is quite simple. Every unit on the map gets a modified ability of runed bracers with 200% damage reduction from spells. Because the damage reduction is bigger than 100%, all spell damage is negativ. Now by simply checking the sign of the damage, it is possible to identify if the damage was physical of not.
More complicated was to actually deal the damage on the target again, so that the heal of the negative damage gets denied. This is the opposite problem of damage-blocking: The unit has to get damaged first and later gets healed automatically. This was problematic if the damage would be mortal. Anyway I solved it, so the damage gets allways dealt correctly. For more details, see section 5.
3. Implementation
The implementation is quite easy, just follow these steps:
- Copy this trigger to your map. With the AddDamageHandler function you can add as many handlers as you like (compare the OnDamage scope).
- Copy the two custom abilities to your map and make sure they have the correct ID in the globals variable block.
- Go to the locust swarm ability and invert its damage return portion from (default) 0.75 to -0.75.
- Remove the spell damage reduction ability from the spell damage reduction items you use (runed bracers). You can configure the resistance of this item in the globals block, modifying BRACERS_SPELL_DAMAGE_REDUCTION.
Remarks:
- The fact that you deleted the ability of the standard runed bracers does not mean you can't use runed bracers in your map. They are allready considered in the system, so they work just as they do in the normal game. If you want to customize its values, don't change them in the object editor but in the code with the globals variable BRACERS_SPELL_DAMAGE_REDUCTION.
- LifeDrain does not work with this system, as the caster doesn't get healed. So if you want to use LifeDrain in your map, you should use a triggered version of this spell.
- Finger of Death should also be triggered, as units don't explode when killed by this spell.
4. Screenshots
Enclosed are two screenshots of the testmap. You can see the damage detection registering the damage events and identifying the correct damage type by coloring the displayed amount of damage either red (physical) or blue (spell).
5. Code
The following code shows the Implementation of the system on my test map. To achieve an after damage event, which is needed here, a 0.0 second timer is used.
JASS:
library DamageEvent/* v 1.3.0.0
**********************************************************************************
*
* Physical Damage Detection Engine
* --------------------------------
* By looking_for_help aka eey
*
* This system is able to detect, modify and deal damage. It can differentiate
* between physical and spell damage, as well as block, reduce or increase the
* applied damage to a desired value. You can also allocate damage events from
* running damage events.
*
**********************************************************************************
*
* Implementation
* --------------
* 1. Copy this trigger to your map. With the AddDamageHandler function you can
* add as many handlers as you like (compare the OnDamage scope).
* 2. Copy the two custom abilities to your map and make sure they have the
* correct ID in the globals variable block.
* 3. Go to the locust swarm ability and invert its damage return portion
* from (default) 0.75 to -0.75.
* 4. Remove the spell damage reduction ability from the spell damage reduction
* items you use (runed bracers). You can configure the resistance of this
* item in the globals block, modifying BRACERS_SPELL_DAMAGE_REDUCTION.
*
**********************************************************************************
*
* Important Notes
* ---------------
* 1. Life Drain does not work with this system, so you should use a triggered
* version of this spell if you want to use it.
* 2. Same for Finger of Death, if you want to use this spell bug free with this
* system, you should use a triggered version of it.
* 3. If you use damage modifiers by setting the damage amount variable, you have
* to use GetUnitLife, GetUnitMaxLife and SetUnitLife instead of GetWidgetLife,
* GetUnitState for UNIT_STATE_MAX_LIFE and SetWidgetLife in your whole map to
* ensure there occure no bugs.
* 4. The boolean USE_SPELL_RESISTANCE_AUTO_DETECT is only neccessary set to true
* if you want to use a customized damage table with spell damage resistance
* above 100%. If this is not the case, it should be set to false, as the
* system is faster then and still works correct.
* 5. As already mentioned you can't use the spell reduction ability when using this
* system (runed bracers and elunes grace). If you want to use them, you can
* trigger them by using the damage modifiers. Runed bracers is already considered
* in this system, so you don't have to code it.
*
**********************************************************************************
*
* System API
* ----------
* unit PDDS.target
* - In this global unit variable, the damaged unit is saved. Don't write any-
* thing into this variable, use it as readonly.
*
* unit PDDS.source
* - In this global unit variable, the damage source is saved. Don't write any-
* thing into this variable, use it as readonly.
*
* real PDDS.amount
* - In this global real variable, the amount of damage is saved. This amount
* can be modified by simply setting it to the desired amount that should be
* applied to the target. Set the amount to 0.0 to block the damage completly.
* Negative values will result in heal.
*
* integer PDDS.damageType
* - In this global integer variable, the damage type of the current damage is
* saved. Use it to differentiate between physical, spell and code damage. The
* variable can takes the values PHYSICAL == 0, SPELL == 1 and CODE == 2. Don't
* write anything into this variable, use it as readonly.
*
* function UnitDamageTargetEx takes unit localSource, unit localTarget, real localAmount,
* boolean attack, boolean ranged, attacktype localAttackType,
* damagetype localDamageType, weapontype localWeaponType
* returns boolean
* - Use this function to allocate attacks from a running damage event. You can use
* it in exactly the same way as the standard native UnitDamageTarget. The function
* is designed recursive, so you can allocate as many attacks as you want. Do not
* use this function outside of a damage event.
*
* function GetUnitLife takes unit u returns real
* - Use this function instead of the GetWidgetLife native. It ensures that you
* get the correct health value, even when damage modifiers are applied. If
* you don't use damage modifiers at all, you don't need this function.
*
* function GetUnitMaxLife takes unit u returns real
* - Use this function instead of the GetUnitState(u, UNIT_STATE_MAX_LIFE) native.
* It will return the correct value, even when damage modifiers are applied. If
* you don't use damage modifiers at all, you don't need this function.
*
* function SetUnitLife takes unit u, real newLife returns nothing
* - Use this function instead of the SetWidgetLife(u, newLife) native if you use
* damage modifiers in your map. Same rules as for the GetUnitMaxLife and the
* GetUnitMaxLife functions.
*
* function AddDamageHandler takes code damageHandler returns nothing
* - Allows you to add a damage handler function to the system. Compare the
* OnDamage scope for an example usage.
*
* function RemoveDamageHandler takes code damageHandler returns nothing
* - Allows you to remove a damage handler function from the system dynamic-
* ally. If you added the same handler function multiple times to the sys-
* tem, this function will remove all of these equal functions.
*
*********************************************************************************/
globals
/*************************************************************************
* Customizable globals
*************************************************************************/
private constant integer DAMAGE_TYPE_DETECTOR = 'A000'
private constant integer SET_MAX_LIFE = 'A001'
private constant integer SPELL_DAMAGE_REDUCTION_ITEM = 'brac'
private constant boolean USE_SPELL_RESISTANCE_AUTO_DETECT = false
private constant real ETHEREAL_DAMAGE_FACTOR = 1.66
private constant real BRACERS_SPELL_DAMAGE_REDUCTION = 0.33
private constant real TRIGGER_CLEANUP_PERIOD = 10.0
/*************************************************************************
* End of Customizable globals
*************************************************************************/
constant integer PHYSICAL = 0
constant integer SPELL = 1
constant integer CODE = 2
constant real UNIT_MIN_LIFE = 0.406
private constant attacktype ATTACK_TYPE_UNIVERSAL = ConvertAttackType(7)
private hashtable h
private real pureAmount
private trigger damageEvent
private trigger damageHandler
private trigger runAllocatedAttacks
private integer allocatedAttacks
private integer totalAllocs
private integer allocCounter
private real damageEventTrigger
endglobals
struct PDDS extends array
static unit source
static unit target
static real amount
static integer damageType
endstruct
/******************************************************************************
* User functions
******************************************************************************/
function AddDamageHandler takes code func returns nothing
local integer id = GetHandleId(Condition(func))
local integer index = 0
// Loop to manage equal damage handlers
loop
exitwhen ( LoadTriggerConditionHandle(h, id, index) == null )
set index = index + 1
endloop
// Store the desired damage handler function
call SaveTriggerConditionHandle(h, id, index, TriggerAddCondition(damageHandler, Filter(func)))
endfunction
function RemoveDamageHandler takes code func returns nothing
local integer id = GetHandleId(Condition(func))
local integer index = 0
// Loop through all equal damage handlers
loop
exitwhen ( LoadTriggerConditionHandle(h, id, index) == null )
call TriggerRemoveCondition(damageHandler, LoadTriggerConditionHandle(h, id, index))
set index = index + 1
endloop
// Clean things up
call FlushChildHashtable(h, id)
endfunction
function GetUnitLife takes unit u returns real
local boolean duringModification
local integer uId = GetHandleId(u)
local real health
local real storedHealth = LoadReal(h, uId, 2)
local real storedDamage = LoadReal(h, uId, 1)
// Check if the unit is being rescued from damage
set duringModification = GetUnitAbilityLevel(u, SET_MAX_LIFE) > 0
if duringModification then
call UnitRemoveAbility(u, SET_MAX_LIFE)
endif
// Get the correct health value of the unit
if storedHealth != 0.0 then
set health = storedHealth - storedDamage
else
set health = GetWidgetLife(u) - storedDamage
endif
// Restore the rescue ability and return
if duringModification then
call UnitAddAbility(u, SET_MAX_LIFE)
endif
return health
endfunction
function GetUnitMaxLife takes unit u returns real
local real maxHealth
// Check if the unit is being rescued from damage
if GetUnitAbilityLevel(u, SET_MAX_LIFE) > 0 then
call UnitRemoveAbility(u, SET_MAX_LIFE)
set maxHealth = GetUnitState(u, UNIT_STATE_MAX_LIFE)
call UnitAddAbility(u, SET_MAX_LIFE)
return maxHealth
endif
// If the unit isn't being rescued, use the standard native
return GetUnitState(u, UNIT_STATE_MAX_LIFE)
endfunction
function SetUnitLife takes unit u, real newLife returns nothing
local integer targetId
local integer oldTimerId
local real oldHealth
// Check if the unit is being rescued from damage
if GetUnitAbilityLevel(u, SET_MAX_LIFE) > 0 then
call UnitRemoveAbility(u, SET_MAX_LIFE)
call SetWidgetLife(u, newLife)
call UnitAddAbility(u, SET_MAX_LIFE)
// Get the unit specific timer information
set targetId = GetHandleId(u)
set oldHealth = LoadReal(h, targetId, 0)
// Update the unit specific timer information
if oldHealth != 0.0 then
set oldTimerId = LoadInteger(h, targetId, 3)
call SaveReal(h, targetId, 2, newLife)
call SaveReal(h, targetId, 0, newLife)
call SaveReal(h, oldTimerId, 4, newLife)
endif
return
endif
// If the unit isn't being rescued, use the standard native
call SetWidgetLife(u, newLife)
endfunction
function UnitDamageTargetEx takes unit localSource, unit localTarget, real localAmount, boolean attack, /*
*/boolean ranged, attacktype localAttackType, damagetype localDamageType, /*
*/weapontype localWeaponType returns boolean
// Avoid infinite loop due to recursion
if PDDS.damageType == CODE then
return false
endif
// Avoid allocating attacks on units that are about to be killed
if ( localTarget == PDDS.target and GetUnitLife(localTarget) - PDDS.amount < UNIT_MIN_LIFE ) then
return false
endif
// Store all damage parameters determined by the user
set allocatedAttacks = allocatedAttacks + 1
call SaveUnitHandle(h, allocatedAttacks, 0, localSource)
call SaveUnitHandle(h, allocatedAttacks, 1, localTarget)
call SaveReal(h, allocatedAttacks, 2, localAmount)
call SaveBoolean(h, allocatedAttacks, 3, attack)
call SaveBoolean(h, allocatedAttacks, 4, ranged)
call SaveInteger(h, allocatedAttacks, 5, GetHandleId(localAttackType))
call SaveInteger(h, allocatedAttacks, 6, GetHandleId(localDamageType))
call SaveInteger(h, allocatedAttacks, 7, GetHandleId(localWeaponType))
// Return true if the damage was allocated
return true
endfunction
/******************************************************************************
* Sub functions
******************************************************************************/
private function DealFixDamage takes unit source, unit target, real pureAmount returns nothing
local real MAX_DAMAGE = 1000000.0
local real beforeHitpoints
local real newHitpoints
// Ensure the amount is positive
if pureAmount < 0 then
set pureAmount = -pureAmount
endif
// Save the targets hitpoints
set beforeHitpoints = GetWidgetLife(target)
set newHitpoints = beforeHitpoints - pureAmount
// Apply the desired, fixed amount
if newHitpoints >= UNIT_MIN_LIFE then
call SetUnitState(target, UNIT_STATE_LIFE, newHitpoints)
else
if ( IsUnitType(target, UNIT_TYPE_ETHEREAL) == false ) then
call SetWidgetLife(target, 1.0)
call UnitDamageTarget(source, target, MAX_DAMAGE, true, false, ATTACK_TYPE_UNIVERSAL, DAMAGE_TYPE_UNIVERSAL, null)
else
call UnitRemoveAbility(target, DAMAGE_TYPE_DETECTOR)
call SetWidgetLife(target, 1.0)
call UnitDamageTarget(source, target, MAX_DAMAGE, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_UNIVERSAL, null)
endif
endif
endfunction
private function GetUnitSpellResistance takes unit u returns real
local real originalHP
local real beforeHP
local real afterHP
local real resistance
local real DUMMY_DAMAGE = 100
local real DUMMY_FACTOR = 0.01
// Deal spell damage in order to get the units resistance
call UnitRemoveAbility(u, DAMAGE_TYPE_DETECTOR)
set originalHP = GetWidgetLife(u)
call UnitAddAbility(u, SET_MAX_LIFE)
call SetWidgetLife(u, 20000.0)
set beforeHP = GetWidgetLife(u)
call DisableTrigger(damageEvent)
call UnitDamageTarget(PDDS.source, u, DUMMY_DAMAGE, true, false, null, DAMAGE_TYPE_UNIVERSAL, null)
call EnableTrigger(damageEvent)
set afterHP = GetWidgetLife(u)
call UnitRemoveAbility(u, SET_MAX_LIFE)
call SetWidgetLife(u, originalHP)
call UnitAddAbility(u, DAMAGE_TYPE_DETECTOR)
call UnitMakeAbilityPermanent(u, true, DAMAGE_TYPE_DETECTOR)
// Calculate this resistance
set resistance = DUMMY_FACTOR*(beforeHP - afterHP)
// If the resistance was greater than 100%, return it
if resistance > 1.0 then
return resistance
else
return 1.0
endif
endfunction
private function UnitHasItemOfType takes unit u, integer itemId returns integer
local integer index = 0
local item indexItem
// Check if the target has a spell damage reducing item
loop
set indexItem = UnitItemInSlot(u, index)
if ( indexItem != null ) and ( GetItemTypeId(indexItem) == itemId ) then
set indexItem = null
return index + 1
endif
set index = index + 1
exitwhen index >= bj_MAX_INVENTORY
endloop
set indexItem = null
return 0
endfunction
/******************************************************************************
* Damage Engine
******************************************************************************/
private function AfterDamage takes nothing returns nothing
local timer time = GetExpiredTimer()
local integer id = GetHandleId(time)
local unit localSource = LoadUnitHandle(h, id, 0)
local unit localTarget = LoadUnitHandle(h, id, 1)
local real amount = LoadReal(h, id, 3)
local real originalHealth = LoadReal(h, id, 4)
// If the damage was modified, restore units health
if originalHealth != 0.0 then
call UnitRemoveAbility(localTarget, SET_MAX_LIFE)
call SetWidgetLife(localTarget, originalHealth)
endif
// Apply the desired amount of damage
if amount > 0.0 then
call DisableTrigger(damageEvent)
call DealFixDamage(localSource, localTarget, amount)
call EnableTrigger(damageEvent)
else
call SetWidgetLife(localTarget, originalHealth - amount)
endif
// Clean things up
call FlushChildHashtable(h, id)
call FlushChildHashtable(h, GetHandleId(localTarget))
call DestroyTimer(time)
set time = null
set localSource = null
set localTarget = null
endfunction
private function DamageEngine takes nothing returns nothing
local timer time
local integer id
local real rawAmount
local real originalHealth
local integer targetId
local integer oldTimerId
local real oldHealth
local real oldOriginalHealth
local real oldAmount
// Set damage variables
set rawAmount = GetEventDamage()
if rawAmount == 0.0 then
return
endif
set PDDS.source = GetEventDamageSource()
set PDDS.target = GetTriggerUnit()
// Determine the damage type
if rawAmount > 0.0 then
if PDDS.damageType != CODE then
set PDDS.damageType = PHYSICAL
endif
set PDDS.amount = rawAmount
else
if PDDS.damageType != CODE then
set PDDS.damageType = SPELL
endif
set PDDS.amount = -rawAmount
endif
// Register spell reduction above 100%
static if USE_SPELL_RESISTANCE_AUTO_DETECT then
set PDDS.amount = GetUnitSpellResistance(PDDS.target)*PDDS.amount
else
if ( IsUnitType(PDDS.target, UNIT_TYPE_ETHEREAL) and IsUnitEnemy(PDDS.target, GetOwningPlayer(PDDS.source)) and rawAmount < 0.0 ) then
set PDDS.amount = ETHEREAL_DAMAGE_FACTOR*PDDS.amount
endif
endif
// Register spell damage reducing items like runed bracers
if ( IsUnitType(PDDS.target, UNIT_TYPE_HERO) and UnitHasItemOfType(PDDS.target, SPELL_DAMAGE_REDUCTION_ITEM) > 0 ) and rawAmount < 0.0 then
set PDDS.amount = (1 - BRACERS_SPELL_DAMAGE_REDUCTION)*PDDS.amount
endif
// Save health and damage variables
if PDDS.damageType != CODE then
call UnitRemoveAbility(PDDS.target, SET_MAX_LIFE)
endif
set pureAmount = PDDS.amount
set originalHealth = GetWidgetLife(PDDS.target)
set oldTimerId = 0
// Call damage handlers
set damageEventTrigger = 0.0
set damageEventTrigger = 1.0
set damageEventTrigger = 0.0
// If the damage was modified, apply the rescue ability
if PDDS.amount != pureAmount then
call UnitAddAbility(PDDS.target, SET_MAX_LIFE)
call SetWidgetLife(PDDS.target, GetWidgetLife(PDDS.target) + pureAmount)
endif
// Check if a previous timer didn't yet expire
set targetId = GetHandleId(PDDS.target)
set oldHealth = LoadReal(h, targetId, 0)
// If this is the case, update the timer information
if oldHealth != 0.0 then
set oldTimerId = LoadInteger(h, targetId, 3)
set oldOriginalHealth = LoadReal(h, targetId, 2)
set oldAmount = LoadReal(h, targetId, 1)
set originalHealth = oldOriginalHealth - oldAmount
call SaveReal(h, oldTimerId, 4, oldOriginalHealth)
endif
// Call after damage event if damage was spell, modified, code or parallel
if ( rawAmount < 0.0 or pureAmount != PDDS.amount or oldTimerId != 0 or allocatedAttacks > 0 ) then
set time = CreateTimer()
set id = GetHandleId(time)
// Save handles for after damage event
call SaveUnitHandle(h, id, 0, PDDS.source)
call SaveUnitHandle(h, id, 1, PDDS.target)
call SaveReal(h, id, 2, pureAmount)
call SaveReal(h, id, 3, PDDS.amount)
call SaveReal(h, id, 4, originalHealth)
// Save this extra to manage parallel damage instances
call SaveReal(h, targetId, 0, GetWidgetLife(PDDS.target))
call SaveReal(h, targetId, 1, PDDS.amount)
call SaveReal(h, targetId, 2, originalHealth)
call SaveInteger(h, targetId, 3, id)
// Avoid healing of negative spell damage
if rawAmount < 0.0 then
call DisableTrigger(damageEvent)
call DealFixDamage(PDDS.source, PDDS.target, -rawAmount)
if ( originalHealth - PDDS.amount < UNIT_MIN_LIFE ) then
call UnitRemoveAbility(PDDS.target, SET_MAX_LIFE)
call DealFixDamage(PDDS.source, PDDS.target, PDDS.amount)
endif
call EnableTrigger(damageEvent)
endif
// Guarantee unit exploding
if originalHealth - PDDS.amount < UNIT_MIN_LIFE then
if rawAmount > 0.0 then
call UnitRemoveAbility(PDDS.target, SET_MAX_LIFE)
call SetWidgetLife(PDDS.target, UNIT_MIN_LIFE)
endif
endif
// Start the after damage event
call TimerStart(time, 0.0, false, function AfterDamage)
endif
// Handle allocated attacks from UnitDamageTargetEx
if totalAllocs == 0 then
set totalAllocs = allocatedAttacks
endif
if allocatedAttacks > 0 then
set allocatedAttacks = allocatedAttacks - 1
set allocCounter = allocCounter + 1
call TriggerEvaluate(runAllocatedAttacks)
endif
// Reset all required variables
set totalAllocs = 0
set allocCounter = -1
set PDDS.damageType = -1
endfunction
/******************************************************************************
* Initialization
******************************************************************************/
private function RestoreTriggers takes nothing returns nothing
local unit enumUnit = GetEnumUnit()
// Re-register units that are alive
if GetUnitTypeId(enumUnit) != 0 then
call TriggerRegisterUnitEvent(damageEvent, enumUnit, EVENT_UNIT_DAMAGED)
endif
set enumUnit = null
endfunction
private function ClearMemory_Actions takes nothing returns nothing
local group g = CreateGroup()
local code c = function DamageEngine
// Reset the damage event
call GroupEnumUnitsInRect(g, GetWorldBounds(), null)
call ResetTrigger(damageEvent)
call DestroyTrigger(damageEvent)
set damageEvent = null
// Rebuild it then
set damageEvent = CreateTrigger()
call TriggerAddCondition(damageEvent, Filter(c))
call ForGroup(g, function RestoreTriggers)
// Clean things up
call DestroyGroup(g)
set g = null
set c = null
endfunction
private function MapInit takes nothing returns nothing
local unit enumUnit = GetEnumUnit()
// Register units on map initialization
call UnitAddAbility(enumUnit, DAMAGE_TYPE_DETECTOR)
call UnitMakeAbilityPermanent(enumUnit, true, DAMAGE_TYPE_DETECTOR)
call TriggerRegisterUnitEvent(damageEvent, enumUnit, EVENT_UNIT_DAMAGED)
set enumUnit = null
endfunction
private function UnitEntersMap takes nothing returns nothing
local unit triggerUnit = GetTriggerUnit()
// Register units that enter the map
if ( GetUnitAbilityLevel(triggerUnit, DAMAGE_TYPE_DETECTOR) < 1 ) then
call UnitAddAbility(triggerUnit, DAMAGE_TYPE_DETECTOR)
call UnitMakeAbilityPermanent(triggerUnit, true, DAMAGE_TYPE_DETECTOR)
call TriggerRegisterUnitEvent(damageEvent, triggerUnit, EVENT_UNIT_DAMAGED)
endif
set triggerUnit = null
endfunction
private function RunAllocatedAttacks takes nothing returns nothing
local integer localAllocAttacks = allocatedAttacks + 1
// Calculate the correct sequence of allocated attacks
set localAllocAttacks = localAllocAttacks - totalAllocs + 1 + 2*allocCounter
// Deal code damage if the unit isn't exploding
set PDDS.damageType = CODE
if GetUnitLife(LoadUnitHandle(h, localAllocAttacks, 1)) >= UNIT_MIN_LIFE then
call UnitDamageTarget(LoadUnitHandle(h, localAllocAttacks, 0), LoadUnitHandle(h, localAllocAttacks, 1), /*
*/LoadReal(h, localAllocAttacks, 2), LoadBoolean(h, localAllocAttacks, 3), /*
*/LoadBoolean(h, localAllocAttacks, 4), ConvertAttackType(LoadInteger(h, /*
*/localAllocAttacks, 5)), ConvertDamageType(LoadInteger(h, localAllocAttacks, 6)), /*
*/ConvertWeaponType(LoadInteger(h, localAllocAttacks, 7)))
else
call FlushChildHashtable(h, localAllocAttacks - 1)
endif
// Clean things up
call FlushChildHashtable(h, localAllocAttacks)
endfunction
private module Inits
private static method onInit takes nothing returns nothing
local group g = CreateGroup()
local region r = CreateRegion()
local trigger UnitEnters = CreateTrigger()
local trigger ClearMemory = CreateTrigger()
local code cDamageEngine = function DamageEngine
local code cUnitEnters = function UnitEntersMap
local code cClearMemory = function ClearMemory_Actions
local code cRunAllocatedAttacks = function RunAllocatedAttacks
// Initialize variables
set h = InitHashtable()
set damageEvent = CreateTrigger()
set damageHandler = CreateTrigger()
set PDDS.damageType = -1
set allocatedAttacks = 0
set runAllocatedAttacks = CreateTrigger()
set totalAllocs = 0
set allocCounter = -1
// Register units on map initialization
call TriggerRegisterVariableEvent(damageHandler, SCOPE_PRIVATE + "damageEventTrigger", EQUAL, 1.0)
call TriggerAddCondition(damageEvent, Filter(cDamageEngine))
call GroupEnumUnitsInRect(g, GetWorldBounds(), null)
call ForGroup(g, function MapInit)
// Register units that enter the map
call RegionAddRect(r, GetWorldBounds())
call TriggerRegisterEnterRegion(UnitEnters, r, null)
call TriggerAddCondition(UnitEnters, Filter(cUnitEnters))
// Register trigger for allocated attacks
call TriggerAddCondition(runAllocatedAttacks, Filter(cRunAllocatedAttacks))
// Clear memory leaks
call TriggerRegisterTimerEvent(ClearMemory, TRIGGER_CLEANUP_PERIOD, true)
call TriggerAddCondition(ClearMemory, Filter(cClearMemory))
// Clean things up
call DestroyGroup(g)
set UnitEnters = null
set ClearMemory = null
set cDamageEngine = null
set cUnitEnters = null
set cClearMemory = null
set cRunAllocatedAttacks = null
set g = null
set r = null
endmethod
endmodule
private struct Init extends array
implement Inits
endstruct
endlibrary
unit PDDS.target
In this global unit variable, the damaged unit is saved. Don't write anything into this variable, use it as readonly.
unit PDDS.source
In this global unit variable, the damage source is saved. Don't write anything into this variable, use it as readonly.
real PDDS.amount
In this global real variable, the amount of damage is saved. This amount can be modified by simply setting it to the desired amount that should be applied to the target. Set the amount to 0.0 to block the damage completly. Negative values will result in heal.
integer PDDS.damageType
In this global integer variable, the damage type of the current damage is saved. Use it to differentiate between physical, spell and code damage. The variable can takes the values PHYSICAL == 0, SPELL == 1 and CODE == 2. Don't write anything into this variable, use it as readonly.
function UnitDamageTargetEx takes unit localSource, unit localTarget, real localAmount, boolean attack, boolean ranged, attacktype localAttackType, damagetype localDamageType, weapontype localWeaponType returns boolean
Use this function to allocate attacks from a running damage event. You can use it in exactly the same way as the standard native UnitDamageTarget. The function is designed recursive, so you can allocate as many attacks as you want. Do not use this function outside of a damage event.
function GetUnitLife takes unit u returns real
Use this function instead of the GetWidgetLife native. It ensures that you get the correct health value, even when damage modifiers are applied. If you don't use damage modifiers at all, you don't need this function.
function GetUnitMaxLife takes unit u returns real
Use this function instead of the GetUnitState(u, UNIT_STATE_MAX_LIFE) native. It will return the correct value, even when damage modifiers are applied. If you don't use damage modifiers at all, you don't need this function.
function SetUnitLife takes unit u, real newLife returns nothing
Use this function instead of the SetWidgetLife(u, newLife) native if you use damage modifiers in your map. Same rules as for the GetUnitMaxLife and the GetUnitMaxLife functions.
function AddDamageHandler takes code damageHandler returns nothing
Allows you to add a damage handler function to the system. Compare the OnDamage scope for an example usage.
function RemoveDamageHandler takes code damageHandler returns nothing
Allows you to remove a damage handler function from the system dynamically. If you added the same handler function multiple times to the system, this function will remove all of these equal functions.
Have fun and feel free to test/comment!
Greetings,
looking_for help
UPDATE Version 0.9:
- Fixed a bug with the carrion swarm ability
- Updated the documentation due to this
- Replaced some BJs (still have to learn quite much about WE though^^)
- Added a fixed testmap
UPDATE Version 0.9a:
- Fixed bugs with damage tables above 100% spell damage
- Fixed bugs with ethereal units
- Implemented a custom version of Life Drain to fix LifeDrain Bug
- Merged everything into one trigger and to vJASS
- Replaced all BJs with natives
- Updated the documentation
UPDATE Version 0.9.9.1:
- Implemented damage modifiers that allow to block, reduce, increase or invert damage
- Fixed bugs with attacks that share the same instance
- Fixed rounding errors with very high spell damages
- Removed all memory leaks
- Fixed a bug with locust swarm ability
- Added additional support for customized damage tables with spell damage above 100%
- Updated the documentation and added a new testmap
UPDATE Version 1.0.0.0:
- Completly reworked the whole system
- Increased performance dramatically
- Added a GUI Version of the system
- Implemented a function to allocate damage events from already running damage events
- Allocating damage events is implemented recursive, so users can allocate as many events as they want
- Implemented a function to dynamically remove damage handlers from the system
- Implemented a function to get a units life always correct, even when damage modifiers are used
- Implemented a function to get a units max life always correct, even when damage modifiers are used
- Implemented a function to set a units life always correct, even when damage modifiers are used
- Changed the way events are triggered and fixed a bug with this
- Moved from trigger actions to trigger conditions
- Added optional support for the TriggerRefresh library by Nestharus
- Reworked the system API, users can now set the damage amount directly without a function call
- Added documentation for all user-relevant functions
- Narrowed the window for the rescue ability to get applied
- Fixed bugs with negative damage events that share the same instance
- Fixed a bug with mortal spell and physical damage that run in the same instance
- Fixed a bug that could occure with zero damage events
- Proved bug-freeness for parallel damage events by brute forcing through all possibilities
- Updated the documentation and added a new testmap
UPDATE Version 1.0.0.1:
- Fixed a minor bug with allocated attacks
UPDATE Version 1.0.0.2:
- Fixed a minor bug with the in-built bracer item
- Now using a module initializer to fix bugs with damage on map init
UPDATE Version 1.1.0.0:
- Fixed a bug that made functions like GetWidgetLife return invalid values after killing a unit with spell damage
UPDATE Version 1.1.1.0:
- Removed two unused local variables
- Removed optional support for TriggerRefresh
UPDATE to Version 1.2.0.0
- Fixed a bug that sometimes wront target and source units were asigned
- Made all variables private that should be (damageEventTrigger variable now uses
SCOPE_PRIVATE
for that).- System variables like target and source are now summarized in one struct and can be accessed by typing
PDDS.target
or PDDS.amount
to avoid naming conflicts.UPDATE to Version 1.3.0.0
- Fixed a bug that revived units were not re-registered by the system under certain circumstances
- Added the possibility to deal damage in an onDamage event before the original damage executes
- Fixed a minor memory leak
Attachments
Last edited: