Problem with dummy unit

Level 16
Joined
Jul 19, 2007
Messages
942
I have problem with a JASS-ability where the dummy unit casts the "armor reduction ability" on damaged units only sometimes and the "armor reduction ability" doesn't seems set level to the same level as the main-ability. What could be wrong? I have tried everything but I can't find anything wrong and I have checked the raw-codes of the ability/dummy and it's all correct...
JASS:
//TESH.scrollpos=153
//TESH.alwaysfold=0
// +------------------------------------------------------------+
// |                                                            |
// |               -=-=- Armor Crush [v1.5] -=-=-               |
// |                -=-=- By WolfieeifloW -=-=-                 |
// |                Requires JASS NewGen and AIDS               |
// |                                                            |
// +------------------------------------------------------------+
// |                                                            |
// |   Gives a 15% chance to deal a portion of your health      |
// |   back at the attacker. This blow is so shocking it        |
// |   also reduces the attackers armor.                        |
// |                                                            |
// +------------------------------------------------------------+
// |                                                            |
// |   -=-=- How To Implement -=-=-                             |
// |      1. Copy this trigger into your map                    |
// |      2. Copy AIDS into your map                            |
// |       b. Implement AIDS (Follow the script instructions)   |
// |      3. Copy the buffs into your map                       |
// |      4. Copy the abilities into your map                   |
// |      5. Copy the dummies into your map                     |
// |      6. Make sure the 'Rawcodes' in the trigger match      |
// |          your buffs/abilities/units in Object Editor       |
// |      7. Customize the spell                                |
// |      8. Enjoy!                                             |
// |                                                            |
// +------------------------------------------------------------+
// |                                                            |
// |   -=-=- Credits -=-=-                                      |
// |      Credits are not needed, but appreciated               |
// |         Just don't claim this as yours                     |
// |                                                            |
// +------------------------------------------------------------+
// |   -=-=- Version History -=-=-                              |
// |                                                            |
// |      Version 1.5                                           |
// |       - Changed PUI to AIDS                                |
// |       - Fixed some coding issues                           |
// |       - Added some test map features                       |
// |       - Touched up coding conventions                      |
// |                                                            |
// |      Version 1.4                                           |
// |         - Fixed a FirstOfGroup to ForGroup                 |
// |         - Fixed a sound leak                               |
// |         - Fixed texttag leak                               |
// |         - Made floating text more efficient                |
// |            Credits to Artificial                           |
// |                                                            |
// |      Version 1.3                                           |
// |         - Fixed a major bug with Armor Crush not stacking  |
// |         - Made Armor Crush based off an aura               |
// |            Prevents the buff from being removed            |
// |         - Implemented PUI to save GetUnitUserData()        |
// |         - Fixed some BJ's                                  |
// |         - Made Armor Crush sound effect customizable       |
// |         - Various minor changes                            |
// |                                                            |
// |      Older versions...                                     |
// |       Read changelog in thread                             |
// |                                                            |
// +------------------------------------------------------------+

scope ArmorCrush initializer Init

// +-------------------------------------+
// |       -=-=- MODIFY HERE -=-=-       |
// |       -=-=- MODIFY HERE -=-=-       |
// |       -=-=- MODIFY HERE -=-=-       |
// +-------------------------------------+
    globals
        private constant integer ABILITY_ID = 'AACR'
        // Rawcode of 'Armor Crush' ability
    
        private constant integer DUMMY_ABILITY_ID = 'A000'
        // Rawcode of 'Armor Crushed' ability
    
        private constant integer BUFF_ID = 'BACR'
        // Rawcode of 'Armor Crush' buff
    
        private constant integer DUMMY_BUFF_ID = 'BACD'
        // Rawcode of 'Armor Crushed' buff
    
        private constant integer DUMMY = 'nACD'
        // Rawcode of 'DummyCaster' unit
    
        private constant integer VOLUME = 0
        // Volume of the played sound
    
        private constant string EFFECT = ""
        // Path for Armor Crush special effect
    
        private constant string ATTACH = ""
        // Attachment point of Armor Crush special effect
        
        private constant string SOUND = ""
        // Path for Armor Crush sound effect
    
        private constant attacktype ATTACK_TYPE = ATTACK_TYPE_HERO
        // Attack type Armor Crush deals
    
        private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_FIRE
        // Damage type Armor Crush deals
    
        private constant real TEXT_OFFSET = 0
        // Floating text offset
    
        private constant real TEXT_SIZE = 10
        // Floating text font size
    
        private constant integer TEXT_RED = 0
        // Floating text red color amount (0-100)
    
        private constant integer TEXT_GREEN = 0
        // Floating text green color amount (0-100)
    
        private constant integer TEXT_BLUE = 100
        // Floating text blue color amount (0-100)
    
        private constant integer TEXT_ALPHA = 0
        // Floating text transparency (0-100)
    
        private constant real TEXT_LIFE = 3
        // Floating text life
    
        private constant real TEXT_AGE = 2
        // Floating text age
    
        private constant real TEXT_SPEED = 75
        // Floating text speed
    
        private constant real TEXT_ANGLE = 90
        // Floating text angle
    
        private constant string DUMMY_STRING = "acidbomb"
        // 'Order String Use/Turn On' for 'Armor Crushed' ability
    
        // Don't change the following three(3) variables
        private sound CrushSound = null // Don't change this!
        private constant group CRUSHED_GROUP = CreateGroup() // Don't change this!
        private integer array UnitCustom // Don't change this!
        // Don't change the above three(3) variables
    endglobals
    
// +------------------------------------------------+
// |       -=-=- CONFIGURABLE FUNCTIONS -=-=-       |
// |       -=-=- CONFIGURABLE FUNCTIONS -=-=-       |
// |       -=-=- CONFIGURABLE FUNCTIONS -=-=-       |
// +------------------------------------------------+
    // Chance to cast Armor Crush
    private function Chance takes integer level returns integer
        return 15
    endfunction
    
    // How many times Armor Crush stacks
    private function Stack takes integer level returns integer
        return 3
    endfunction
    
    // Percentage of health Armor Crush returns
    // Each 0.01 is equal to 1% (So 0.05 would be 5%, 0.25 would be 25%, and so on)
    private function Return takes integer level returns real
        return level * 0.02
    endfunction    

