• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • 🏆 Hive's 6th HD Modeling Contest: Mechanical is now open! Design and model a mechanical creature, mechanized animal, a futuristic robotic being, or anything else your imagination can tinker with! 📅 Submissions close on June 30, 2024. Don't miss this opportunity to let your creativity shine! Enter now and show us your mechanical masterpiece! 🔗 Click here to enter!

[vJASS] What's the bug cause?

Status
Not open for further replies.
Level 17
Joined
Feb 11, 2011
Messages
1,860
I have a spell that gives a chance to block damage done. Then, if the attacker is non-hero, it returns the damage amount (with the use of a missile). However, this missile sometimes bugs. It randomly gets 'attached' to the target's head and then gets destroyed a few seconds later. What's wrong?

JASS:
scope HeavyDefences initializer onInit

    globals
        private constant integer SPELL_ID = 'A004'
        private constant integer MISSILE_ID = 'h00H'
        private constant real MISSILE_SPEED = 1000 * 0.03
    endglobals

    public struct Missile
    
        unit caster = null
        unit missile = null
        unit target = null
        real angle = 0
        real damage = 0
        real distance = 0
        
        method destroy takes nothing returns nothing
            set .missile = null
            set .target = null
            set .caster = null
            call .deallocate()
        endmethod
        
        private static method Timer_Actions takes nothing returns nothing
            local timer t = GetExpiredTimer()
            local thistype this = GetTimerData(t)
            local real dx = GetUnitX(.missile) - GetUnitX(.target)
            local real dy = GetUnitY(.missile) - GetUnitY(.target)
            local real x
            local real y
            set .distance = dx * dx + dy * dy
            if .distance > (2 * MISSILE_SPEED) and not IsUnitType(.target, UNIT_TYPE_DEAD) then
                set .angle = bj_RADTODEG * (Atan2(GetUnitY(.target) - GetUnitY(.missile), GetUnitX(.target) - GetUnitX(.missile)))
                set x = GetUnitX(.missile) + MISSILE_SPEED * Cos(bj_DEGTORAD * .angle)
                set y = GetUnitY(.missile) + MISSILE_SPEED * Sin(bj_DEGTORAD * .angle)
                call SetUnitX(.missile, x)
                call SetUnitY(.missile, y)
                call SetUnitFacing(.missile, .angle)
            elseif IsUnitType(.target, UNIT_TYPE_DEAD) then
                call RemoveUnit(.missile)
                call ReleaseTimer(t)
                call .destroy()
            endif
            if .distance <= (2 * MISSILE_SPEED) then
                call UnitDamageTarget(.caster, .target, .damage, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, null)
                call RemoveUnit(.missile)
                call ReleaseTimer(t)
                call .destroy()
            endif
            set t = null
        endmethod
        
        public static method Create takes unit target, unit source, real damage returns thistype
            local thistype this = thistype.allocate()
            local timer t = NewTimerEx(this)
            local real x = GetUnitX(source)
            local real y = GetUnitY(source)
            set .target = target
            set .caster = source
            set .damage = damage
            set .angle = bj_RADTODEG * (Atan2(GetUnitY(.target) - y, GetUnitX(.target) - x))
            set .missile = CreateUnit(Player(0), MISSILE_ID, x, y, .angle)
            call TimerStart(t, 0.03, true, function thistype.Timer_Actions)
            set t = null
            set target = null
            set source = null
            return this
        endmethod
    
    endstruct
    
    private function Actions takes nothing returns nothing
        local unit attacker = Damage.source
        local unit damaged = GetTriggerUnit()
        local unit dummy
        if GetUnitAbilityLevel(damaged, SPELL_ID) > 0 and GetRandomInt(1, 1) <= (4 * GetUnitAbilityLevel(damaged, SPELL_ID) + 2) then
            call SetWidgetLife(damaged, GetWidgetLife(damaged) + R2I(Damage.amount))
            call DestroyEffect(AddSpecialEffectTarget("Abilities\\Spells\\Human\\Defend\\DefendCaster.mdl", damaged, "origin"))
            if not IsUnitType(attacker, UNIT_TYPE_HERO) then
                call Missile.Create(attacker, damaged, Damage.amount)
            endif
        endif
        set attacker = null
        set damaged = null
    endfunction

    private function onInit takes nothing returns nothing
        call RegisterDamageById(function Actions, DAMAGE_ID_MELEE)
        call RegisterDamageById(function Actions, DAMAGE_ID_RANGED)
    endfunction