// +----------------------------------------------+
// |       -=-=- NO TOUCHIE PAST HERE -=-=-       |
// |       -=-=- NO TOUCHIE PAST HERE -=-=-       |
// |       -=-=- NO TOUCHIE PAST HERE -=-=-       |
// +----------------------------------------------+
    private function ACConditions takes nothing returns boolean
        return GetRandomInt(1, 100) <= Chance(GetUnitAbilityLevel(GetTriggerUnit(), ABILITY_ID)) and GetUnitAbilityLevel(GetTriggerUnit(), BUFF_ID) > 0
    endfunction

    private function ACActions takes nothing returns nothing
        local unit ar = GetAttacker()
        local real x = GetUnitX(ar)
        local real y = GetUnitY(ar)
        local unit ad = GetTriggerUnit()
        local integer damageAttacker
        local texttag tt  = CreateTextTag()
        local unit u
        local integer QT = GetUnitAbilityLevel(ad, ABILITY_ID)
        
        if CrushSound == null then
            set CrushSound = CreateSound(SOUND, false, true, true, 10, 10, "SpellsEAX")
        endif
        call AttachSoundToUnit(CrushSound, ar)
        call SetSoundVolume(CrushSound, VOLUME)
        call StartSound(CrushSound)
        call KillSoundWhenDone(CrushSound)
        set CrushSound = null
        call DestroyEffect(AddSpecialEffectTarget(EFFECT, ar, ATTACH))
        set damageAttacker = R2I(GetWidgetLife(ar))
        call UnitDamageTarget(ad, ar, (GetUnitState(ad, UNIT_STATE_LIFE) * (Return(QT))), true, true, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE_WHOKNOWS)
        set damageAttacker = damageAttacker - R2I(GetWidgetLife(ar))
        set u = CreateUnit(GetOwningPlayer(ad), DUMMY, x, y, GetUnitFacing(ar))
        call SetUnitAbilityLevel(u, DUMMY_ABILITY_ID, (UnitCustom[GetUnitIndex(ar)] * QT))
        call IssueTargetOrder(u, DUMMY_STRING, ar)
        call UnitApplyTimedLife(u, 'BACD', 5.00)
        set tt = CreateTextTagUnitBJ((I2S(damageAttacker) + "!" ), ar, TEXT_OFFSET, TEXT_SIZE, TEXT_RED, TEXT_GREEN, TEXT_BLUE, TEXT_ALPHA)
        call SetTextTagPermanent(tt, false)
        call SetTextTagLifespan(tt, TEXT_LIFE)
        call SetTextTagFadepoint(tt, TEXT_AGE)
        call SetTextTagVelocityBJ(tt, TEXT_SPEED, TEXT_ANGLE)
        set tt = null
        if UnitCustom[GetUnitIndex(ar)] < Stack(QT) then
            set UnitCustom[GetUnitIndex(ar)] = UnitCustom[GetUnitIndex(ar)] + 1
            set u = CreateUnit(GetOwningPlayer(ad), DUMMY, x, y, GetUnitFacing(ar))
            call SetUnitAbilityLevel(u, DUMMY_ABILITY_ID, (UnitCustom[GetUnitIndex(ar)] * QT))
            call IssueTargetOrder(u, DUMMY_STRING, ar)
            call UnitApplyTimedLife(u, 'BACD', 5.00)
            if (IsUnitInGroup(ar, CRUSHED_GROUP) == false) then
                call GroupAddUnit(CRUSHED_GROUP, ar)
            endif
        endif
        set ar = null
        set ad = null
        set u = null
    endfunction

    private function CGRConditions takes nothing returns boolean
        return FirstOfGroup(CRUSHED_GROUP) != null
    endfunction

    private function RemoveU takes nothing returns nothing
        if GetUnitAbilityLevel(GetEnumUnit(), DUMMY_BUFF_ID) < 1 then
            call GroupRemoveUnit(CRUSHED_GROUP, GetEnumUnit())
            set UnitCustom[GetUnitIndex(GetEnumUnit())] = 0
        endif
    endfunction
    
    private function CGRActions takes nothing returns nothing
        call ForGroup(CRUSHED_GROUP, function RemoveU)
    endfunction

//===========================================================================
    private function Init takes nothing returns nothing
        local trigger trig = CreateTrigger()
        local integer index = 0
        
        loop
            call TriggerRegisterPlayerUnitEvent(trig, Player(index), EVENT_PLAYER_UNIT_ATTACKED, null)
            set index = index + 1
        exitwhen index == bj_MAX_PLAYER_SLOTS
        endloop
        call TriggerAddCondition(trig, Condition(function ACConditions))
        call TriggerAddAction(trig, function ACActions)
        
        set trig = CreateTrigger()
        call TriggerRegisterTimerEvent(trig, 1.50, true)
        call TriggerAddCondition(trig, Condition(function CGRConditions))
        call TriggerAddAction(trig, function CGRActions)
        set trig = null
    endfunction

endscope
 
Make sure the Dummy is setup properly. The fact that the code is trying to change the facing angle of the Dummy and give it an unnecessarily long expiration timer (5 seconds) implies that the Dummy has the wrong settings.

To make a Dummy unit:
Copy and paste the Locust.
Set Movement Type = None, Attacks Enabled = None, Speed Base = 0.
Every other setting is optional.

Also, if the Dummy needs to move around, like if it's being used to simulate a missile, then you shouldn't be relying on it cast abilities. That's not an issue here, but something to keep in mind. In other words, you usually want a "Caster Dummy" and a "Missile Dummy" with separate settings.

Here's some improved code that I ran through ChatGPT. I didn't test it but it looks fine:
JASS:
scope ArmorCrush initializer Init
    globals
        // ================= CONFIG =================
        private constant integer ABILITY_ID = 'AACR'
        private constant integer DUMMY_ABILITY_ID = 'A000'
        private constant integer BUFF_ID = 'BACR'
        private constant integer DUMMY = 'nACD'
        private constant real STACK_DURATION = 5.0
        private constant real PERIOD = 0.02

        // Do not change this!
        private group Active = CreateGroup()
        private integer array Stacks
        private real array ExpireTime
        private trigger PeriodicTrigger = null
        private integer ActiveCount = 0
    endglobals
 
    // ================= CONFIG =================
    private function Chance takes integer level returns integer
        return 15
    endfunction
 
    private function MaxStacks takes integer level returns integer
        return 3
    endfunction
 
    private function Return takes integer level returns real
        return level * 0.02
    endfunction
 
    // ================= CORE =================
    // Do not change this!
 
    private function Conditions takes nothing returns boolean
        return GetUnitAbilityLevel(GetTriggerUnit(), BUFF_ID) > 0 and GetRandomInt(1, 100) <= Chance(GetUnitAbilityLevel(GetTriggerUnit(), ABILITY_ID))
    endfunction
 
    private function ApplyStacks takes unit target, integer level returns integer
        local integer id = GetUnitUserData(target)
        local integer max = MaxStacks(level)
 
        if Stacks[id] < max then
            set Stacks[id] = Stacks[id] + 1
        endif
 
        // Refresh duration
        set ExpireTime[id] = STACK_DURATION
 
        if not IsUnitInGroup(target, Active) then
            call GroupAddUnit(Active, target)
            set ActiveCount = ActiveCount + 1
 
            // Enable periodic if first unit
            if ActiveCount == 1 then
                call EnableTrigger(PeriodicTrigger)
            endif
        endif
 
        return Stacks[id]
    endfunction
 
    private function CastDummy takes unit caster, unit target, integer stacks returns nothing
        local unit d = CreateUnit(GetOwningPlayer(caster), DUMMY, GetUnitX(target), GetUnitY(target), 0)
 
        call SetUnitAbilityLevel(d, DUMMY_ABILITY_ID, stacks)
        call IssueTargetOrder(d, "acidbomb", target)
        call UnitApplyTimedLife(d, 'BTLF', 2.0)
 
        set d = null
    endfunction
 
    private function OnProc takes nothing returns nothing
        local unit defender = GetTriggerUnit()
        local unit attacker = GetAttacker()
        local integer lvl = GetUnitAbilityLevel(defender, ABILITY_ID)
        local integer stacks
        local real damage
 
        set damage = GetUnitState(defender, UNIT_STATE_LIFE) * Return(lvl)
        call UnitDamageTarget(defender, attacker, damage, true, true, ATTACK_TYPE_HERO, DAMAGE_TYPE_FIRE, null)
 
        set stacks = ApplyStacks(attacker, lvl) + ((lvl - 1) * 3)
        call CastDummy(defender, attacker, stacks)
 
        set defender = null
        set attacker = null
    endfunction
 
    // ================= PERIODIC =================
 
    private function LoopEnum takes nothing returns nothing
        local unit u = GetEnumUnit()
        local integer id = GetUnitUserData(u)
 
        set ExpireTime[id] = ExpireTime[id] - PERIOD
 
        if ExpireTime[id] <= 0.01 then
            set Stacks[id] = 0
            call GroupRemoveUnit(Active, u)
 
            set ActiveCount = ActiveCount - 1
 
            // Disable periodic if no units left
            if ActiveCount == 0 then
                call DisableTrigger(PeriodicTrigger)
            endif
        endif
 
        set u = null
    endfunction
 
    private function Periodic takes nothing returns nothing
        call ForGroup(Active, function LoopEnum)
    endfunction
 
    // ================= INIT =================
 
    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
 
        loop
            call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ATTACKED, null)
            set i = i + 1
            exitwhen i == bj_MAX_PLAYER_SLOTS
        endloop
 
        call TriggerAddCondition(t, Condition(function Conditions))
        call TriggerAddAction(t, function OnProc)
 
        // Periodic trigger (start disabled)
        set PeriodicTrigger = CreateTrigger()
        call TriggerRegisterTimerEvent(PeriodicTrigger, PERIOD, true)
        call TriggerAddAction(PeriodicTrigger, function Periodic)
        call DisableTrigger(PeriodicTrigger)
 
        set t = null
    endfunction
 
    endscope
Remember to only have one Unit Indexer system in your map. If you download a custom spell that uses Custom Value (GetUnitUserData) that means it's likely relying on a Unit Indexer. If not, it should be. The issue is that the author of the spell isn't going to assume that you already have a Unit Indexer in your map, so they tell you to import a new one. Do NOT import it if you already have one.

And there could be issues if the author uses specific API (code) from their suggested Unit Indexer. In that case you'll have to manually adjust the code OR switch over to their system. I would personally stick with Bribe's Unit Indexer and just have ChatGPT modify any "bad" code to use a generic design or reference Bribe's variables instead.
 
Last edited:
Make sure the Dummy is setup properly. The fact that the code is trying to change the facing angle of the Dummy and give it an unnecessarily long expiration timer (5 seconds) implies that the Dummy has the wrong settings.

To make a Dummy unit:
Copy and paste the Locust.
Set Movement Type = None, Attacks Enabled = None, Speed Base = 0.
Every other setting is optional.

Also, if the Dummy needs to move around, like if it's being used to simulate a missile, then you shouldn't be relying on it cast abilities. That's not an issue here, but something to keep in mind. In other words, you usually want a "Caster Dummy" and a "Missile Dummy" with separate settings.