endscope
Notes:
- I am giving it a 100% chance to fire for testing purposes.
- I know this is a very crude damage-blocking spell, but that doesn't matter. The focus is the missile bug.
- Any optimizations welcome!
 
Level 17
Joined
Feb 11, 2011
Messages
1,860
No, I meant 2 * MISSILE_SPEED. Say the missile moves 10 units every 0.03 seconds (333.33 units per second). The missile is therefore destroyed when the distance between it and the target is less than or equal to 20 (2 * 10).

Would it be better to say 10^2?
How would I avoid a double release?
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
The missile arrives when the distance is <= what it moves via interval. But what you saved as "distance" is the square of the distance. So since one side was squared in this inequation, the other needs too.

How to avoid the double release? Well, by eliminating the possibility that both code branches execute, setting/checking a flag for example or nesting it like this:

JASS:
            if IsUnitType(.target, UNIT_TYPE_DEAD) then
                call RemoveUnit(.missile)
                call ReleaseTimer(t)
                call .destroy()
            elseif .distance > (2 * MISSILE_SPEED) then
                set .angle = bj_RADTODEG * (Atan2(GetUnitY(.target) - GetUnitY(.missile), GetUnitX(.target) - GetUnitX(.missile)))
                set x = GetUnitX(.missile) + MISSILE_SPEED * Cos(bj_DEGTORAD * .angle)
                set y = GetUnitY(.missile) + MISSILE_SPEED * Sin(bj_DEGTORAD * .angle)
                call SetUnitX(.missile, x)
                call SetUnitY(.missile, y)
                call SetUnitFacing(.missile, .angle)
            else
                call UnitDamageTarget(.caster, .target, .damage, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, null)
                call RemoveUnit(.missile)
                call ReleaseTimer(t)
                call .destroy()
            endif
 
Level 17
Joined
Feb 11, 2011
Messages
1,860
Sigh, that's what I was missing (square root). Don't know how I forgot that part. Anyway, thanks for your help!

EDIT: How can I fix this problem:

For testing I placed a hero which has 800HP. Then I placed an enemy Necromancer that attacks it. Let's say the enemy does exactly 3 damage. The hero doesn't block the damage - his life goes down to 797 instead of staying at 800. How can I fix this?
 
Last edited:
Level 26
Joined
Aug 18, 2009
Messages
4,097
Thing is SquareRoot is rather slow, so when you just want to compare distances, you usually do it via the squares.

If a <= b, then it has to be a² <= b² too unless there is negativity but that's excluded here (dX*dX=dX² always positive, sum of positive values is also positive).

As for your damage block:
Do I assume right that the event is Unit takes damage? This one triggers before the damage is dealt. So if the unit is capped at 800hp max, increasing it by 3 won't help at all, will still have 800hp and then the damage is applied. If you do not have a full custom damage system, pieces of advice are to add Hardened Skin ability, short-temporarily set the unit invulnerable, deal surplus damage via trigger yourself or also heighten the max hp and take it away again after a null-timer.
 