Here's some improved code that I ran through ChatGPT. I didn't test it but it looks fine:
JASS:
scope ArmorCrush initializer Init
    globals
        // ================= CONFIG =================
        private constant integer ABILITY_ID = 'AACR'
        private constant integer DUMMY_ABILITY_ID = 'A000'
        private constant integer DUMMY = 'nACD'
        private constant real STACK_DURATION = 5.0
        private constant real PERIOD = 0.02

        // Do not change this!
        private group Active = CreateGroup()
        private integer array Stacks
        private real array ExpireTime
        private trigger PeriodicTrigger = null
        private integer ActiveCount = 0
    endglobals
 
    // ================= CONFIG =================
    private function Chance takes integer level returns integer
        return 15
    endfunction
 
    private function MaxStacks takes integer level returns integer
        return 3
    endfunction
 
    private function Return takes integer level returns real
        return level * 0.02
    endfunction
 
    // ================= CORE =================
    // Do not change this!
 
    private function Conditions takes nothing returns boolean
        return GetRandomInt(1, 100) <= Chance(GetUnitAbilityLevel(GetTriggerUnit(), ABILITY_ID))
    endfunction
 
    private function ApplyStacks takes unit target, integer level returns integer
        local integer id = GetUnitUserData(target)
        local integer max = MaxStacks(level)
 
        if Stacks[id] < max then
            set Stacks[id] = Stacks[id] + 1
        endif
 
        // Refresh duration
        set ExpireTime[id] = STACK_DURATION
 
        if not IsUnitInGroup(target, Active) then
            call GroupAddUnit(Active, target)
            set ActiveCount = ActiveCount + 1
 
            // Enable periodic if first unit
            if ActiveCount == 1 then
                call EnableTrigger(PeriodicTrigger)
            endif
        endif
 
        return Stacks[id]
    endfunction
 
    private function CastDummy takes unit caster, unit target, integer stacks, integer level returns nothing
        local unit d = CreateUnit(GetOwningPlayer(caster), DUMMY, GetUnitX(target), GetUnitY(target), 0)
 
        call SetUnitAbilityLevel(d, DUMMY_ABILITY_ID, stacks * level)
        call IssueTargetOrder(d, "acidbomb", target)
        call UnitApplyTimedLife(d, 'BTLF', 2.0)
 
        set d = null
    endfunction
 
    private function OnProc takes nothing returns nothing
        local unit defender = GetTriggerUnit()
        local unit attacker = GetAttacker()
        local integer lvl = GetUnitAbilityLevel(defender, ABILITY_ID)
        local integer stacks
        local real damage
 
        set damage = GetUnitState(defender, UNIT_STATE_LIFE) * Return(lvl)
        call UnitDamageTarget(defender, attacker, damage, true, true, ATTACK_TYPE_HERO, DAMAGE_TYPE_FIRE, null)
 
        set stacks = ApplyStacks(attacker, lvl)
        call CastDummy(defender, attacker, stacks, lvl)
 
        set defender = null
        set attacker = null
    endfunction
 
    // ================= PERIODIC =================
 
    private function LoopEnum takes nothing returns nothing
        local unit u = GetEnumUnit()
        local integer id = GetUnitUserData(u)
 
        set ExpireTime[id] = ExpireTime[id] - PERIOD
 
        if ExpireTime[id] <= 0.01 then
            set Stacks[id] = 0
            call GroupRemoveUnit(Active, u)
 
            set ActiveCount = ActiveCount - 1
 
            // Disable periodic if no units left
            if ActiveCount == 0 then
                call DisableTrigger(PeriodicTrigger)
            endif
        endif
 
        set u = null
    endfunction
 
    private function Periodic takes nothing returns nothing
        call ForGroup(Active, function LoopEnum)
    endfunction
 
    // ================= INIT =================
 
    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
 
        loop
            call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ATTACKED, null)
            set i = i + 1
            exitwhen i == bj_MAX_PLAYER_SLOTS
        endloop
 
        call TriggerAddCondition(t, Condition(function Conditions))
        call TriggerAddAction(t, function OnProc)
 
        // Periodic trigger (start disabled)
        set PeriodicTrigger = CreateTrigger()
        call TriggerRegisterTimerEvent(PeriodicTrigger, PERIOD, true)
        call TriggerAddAction(PeriodicTrigger, function Periodic)
        call DisableTrigger(PeriodicTrigger)
 
        set t = null
    endfunction
 
    endscope
Remember to only have one Unit Indexer system in your map. If you download a custom spell that uses Custom Value (GetUnitUserData) that means it's likely relying on a Unit Indexer. If not, it should be. The issue is that the author of the spell isn't going to assume that you already have a Unit Indexer in your map, so they tell you to import a new one. Do NOT import it if you already have one.

And there could be issues if the author uses specific API (code) from their suggested Unit Indexer. In that case you'll have to manually adjust the code OR switch over to their system. I would personally stick with Bribe's Unit Indexer and just have ChatGPT modify any "bad" code to use a generic design or reference Bribe's variables instead.
Oh the dummy unit's Speed Base wasn't set to 0 and thats why it didn't work correctly. Now it seems to be working. I didn't test the JASS-trigger you sent but I guess I don't have to since the old one seems to be working.
 
but I guess I don't have to since the old one seems to be working.
The old script uses a very primitive way to remove units from the CRUSHED_GROUP. It checks them every 1.5 seconds, but it doesn't have a robust way to handle units that die, are removed from the game, or have their custom values overwritten by another system.

Eventually, that group will fill up with "stale" unit references or units that shouldn't be there, leading to a slow memory leak or even a desync in multiplayer.
Then "Level 0" Math is still there

On the first proc, UnitCustom is 0.
0 * Level = 0.
The dummy casts a Level 0 ability.

In many versions of WC3, a Level 0 ability does the default data or nothing at all, if I’m not wrong, the actual armor reduction might not be applying correctly on that first strike.

Efficiency (The Periodic vs. Group argument)

Uncle’s code is Event-Driven. It only runs when someone is actually debuffed.

The old code has a trigger that is Always On, constantly polling the CRUSHED_GROUP every 1.5 seconds for the entire duration of the match, even if no one has picked that hero or used that ability for 40 minutes.