Level 18
Joined
Jan 21, 2006
Messages
2,552
You could just use my projectiles system. It probably does what you want. Also it prevents this sort of thing by calculating the time it is going to require for the projectile to reach its target based on its given velocity and acceleration.
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
And would be nice to know how these imports work. GetEventDamage already includes modifications like armor, Critical Strike or Hardened Skin. So to determine the actual damage, you would take it off. But I just tested. Hardened Skin does not show effect anymore when added on the damage event. Invulnerability works but cancels orders on that target. So maybe go with the increased life/max life method. If you have the right amount of damage you want to deal, you can easily pass a Hardened Skin/invulnerability with the proper damage type in UnitDamageTarget.
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
That's one of the things you should have anyway unless you do not use units/unit's life at all in your map. See here for example. The max life is changed via the effects of abilities and there is even a bug one can abuse. From PitzerMike's ability guide:

AId1, AItg, AIlf, AImb, AIsi, Aamk, AIsr, AIdd, AIas, AIms, Arel, AIrm, AIcs (Defense, Damage, Life, Mana, Sight Range, Attribute Bonus, Runed Bracers, Defend Item, Attack Speed, Movement Speed, Life Regeneration, Mana Regeneration, Critical Strike Item): These item abilities can also be added as unit abilities to change defense, damage, life, mana, sight range, agility, strength and intelligence, magic resistance, damage deflection and damage reduction, attack speed, movement speed, life regeneration, mana regeneration and critical strike and evasion of a unit. Fortunately the effects of those abilities stack so you only need a few copies of them to get any bonus by using a bitflagging technique. Weaaddar and Blade have shown how it can be done with their BonusMod and AttributeMod systems. All of these abilities also work with negative values, so you can decrease stats with them. For sight range however, negative values only work if the unit doesn't have any ability that detects hidden units. The 'AIdd' ability has the same fields as 'Aegr' (Elunes Grace), namely magic resistance, damage reduction and deflection. Unfortunately its attack speed factor and movement speed factor fields don't work. This ability as well as critical strike, life and mana regeneration and attack and movement speed bonuses didn't stack in older versions, but since the latest patch they finally do. A special bug allows you to to adjust the max mana and life of a unit without a permanent ability on that unit. Simply make a custom AIlf (Life Bonus) or AImb (Mana Bonus) ability and give it more than one level. Now set the bonus for level one to zero and the level two to the exact opposit you want to achieve. That would be -200 if you want to add 200. Now use UnitAddAbility and SetUnitAbilityLevel to add the ability at level two to your target. There's a bug in those abilities that prevents the bonus from being applied correctly. Then immediately remove the ability again using UnitRemoveAbility. This time it will try to undo the -200 bonus (which hadn't been applied due to a bug) and add 200 to the unit. That's it, you've changed the life/mana without having to keep any abilities on the unit.
 
Level 37
Joined
Mar 6, 2006
Messages
9,240
Use item life bonus as the base ability.

Set level 1 data to 0, level 2 to x and level 3 to -x. When you want to add max hp, add the abil, set level to 3, remove abil. To reduce max hp, add abil, set level to 2, remove abil.

When a unit takes damage, add max hp, start a 0.00 timer. When the timer expires, reduce hp.

If this is for multiple units, then check that the timer is not started yet before starting it again. Unit takes damage can fire multiple times before a timer with 0.00 expiration expires, so make sure you use a correct method to handle all units with one timer. Unit group will work, or a stack or whatever.
 
Level 17
Joined
Feb 11, 2011
Messages
1,860
It seems like I have found a solution:

JASS:
scope HeavyDefences initializer onInit

    globals
        private constant integer SPELL_ID = 'A004'
        private constant integer MISSILE_ID = 'h00H'
        private constant real MISSILE_SPEED = 1000 * 0.03
    endglobals

    public struct Missile
    
        unit caster = null
        unit missile = null
        unit target = null
        real angle = 0
        real damage = 0
        real distance = 0
        
        method destroy takes nothing returns nothing
            set .missile = null
            set .target = null
            set .caster = null
            call .deallocate()
        endmethod
        
        private static method Timer_Actions takes nothing returns nothing
            local timer t = GetExpiredTimer()
            local thistype this = GetTimerData(t)
            local real dx = GetUnitX(.missile) - GetUnitX(.target)
            local real dy = GetUnitY(.missile) - GetUnitY(.target)
            local real x
            local real y
            set .distance = SquareRoot(dx * dx + dy * dy)
            if .distance > (2 * MISSILE_SPEED) and not IsUnitType(.target, UNIT_TYPE_DEAD) then
                set .angle = bj_RADTODEG * (Atan2(GetUnitY(.target) - GetUnitY(.missile), GetUnitX(.target) - GetUnitX(.missile)))
                set x = GetUnitX(.missile) + MISSILE_SPEED * Cos(bj_DEGTORAD * .angle)
                set y = GetUnitY(.missile) + MISSILE_SPEED * Sin(bj_DEGTORAD * .angle)
                call SetUnitX(.missile, x)
                call SetUnitY(.missile, y)
                call SetUnitFacing(.missile, .angle)
            else
                if .distance <= (2 * MISSILE_SPEED) then
                    call UnitDamageTarget(.caster, .target, .damage, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, null)
                endif
                call KillUnit(.missile)
                call ReleaseTimer(t)
                call .destroy()
            endif
            set t = null
        endmethod
        
        public static method Create takes unit target, unit source, real damage returns thistype
            local thistype this = thistype.allocate()
            local timer t = NewTimerEx(this)
            local real x = GetUnitX(source)
            local real y = GetUnitY(source)
            set .target = target
            set .caster = source
            set .damage = damage
            set .angle = bj_RADTODEG * (Atan2(GetUnitY(.target) - y, GetUnitX(.target) - x))
            set .missile = CreateUnit(Player(0), MISSILE_ID, x, y, .angle)
            call TimerStart(t, 0.03, true, function thistype.Timer_Actions)
            set t = null
            set target = null
            set source = null
            return this
        endmethod
    
    endstruct
    
    private struct Data
    
        unit caster
        real damage
    
        private method destroy takes nothing returns nothing
            set .caster = null
            call .deallocate()
        endmethod
    
        private static method Timer_Actions takes nothing returns nothing
            local timer t = GetExpiredTimer()
            local thistype this = GetTimerData(t)
            call SetWidgetLife(.caster, GetWidgetLife(.caster) + .damage)
            call ReleaseTimer(t)
            call .destroy()
            set t = null
        endmethod
    
        private static method Actions takes nothing returns nothing
            local thistype this = thistype.allocate()
            local timer t = NewTimerEx(this)
            local unit attacker = Damage.source
            local unit damaged = GetTriggerUnit()
            local unit dummy
            set .caster = damaged
            set .damage = Damage.amount
            if GetUnitAbilityLevel(damaged, SPELL_ID) > 0 and GetRandomInt(1, 1) <= (4 * GetUnitAbilityLevel(damaged, SPELL_ID) + 2) then
                call TimerStart(t, 0., false, function thistype.Timer_Actions)
                if not IsUnitType(attacker, UNIT_TYPE_HERO) then
                    call Missile.Create(attacker, damaged, Damage.amount)
                endif
            endif
            set attacker = null
            set damaged = null
            set t = null
        endmethod

        private static method onInit takes nothing returns nothing
            call RegisterDamageById(function thistype.Actions, DAMAGE_ID_MELEE)
            call RegisterDamageById(function thistype.Actions, DAMAGE_ID_RANGED)
        endmethod

    endstruct
    
endscope
I start a timer that expires in 0 seconds. When it expires, it adds the damage done to the unit's life.
 
Level 17
Joined
Feb 11, 2011
Messages
1,860
Update:
I've got the blocking working! Now I just have one last question: how do I do this part:

Maker said:
If this is for multiple units, then check that the timer is not started yet before starting it again. Unit takes damage can fire multiple times before a timer with 0.00 expiration expires, so make sure you use a correct method to handle all units with one timer. Unit group will work, or a stack or whatever.

EDIT: Would this be a suitable way:
Note: I am using AIDS as an indexer.

JASS:
scope HeavyDefences initializer onInit

    globals
        private constant integer SPELL_ID = 'A004'
        private constant integer LIFE_BONUS_ID = 'A007'
        private constant integer MISSILE_ID = 'h00H'
        private constant real MISSILE_SPEED = 1000 * 0.03
        private boolean array ticking
    endglobals

    public struct Missile
    
        unit caster = null
        unit missile = null
        unit target = null
        real angle = 0
        real damage = 0
        real distance = 0
        
        method destroy takes nothing returns nothing
            set .missile = null
            set .target = null
            set .caster = null
            call .deallocate()
        endmethod
        
        private static method Timer_Actions takes nothing returns nothing
            local timer t = GetExpiredTimer()
            local thistype this = GetTimerData(t)
            local real dx = GetUnitX(.missile) - GetUnitX(.target)
            local real dy = GetUnitY(.missile) - GetUnitY(.target)
            local real x
            local real y
            set .distance = SquareRoot(dx * dx + dy * dy)
            if .distance > (2 * MISSILE_SPEED) and not IsUnitType(.target, UNIT_TYPE_DEAD) then
                set .angle = bj_RADTODEG * (Atan2(GetUnitY(.target) - GetUnitY(.missile), GetUnitX(.target) - GetUnitX(.missile)))
                set x = GetUnitX(.missile) + MISSILE_SPEED * Cos(bj_DEGTORAD * .angle)
                set y = GetUnitY(.missile) + MISSILE_SPEED * Sin(bj_DEGTORAD * .angle)
                call SetUnitX(.missile, x)
                call SetUnitY(.missile, y)
                call SetUnitFacing(.missile, .angle)
            else
                if .distance <= (2 * MISSILE_SPEED) then
                    call UnitDamageTarget(.caster, .target, .damage, false, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, null)
                endif
                call KillUnit(.missile)
                call ReleaseTimer(t)
                call .destroy()
            endif
            set t = null
        endmethod
        
        public static method Create takes unit target, unit source, real damage returns thistype
            local thistype this = thistype.allocate()
            local timer t = NewTimerEx(this)
            local real x = GetUnitX(source)
            local real y = GetUnitY(source)
            set .target = target
            set .caster = source
            set .damage = damage
            set .angle = bj_RADTODEG * (Atan2(GetUnitY(.target) - y, GetUnitX(.target) - x))
            set .missile = CreateUnit(Player(0), MISSILE_ID, x, y, .angle)
            call TimerStart(t, 0.03, true, function thistype.Timer_Actions)
            set t = null
            set target = null
            set source = null
            return this
        endmethod
    
    endstruct
    
    private struct Data
    
        unit caster
        real damage
    
        private method destroy takes nothing returns nothing
            set .caster = null
            call .deallocate()
        endmethod
    
        private static method Timer_Actions takes nothing returns nothing
            local timer t = GetExpiredTimer()
            local thistype this = GetTimerData(t)
            call UnitAddAbility(.caster, LIFE_BONUS_ID)
            call SetUnitAbilityLevel(.caster, LIFE_BONUS_ID, 3)
            call UnitRemoveAbility(.caster, LIFE_BONUS_ID)
            set ticking[GetUnitId(.caster)] = false
            call ReleaseTimer(t)
            call .destroy()
            set t = null
        endmethod
    
        private static method Actions takes nothing returns nothing
            local thistype this = thistype.allocate()
            local timer t = NewTimerEx(this)
            local unit attacker = Damage.source
            local unit damaged = GetTriggerUnit()
            set .caster = damaged
            set .damage = Damage.amount
            if GetUnitAbilityLevel(damaged, SPELL_ID) > 0 and GetRandomInt(1, 1) <= (4 * GetUnitAbilityLevel(damaged, SPELL_ID) + 2) then
                call DestroyEffect(AddSpecialEffectTarget("Abilities\\Spells\\Human\\Defend\\DefendCaster.mdl", damaged, "origin"))
                call UnitAddAbility(damaged, LIFE_BONUS_ID)
                call SetUnitAbilityLevel(damaged, LIFE_BONUS_ID, 2)
                call UnitRemoveAbility(damaged, LIFE_BONUS_ID)
                if not ticking[GetUnitId(damaged)] then
                    set ticking[GetUnitId(damaged)] = true
                    call TimerStart(t, 0, false, function thistype.Timer_Actions)
                endif
                if not IsUnitType(attacker, UNIT_TYPE_HERO) then
                    call Missile.Create(attacker, damaged, GetUnitAbilityLevel(damaged, SPELL_ID) * 25)
                endif
            endif
            set attacker = null
            set damaged = null
            set t = null
        endmethod

        private static method onInit takes nothing returns nothing
            call RegisterDamageById(function thistype.Actions, DAMAGE_ID_MELEE)
            call RegisterDamageById(function thistype.Actions, DAMAGE_ID_RANGED)
        endmethod
    
    endstruct
    
endscope
Also, what is a recommended value to set the item life bonus value as?

EDIT:
After testing with the indexing method, there were bugs where the life bonus wasn't removed.
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
No, you increase the life on every damage but only take it back if ticking is false? Maker meant that you have just one timer and add all the targets to a stack, so the timer can loop over them when expiring. Since a unit might be hurt multiple times before the timer finishes, there has to be the possibility to include it multiple times in the stack or you acculumate all the bonuses per unit. But it's also okay if each instance gets their own timer.
You should save the damage anyway and heighten the max life by the taken damage value unless you take a really big bonus that ensures there is no killing blow. If you have a real stat-setting system, you can define the bonus with any number input.
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
JASS:
        static thistype array CASTER_ATTACHED_DATA

        unit caster
        real damage

        private static method Timer_Actions takes nothing returns nothing
            local timer t = GetExpiredTimer()

            local thistype this = GetTimerData(t)

            local unit caster = .caster
            local real damage = .damage

            set .damage = 0.

            set thistype.CASTER_ATTACHED_DATA[GetUnitId(caster)] = 0

            call ReleaseTimer(t)
            call .destroy()

            set t = null

            call SubtractUnitMaxLife(caster, damage)

            set caster = null
        endmethod
        
        private static method Actions takes nothing returns nothing
            local thistype this
            local timer t
            local unit attacker = Damage.source
            local unit damaged = GetTriggerUnit()
            if GetUnitAbilityLevel(damaged, SPELL_ID) > 0 and GetRandomInt(1, 1) <= (4 * GetUnitAbilityLevel(damaged, SPELL_ID) + 2) then
                set this = thistype.CASTER_ATTACHED_DATA[GetUnitId(damaged)]

                if (this == 0) then
                    set this = thistype.allocate()

                    set t = NewTimerEx(this)

                    set .caster = damaged

                    set thistype.CASTER_ATTACHED_DATA[GetUnitId(damaged)] = this

                    call TimerStart(t, 0, false, function thistype.Timer_Actions)

                    set t = null
                endif

                set .damage = .damage + Damage.amount

                call DestroyEffect(AddSpecialEffectTarget("Abilities\\Spells\\Human\\Defend\\DefendCaster.mdl", damaged, "origin"))
                call AddUnitMaxLife(Damage.amount)

                if not IsUnitType(attacker, UNIT_TYPE_HERO) then
                    call Missile.Create(attacker, damaged, GetUnitAbilityLevel(damaged, SPELL_ID) * 25)
                endif
            endif
            set attacker = null
            set damaged = null
        endmethod
 
Status
Not open for further replies.
Top