The script screams 2009, especially cuz of the Advanced Indexing and data storage, but if i recall you are using bribe’s in your map.

@Uncle sorry for the tag, but wanted to check:

call SetUnitAbilityLevel(d, DUMMY_ABILITY_ID, stacks * level)

Will this create a lvl overflow bug?
 
Last edited:
Will this create a lvl overflow bug?
I didn't notice that, it should just be set to the stacks variable.

But this design could allow you to have scaling armor reduction based on the ability level. For instance, if this Armor Crush ability had 3 levels...

Levels 1-3 of Acid Bomb could represent 1, 2, and 3 stacks for Crush Level 1.
4-6 = stacks for Level 2.
7-9 = stacks for Level 3.

Would have to play with the math a bit, though.
 
Make sure the Dummy is setup properly. The fact that the code is trying to change the facing angle of the Dummy and give it an unnecessarily long expiration timer (5 seconds) implies that the Dummy has the wrong settings.

To make a Dummy unit:
Copy and paste the Locust.
Set Movement Type = None, Attacks Enabled = None, Speed Base = 0.
Every other setting is optional.

Also, if the Dummy needs to move around, like if it's being used to simulate a missile, then you shouldn't be relying on it cast abilities. That's not an issue here, but something to keep in mind. In other words, you usually want a "Caster Dummy" and a "Missile Dummy" with separate settings.

Here's some improved code that I ran through ChatGPT. I didn't test it but it looks fine:
JASS:
scope ArmorCrush initializer Init
    globals
        // ================= CONFIG =================
        private constant integer ABILITY_ID = 'AACR'
        private constant integer DUMMY_ABILITY_ID = 'A000'
        private constant integer DUMMY = 'nACD'
        private constant real STACK_DURATION = 5.0
        private constant real PERIOD = 0.02

        // Do not change this!
        private group Active = CreateGroup()
        private integer array Stacks
        private real array ExpireTime
        private trigger PeriodicTrigger = null
        private integer ActiveCount = 0
    endglobals
 
    // ================= CONFIG =================
    private function Chance takes integer level returns integer
        return 15
    endfunction
 
    private function MaxStacks takes integer level returns integer
        return 3
    endfunction
 
    private function Return takes integer level returns real
        return level * 0.02
    endfunction
 
    // ================= CORE =================
    // Do not change this!
 
    private function Conditions takes nothing returns boolean
        return GetRandomInt(1, 100) <= Chance(GetUnitAbilityLevel(GetTriggerUnit(), ABILITY_ID))
    endfunction
 
    private function ApplyStacks takes unit target, integer level returns integer
        local integer id = GetUnitUserData(target)
        local integer max = MaxStacks(level)
 
        if Stacks[id] < max then
            set Stacks[id] = Stacks[id] + 1
        endif
 
        // Refresh duration
        set ExpireTime[id] = STACK_DURATION
 
        if not IsUnitInGroup(target, Active) then
            call GroupAddUnit(Active, target)
            set ActiveCount = ActiveCount + 1
 
            // Enable periodic if first unit
            if ActiveCount == 1 then
                call EnableTrigger(PeriodicTrigger)
            endif
        endif
 
        return Stacks[id]
    endfunction
 
    private function CastDummy takes unit caster, unit target, integer stacks returns nothing
        local unit d = CreateUnit(GetOwningPlayer(caster), DUMMY, GetUnitX(target), GetUnitY(target), 0)
 
        call SetUnitAbilityLevel(d, DUMMY_ABILITY_ID, stacks)
        call IssueTargetOrder(d, "acidbomb", target)
        call UnitApplyTimedLife(d, 'BTLF', 2.0)
 
        set d = null
    endfunction
 
    private function OnProc takes nothing returns nothing
        local unit defender = GetTriggerUnit()
        local unit attacker = GetAttacker()
        local integer lvl = GetUnitAbilityLevel(defender, ABILITY_ID)
        local integer stacks
        local real damage
 
        set damage = GetUnitState(defender, UNIT_STATE_LIFE) * Return(lvl)
        call UnitDamageTarget(defender, attacker, damage, true, true, ATTACK_TYPE_HERO, DAMAGE_TYPE_FIRE, null)
 
        set stacks = ApplyStacks(attacker, lvl)
        call CastDummy(defender, attacker, stacks)
 
        set defender = null
        set attacker = null
    endfunction
 
    // ================= PERIODIC =================
 
    private function LoopEnum takes nothing returns nothing
        local unit u = GetEnumUnit()
        local integer id = GetUnitUserData(u)
 
        set ExpireTime[id] = ExpireTime[id] - PERIOD
 
        if ExpireTime[id] <= 0.01 then
            set Stacks[id] = 0
            call GroupRemoveUnit(Active, u)
 
            set ActiveCount = ActiveCount - 1
 
            // Disable periodic if no units left
            if ActiveCount == 0 then
                call DisableTrigger(PeriodicTrigger)
            endif
        endif
 
        set u = null
    endfunction
 
    private function Periodic takes nothing returns nothing
        call ForGroup(Active, function LoopEnum)
    endfunction
 
    // ================= INIT =================
 
    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
 
        loop
            call TriggerRegisterPlayerUnitEvent(t, Player(i), EVENT_PLAYER_UNIT_ATTACKED, null)
            set i = i + 1
            exitwhen i == bj_MAX_PLAYER_SLOTS
        endloop
 
        call TriggerAddCondition(t, Condition(function Conditions))
        call TriggerAddAction(t, function OnProc)
 
        // Periodic trigger (start disabled)
        set PeriodicTrigger = CreateTrigger()
        call TriggerRegisterTimerEvent(PeriodicTrigger, PERIOD, true)
        call TriggerAddAction(PeriodicTrigger, function Periodic)
        call DisableTrigger(PeriodicTrigger)
 
        set t = null
    endfunction
 
    endscope
Remember to only have one Unit Indexer system in your map. If you download a custom spell that uses Custom Value (GetUnitUserData) that means it's likely relying on a Unit Indexer. If not, it should be. The issue is that the author of the spell isn't going to assume that you already have a Unit Indexer in your map, so they tell you to import a new one. Do NOT import it if you already have one.

And there could be issues if the author uses specific API (code) from their suggested Unit Indexer. In that case you'll have to manually adjust the code OR switch over to their system. I would personally stick with Bribe's Unit Indexer and just have ChatGPT modify any "bad" code to use a generic design or reference Bribe's variables instead.
I tried to add this JASS-trigger into my map but it causes HUGE lag and almost makes my map freeze when it deals the damage and reduce the attackers armor... And yes I have only 1 unit indexer system in my map.
 
I tried to add this JASS-trigger into my map but it causes HUGE lag and almost makes my map freeze when it deals the damage and reduce the attackers armor... And yes I have only 1 unit indexer system in my map
I have a couple of suspicions (assuming everything is set up correctly on your end):

The periodic runs every 0.02s and processes the Active group each tick. If units aren’t being removed properly (or stacks keep refreshing), that group can grow quickly and end up being iterated ~50 times/sec; which would explain the huge lag.

Things I’d check:
  • Are units ever leaving the Active group? (Is ExpireTime constantly refreshing?)
  • Are dead/removed units being cleaned up properly?
  • EVENT_PLAYER_UNIT_ATTACKED fires very often — this might be over-triggering
Quick test:

Set PERIOD to 0.10 —if the lag drops, it’s a scaling issue rather than a logic bug, and you can go from there.

That said, I’d still wait for Uncle to confirm.

! I’m also wondering does this really need a 0.02 periodic? i suppose this could be made fully event-driven with a timer per unit and avoid iterating the group every tick entirely.
 
Last edited:
I have a couple of suspicions (assuming everything is set up correctly on your end):

The periodic runs every 0.02s and processes the Active group each tick. If units aren’t being removed properly (or stacks keep refreshing), that group can grow quickly and end up being iterated ~50 times/sec; which would explain the huge lag.

Things I’d check:
  • Are units ever leaving the Active group? (Is ExpireTime constantly refreshing?)
  • Are dead/removed units being cleaned up properly?
  • EVENT_PLAYER_UNIT_ATTACKED fires very often — this might be over-triggering
Quick test:

Set PERIOD to 0.10 —if the lag drops, it’s a scaling issue rather than a logic bug, and you can go from there.

That said, I’d still wait for Uncle to confirm.

! I’m also wondering does this really need a 0.02 periodic? i suppose this could be made fully event-driven with a timer per unit and avoid iterating the group every tick entirely.
Still same lag... Also it seems like ALL units have the chance to damage and reduce the attackers armor even if it doesn't have the ability... Only 1 Hero is mean't to have this ability and it should only work on that Hero when the ability is chosen.
 
Still same lag... Also it seems like ALL units have the chance to damage and reduce the attackers armor even if it doesn't have the ability... Only 1 Hero is mean't to have this ability and it should only work on that Hero when the ability is chosen.

Code:
private function Conditions takes nothing returns boolean
        // We MUST check if the attacked unit actually has the Aura buff first.
        // I suppose the engine rolls the 15% chance for everyone on the map
        return GetUnitAbilityLevel(GetTriggerUnit(), BUFF_ID) > 0 and GetRandomInt(1, 100) <= Chance(GetUnitAbilityLevel(GetTriggerUnit(), ABILITY_ID))
    endfunction

Replace the condition function, I completely failed to see that there is no buff check,

Also make sure you still have private constant integer BUFF_ID = 'BACR' declared in your globals, this is after all an aura spell.
 
Code:
private function Conditions takes nothing returns boolean
        // We MUST check if the attacked unit actually has the Aura buff first.
        // I suppose the engine rolls the 15% chance for everyone on the map
        return GetUnitAbilityLevel(GetTriggerUnit(), BUFF_ID) > 0 and GetRandomInt(1, 100) <= Chance(GetUnitAbilityLevel(GetTriggerUnit(), ABILITY_ID))
    endfunction

Replace the condition function, I completely failed to see that there is no buff check,

Also make sure you still have private constant integer BUFF_ID = 'BACR' declared in your globals, this is after all an aura spell.
Ok. Now let's solve the lag problem. I hope Uncle can help...
 
I would assume that the lag comes from either:
1. "A unit is Attacked" Event. If you weren't using this Event anywhere else before then it would likely add overhead.
2. The Acid Bomb settings are messed up.
3. You're overwriting Custom Values. A very easy mistake to make, all it takes is importing a custom spell or system without looking into what it's doing.
Also, you never want to use this Action in your GUI triggers. This is reserved for the Unit Indexer:
  • Unit - Set unit custom value
A short period of 0.02 seconds should be irrelevant here. It's what you're doing every 0.02 seconds that matters.
Simple math every 0.02s -> Not an issue.
Creating a unit every 0.02s -> Problematic.
But feel free to change it to say 0.05s, the lack of precision is not a big deal.
 
Last edited:
I would assume that the lag comes from either:
1. "A unit is Attacked" Event. If you weren't using this Event anywhere else before then it would likely add overhead.
2. The Acid Bomb settings are messed up.
3. You're overwriting Custom Values. A very easy mistake to make, all it takes is importing a custom spell or system without looking into what it's doing.
Also, you never want to use this Action in your GUI triggers. This is reserved for the Unit Indexer:
  • Unit - Set unit custom value
A short period of 0.02 seconds should be irrelevant here. It's what you're doing every 0.02 seconds that matters.
Simple math every 0.02s -> Not an issue.
Creating a unit every 0.02s -> Problematic.
But feel free to change it to say 0.05s, the lack of precision is not a big deal.
I think number 1 could be the reason. I'm using "A unit is Attacked" Event in other abilities too and maybe they conflict? I dunno... So what can I do to solve that?
 
I think number 1 could be the reason. I'm using "A unit is Attacked" Event in other abilities too and maybe they conflict? I dunno... So what can I do to solve that?
No, that wouldn't be it. There is no conflict and the type of lag from using the Event would be happening in those other triggers.

The issue is likely related to the Acid Bomb ability having weird settings. You have to be careful with using short Durations/Intervals. A value of 0.00 can cause major issues.

I would also ensure that your Dummy unit isn't doing anything weird. I often give the Dummy a visible Model, say a Footman, so that you can see what it's doing and notice any weird behavior. For example, can the Dummy attack?

Lastly, copy the code and run it through ChatGPT and ask it if you see any potential issues that would cause immense lag. Just remember that it can give misinformation so double check with us here.
 
Last edited:
No, that wouldn't be it. There is no conflict and the type of lag from using the Event would be happening in those other triggers.

The issue is likely related to the Acid Bomb ability having weird settings. You have to be careful with using short Duration/Intervals. A value of 0.00 can cause major issues.

I would also ensure that your Dummy unit isn't doing anything weird. I often give the Dummy a visible Model, say a Footman, so that you can see what it's doing and notice any weird behavior. For example, can the Dummy attack?

Lastly, copy the code and run it through ChatGPT and ask it if you see any potential issues that would cause immense lag. Just remember that it can give you some misinformation so double check with us here.
Oh there it is. I set the damage interval on the acid bomb ability to 0 and that's why it almost freezes the map! Changed it to 1 now and it works like a charm :thumbs_up:
 
Hmm still it seems like it doesn't reduce the armor correctly... When I levled to lvl2 of this ability, it still reduces the attacker's armor by 1 :-/
Did you change Acid Bomb to have 3 levels with each level reducing the Armor by a different amount?

That's what makes it "stack", it's just using a stronger Acid Bomb each time.

This code creates the Dummy on top of the attacking unit (target):
JASS:
    private function CastDummy takes unit caster, unit target, integer stacks returns nothing
        local unit d = CreateUnit(GetOwningPlayer(caster), DUMMY, GetUnitX(target), GetUnitY(target), 0)
 
        call SetUnitAbilityLevel(d, DUMMY_ABILITY_ID, stacks)
        call IssueTargetOrder(d, "acidbomb", target)
        call UnitApplyTimedLife(d, 'BTLF', 2.0)
 
        set d = null
    endfunction
Then it increases the level of Acid Bomb based on the number of stacks. You define the max amount here:
JASS:
    private function MaxStacks takes integer level returns integer
        return 3
    endfunction
Then it orders the Dummy to cast Acid Bomb on the target.
 
Did you change Acid Bomb to have 3 levels with each level reducing the Armor by a different amount?

That's what makes it "stack", it's just using a stronger Acid Bomb each time.

This code creates the Dummy on top of the attacking unit (target):
JASS:
    private function CastDummy takes unit caster, unit target, integer stacks returns nothing
        local unit d = CreateUnit(GetOwningPlayer(caster), DUMMY, GetUnitX(target), GetUnitY(target), 0)
 
        call SetUnitAbilityLevel(d, DUMMY_ABILITY_ID, stacks)
        call IssueTargetOrder(d, "acidbomb", target)
        call UnitApplyTimedLife(d, 'BTLF', 2.0)
 
        set d = null
    endfunction
Then it increases the level of Acid Bomb based on the number of stacks. You define the max amount here:
JASS:
    private function MaxStacks takes integer level returns integer
        return 3
    endfunction
Then it orders the Dummy to cast Acid Bomb on the target.
Well only lvl1 seems to be working correctly... It seems like it doesn't set the lvl of the acid bomb ability to the level of armor crush for the triggering unit :-/
 
This is the line of code that changes the ability level:
JASS:
call SetUnitAbilityLevel(d, DUMMY_ABILITY_ID, stacks)
We know that the Dummy is casting the ability so that rules out an issue with the Unit variable "d".

Another possible issue would be that "DUMMY_ABILITY_ID" is incorrect. But that's what the Dummy is successfully casting, so surely it can't be that.

The next issue would be that "stacks" is always equal to 1, so the ability is always set to level 1. Seems like we've narrowed it down.

With that in mind, have you tried updating the code? If you see from previous replies, Axolotl and I discussed how "stacks" was incorrect in the original code. I've gone and updated everything in the original post's code to reflect this.
 
Last edited:
This is the line of code that changes the ability level:
JASS:
call SetUnitAbilityLevel(d, DUMMY_ABILITY_ID, stacks)
We know that the Dummy is casting the ability so that rules out an issue with the Unit variable "d".

Another possible issue would be that "DUMMY_ABILITY_ID" is incorrect. But that's what the Dummy is successfully casting, so surely it can't be that.

The next issue would be that "stacks" is always equal to 1, so the ability is always set to level 1. Seems like we've narrowed it down.

With that in mind, have you tried updating the code? If you see from previous replies, Axolotl and I discussed how "stacks" was incorrect in the original code. I've gone and updated everything in the original post's code to reflect this.
Yes I'm using the code you sent to me
 
BUFF_ID: This MUST be the Aura buff that the Hero carries. The trigger checks this to see if the hero is actually 'ready' to proc the armor crush. If you put the Acid Bomb buff here, the ability will probably fail to trigger.

As for the level issue, I believe the problem is that the code ignores the hero's lvl - We can map the hero's lvl and stacks together, so update your cast dummy with this:


Code:
private function CastDummy takes unit caster, unit target, integer stacks, integer lvl returns nothing
    local unit d = CreateUnit(GetOwningPlayer(caster), DUMMY, GetUnitX(target), GetUnitY(target), 0)
   
    // Math: (Hero Level 1 = levels 1-3), (Hero Level 2 = levels 4-6), (Hero Level 3 = levels 7-9)
    local integer dummyLevel = ((lvl - 1) * MaxStacks(lvl)) + stacks
   
    call SetUnitAbilityLevel(d, DUMMY_ABILITY_ID, dummyLevel)
    call IssueTargetOrder(d, "acidbomb", target)
    call UnitApplyTimedLife(d, 'BTLF', 2.0)
   
    set d = null
endfunction

Then also, update the call inside the onproc:
Code:
// Find this line in OnProc and add 'lvl' to the end:
call CastDummy(defender, attacker, stacks, lvl)
 
BUFF_ID: This MUST be the Aura buff that the Hero carries. The trigger checks this to see if the hero is actually 'ready' to proc the armor crush. If you put the Acid Bomb buff here, the ability will probably fail to trigger.

As for the level issue, I believe the problem is that the code ignores the hero's lvl - We can map the hero's lvl and stacks together, so update your cast dummy with this:


Code:
private function CastDummy takes unit caster, unit target, integer stacks, integer lvl returns nothing
    local unit d = CreateUnit(GetOwningPlayer(caster), DUMMY, GetUnitX(target), GetUnitY(target), 0)
  
    // Math: (Hero Level 1 = levels 1-3), (Hero Level 2 = levels 4-6), (Hero Level 3 = levels 7-9)
    local integer dummyLevel = ((lvl - 1) * MaxStacks(lvl)) + stacks
  
    call SetUnitAbilityLevel(d, DUMMY_ABILITY_ID, dummyLevel)
    call IssueTargetOrder(d, "acidbomb", target)
    call UnitApplyTimedLife(d, 'BTLF', 2.0)
  
    set d = null
endfunction

Then also, update the call inside the onproc:
Code:
// Find this line in OnProc and add 'lvl' to the end:
call CastDummy(defender, attacker, stacks, lvl)
Ok but this ability actually has 4 levels.
 
Ok but this ability actually has 4 levels.
:D

Since the system maps stacks directly to ability levels, the dummy ability needs enough levels to cover every combination of (hero level × stacks).

With 4 ability levels and 3 max stacks, that means:
4 × 3 = 12 total levels.

So your mapping becomes:

Code:
private function CastDummy takes unit caster, unit target, integer stacks, integer lvl returns nothing
    local unit d = CreateUnit(GetOwningPlayer(caster), DUMMY, GetUnitX(target), GetUnitY(target), 0)

    // L1: 1–3
    // L2: 4–6
    // L3: 7–9
    // L4: 10–12
    local integer dummyLevel = ((lvl - 1) * 3) + stacks

    call SetUnitAbilityLevel(d, DUMMY_ABILITY_ID, dummyLevel)
    call IssueTargetOrder(d, "acidbomb", target)
    call UnitApplyTimedLife(d, 'BTLF', 2.0)

    set d = null
endfunction


Also double-check the Object Editor:

"Stats - Levels" for the dummy ability must be set to 12, otherwise anything above the defined max will fall back to level 1
 
Hmm actually lvl 2 is meant to reduce armor by -2, -4, -6 but atm it seems to reduce it by -4,-5,-6...
ac.png
 
The code is doing exactly what it's told; it's picking levels 4, 5, and The reason you're seeing -4, -5, -6 is because of how you set up the values in the Object Editor.

You need to fill out all 12 levels of your Acid Bomb like this to match your tooltip:




Dummy LevelHero LevelStacks Armor Reduction (Set this in OE)
1Lvl 11 Stack-1
2Lvl 12 Stacks-2
3Lvl 13 Stacks-3
4Lvl 21 Stack-2 (You probably have -4 here)
5Lvl 22 Stacks-4 (You probably have -5 here)
6Lvl 23 Stacks-6
7Lvl 31 Stack-3
8Lvl 32 Stacks-6
9Lvl 33 Stacks-9
10Lvl 41 Stack-4
11Lvl 42 Stacks-8
12Lvl 43 Stacks-12

Also note: Acid Bomb uses positive values for Armor Reduction in the Object Editor.

So instead of -2, -4, -6 etc, you should enter:
2, 4, 6 etc.

The engine applies it as a negative armor debuff automatically.
 
Last edited:
BUFF_ID is the caster's buff, but it looks like you figured that out.

Anyway, I updated the code:
JASS:
    private function OnProc takes nothing returns nothing
        local unit defender = GetTriggerUnit()
        local unit attacker = GetAttacker()
        local integer lvl = GetUnitAbilityLevel(defender, ABILITY_ID)
        local integer stacks
        local real damage
 
        set damage = GetUnitState(defender, UNIT_STATE_LIFE) * Return(lvl)
        call UnitDamageTarget(defender, attacker, damage, true, true, ATTACK_TYPE_HERO, DAMAGE_TYPE_FIRE, null)
 
        set stacks = ApplyStacks(attacker, lvl) + ((lvl - 1) * 3)
        call CastDummy(defender, attacker, stacks)
 
        set defender = null
        set attacker = null
    endfunction
Now it's using the formula that Axolotl suggested, although I put it in the OnProc function instead (not important).
 
The code is doing exactly what it's told; it's picking levels 4, 5, and The reason you're seeing -4, -5, -6 is because of how you set up the values in the Object Editor.

You need to fill out all 12 levels of your Acid Bomb like this to match your tooltip:




Dummy LevelHero LevelStacks Armor Reduction (Set this in OE)
1Lvl 11 Stack-1
2Lvl 12 Stacks-2
3Lvl 13 Stacks-3
4Lvl 21 Stack-2 (You probably have -4 here)
5Lvl 22 Stacks-4 (You probably have -5 here)
6Lvl 23 Stacks-6
7Lvl 31 Stack-3
8Lvl 32 Stacks-6
9Lvl 33 Stacks-9
10Lvl 41 Stack-4
11Lvl 42 Stacks-8
12Lvl 43 Stacks-12

Also note: Acid Bomb uses positive values for Armor Reduction in the Object Editor.

So instead of -2, -4, -6 etc, you should enter:
2, 4, 6 etc.

The engine applies it as a negative armor debuff automatically.
Oh so that was the cause of it... Thanks for help it works now!
 
Back
Top