[Lua] Delayed actions and MUI management in LUA

Level 7
Joined
Feb 8, 2015
Messages
124
Recently getting into lua (after just getting into JASS...) and while the use of local variables makes a lot of MUI easier, I'm not certain how to deal with delayed/repeating actions for multiple instances.

The spell I'm making is fairly straight forward. Mark a target area and continually deal damage through it (like a Blizzard, but without the channel)... There's also a dummy unit involved, but that part is simpler.
Now, in JASS I would've done something like this:
JASS:
function Trig_Nebula_cast_Actions takes nothing returns nothing
    local unit u = GetTriggerUnit()
    local timer t = CreateTimer()
    local integer id = GetHandleId(t)

    local real area = 200      // placeholder value for readability
    local real damage = 100 // placeholder value for readability
    local integer wcount=1

    local real x = GetSpellTargetX()
    local real y = GetSpellTargetY()

    call SaveUnitHandle(udg_Spell_Table, id, 1, u)             
    call SaveReal(udg_Spell_Table, id, 2, damage)
    call SaveReal(udg_Spell_Table, id, 3, area)
    call SaveReal(udg_Spell_Table, id, 4, x)
    call SaveReal(udg_Spell_Table, id, 5, y)
    call SaveInteger(udg_Spell_Table, id, 6, count)

    call TimerStart(t, 1.0, false, function PeriodicDamage)
    set t = null
    set u = null
endfunction
And the periodically (recursively) run function is
JASS:
function PeriodicDamage takes nothing returns nothing
    local timer t = GetExpiredTimer()
    local integer id = GetHandleId(t)

    local unit u = LoadUnitHandle(udg_Spell_Table, id, 1)
    local real dmg = LoadReal(udg_Spell_Table, id, 2)
    local real area = LoadReal(udg_Spell_Table, id, 3)
    local real x = LoadReal(udg_Spell_Table, id, 4)
    local real y = LoadReal(udg_Spell_Table, id, 5)
    local integer count = LoadInteger(udg_Spell_Table, id, 6)

    local group ug = CreateGroup()
    local unit pu
    if wcount <= 6 then
        call GroupEnumUnitsInRange(ug, x, y, area, null)
            loop
               set pu = FirstOfGroup(ug)
               exitwhen (pickedu == null)
               if IsUnitEnemy(pu, GetOwningPlayer(u)) then
                   call UnitDamageTarget(u, pu, dmg, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, null)
               endif
               call GroupRemoveUnit(ug, pu)
           endloop
        call DestroyEffect(AddSpecialEffect("SpecialEffect\\Something.mdl", x , y))
        call SaveInteger(udg_Spell_Table, id, 6, count+1)
        call TimerStart(t, 1.0, false, function PeriodicDamage)
    else
        //cleanup
        call PauseTimer(t)
        call DestroyTimer(t)
        call FlushChildHashtable(udg_Spell_Table, id)
    endif

    call DestroyGroup(ug)
    set ug = null
    set pickedu = null
    set u = null
    set t = null
endfunction


That's all very well and good for JASS and global hashtables (from the GUI variable editor), but that's not how you're supposed to do things in Lua, as I understand it.
My queston (perhaps too broad for this forum) then; how am I supposed to do this in lua?
Using global variables seems out of the question - since I want to allow multiple simultaneous instances, each counting waves separately and "remembering" the right caster. I take it this is a place to use lua tables? But I'm not experienced with it, as I've only coded in Python, C++ and JASS.
 
Lua lets you linearize delays by creating a blocking function (like TriggerSleepAction but does not suck), which is similar to how waits are implemented in StarCraft II. This is done using the coroutine functionality.

Your cast instance is run as a coroutine. A custom "wait" function starts a timer with a timeout of the desired duration to wait, which on expiry resumes the current coroutine and then after starting the timer yields the current coroutine. From the programming perspective the wait function is a blocking call which halts execution of the current thread until the condition is met.

This principle is not just limited to waiting some amount of time. It could be used to implement more complex waits such as until a unit is damaged, until channelling completes, e.t.c.

This is by far the most intuitive way to implement a simple ability. It does have its limitations though, in that complicated branching, or spaghetti, logic is not really supported. At least in the implementation described above.

Otherwise nothing stops you from using a more traditional, instance based, approach. Similar to how MUI GUI triggers are made. A global table could be a list of instance tables which hold the state of each active cast. These instance tables get reused to avoid garbage collector overhead.
 
Here's an example that aims to keep it as simple as possible. You'd want to expand upon this with your own system(s) to not only simplify the process but allow for more control, like Dr suggested.
Lua:
function Nebula_Init()
    -- Define the stats for [Q]
    local q_dmg = { 100, 200, 300, 400 }
    local q_aoe = { 200, 200, 200, 200 }
    local q_waves = { 3, 4, 5, 6 }
 
    -- Then create the spell effects for [Q]
    local function Nebula_Q()

        -- Exit early if this is the wrong ability
        local abilId = GetSpellAbilityId()
        if abilId ~= FourCC("A000") then
            return
        end
 
        -- Getters
        local u = GetTriggerUnit()
        local x = GetSpellTargetX()
        local y = GetSpellTargetY()
        local lvl = GetUnitAbilityLevel(u, abilId)
 
        -- Stats
        local dmg = q_dmg[lvl]
        local aoe = q_aoe[lvl]
        local waves = q_waves[lvl]
 
        -- Objects
        local ug = CreateGroup()
        local t = CreateTimer()
 
        -- START --
        TimerStart(t, 1.0, true, function()

            -- Deal AoE damage
            GroupEnumUnitsInRange(ug, x, y, aoe, nil)
            ForGroup(ug, function()
                local enemy = GetEnumUnit()
                if IsUnitEnemy(u, GetOwningPlayer(enemy)) then
                    UnitDamageTarget(u, enemy, dmg, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, nil)
                end
            end)
    
            -- Create visual effects
            DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\Other\\NeutralBuildingExplosion\\NeutralBuildingExplosion.mdl", x , y))

            -- Try to end the spell
            waves = waves - 1
            if waves == 0 then
                DestroyGroup(ug)
                PauseTimer(t)
                DestroyTimer(t)
            end
        end)
        -- END --
    end

    -- Then register [Q] as a new spell event
    local tr = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(tr, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    TriggerAddAction(tr, Nebula_Q)
end
Lua has some garbage collection quirks so hopefully everything declared in there persists, I generally don't design things this way.

And here's the lazy way to initialize all of this:
  • Nebula Init
    • Events
      • Time - Elapsed game time is 0.00 seconds
    • Conditions
    • Actions
      • Custom script: Nebula_Init()
 

Attachments

Last edited:
Lua lets you linearize delays by creating a blocking function (like TriggerSleepAction but does not suck), which is similar to how waits are implemented in StarCraft II. This is done using the coroutine functionality.

Your cast instance is run as a coroutine. A custom "wait" function starts a timer with a timeout of the desired duration to wait, which on expiry resumes the current coroutine and then after starting the timer yields the current coroutine. From the programming perspective the wait function is a blocking call which halts execution of the current thread until the condition is met.

This principle is not just limited to waiting some amount of time. It could be used to implement more complex waits such as until a unit is damaged, until channelling completes, e.t.c.

This is by far the most intuitive way to implement a simple ability. It does have its limitations though, in that complicated branching, or spaghetti, logic is not really supported. At least in the implementation described above.

Otherwise nothing stops you from using a more traditional, instance based, approach. Similar to how MUI GUI triggers are made. A global table could be a list of instance tables which hold the state of each active cast. These instance tables get reused to avoid garbage collector overhead.

I never would've thought of using wait (sleep) functions. I've gotten so used to the WC3 rule of Waits being broken and trashy problematic for GUI purposes.
Although how does that stack up against MUI? If another unit fires the ability while the SleepWhateverFunc() from a previous cast is still counting down, does it just start another instance ("thread")?

With that being said, I feel like tracking instances in a table makes more sense to me - but that might just be the old GUI/JASS hashtable mindset.
Intuitively (to me) an "array of arrays" would work; each index containing an array (or list, or table, or whatever) with relevant data, then cycle through it.
I.e. the classic mui approach of index = index + 1, and looping through them all periodically.

Lua still a bit esoteric to me; and "best practices" more so.
Thanks for the replies!

Lua:
function Nebula_Init()
    -- Define the stats for [Q]
    local q_dmg = { 100, 200, 300, 400 }
    local q_aoe = { 200, 200, 200, 200 }
    local q_waves = { 3, 4, 5, 6 }

I got around having to do this with a small snippet I made! - Though admittedly only the JASS version is tested.
Basically, since most of my spells are based on Channel - where most of the Object Editor values are actually entirely irrelevant - I load ability stats into the trigger, which means that if I'm nerfing or buffing something, I only have to change the Object Editor values! Everything else is just a copy.

e.g.
set Dmg = GetAbilityField("herodur", 'A00Q', (level of ability being cast))
set Dur = GetAbilityField("normaldur", 'A00Q', (level of ability being cast))
etc.

And the tooltips are likewise just referencing these values, e.g. "<A00Q:ANcl,HeroDur1> damage per second."
 
Last edited:
I never would've thought of using wait (sleep) functions. I've gotten so used to the WC3 rule of Waits being broken and trashy problematic for GUI purposes.
Just to clarify, he's not suggesting that you use the Wait functions, but mentioning some similar concepts like Coroutines. That WC3 rule still applies, regardless of whether you use GUI or not.
I got around having to do this with a small snippet I made!
That's nice, it's probably the easiest way to maintain your Tooltips, although somewhat limited.

So with Lua you'll still want to use Timers since they're precise, and as I showed in my code you no longer need a separate callback function for them (at least a named one). You also no longer need to link your data together in such a cumbersome way, ie: a Hashtable pairing a Timer to various sets of data. In fact, you should never use the GetHandleId() function in Lua as it causes desyncs.

Also, looking at my example code, the function that handles the logic for your [Q] spell is all handled locally within that Init function. So all of the local variables that are declared within it are able to be "snapshotted" and referenced at any point in time. In other words, your entire hero could be crammed into that function. That being said, if you need to access any of that data from outside of the function then you're going to want to have things handled more publicly than privately, like global tables that store your data (similar to a Hashtable).

Another great thing about Lua is how you can use anything as an [index] in your Arrays (aka Tables). Here's an example:
Lua:
MyArray = {} -- this is technically a table
MyArray["Hello"] = 100
MyArray[123456789] = 200

function Example()
    local u = CreateUnit(...)
    MyArray[u] = 300
    local p = Player(0)
    MyArray[p] = 400
end
I've stored different values using "Hello", 123456789, the handle id of the Unit I created, and Player 1, as different [indices].

To add even more onto this info dump, here's a nice way to design things -> OOP works well for all sorts of things. In this case, you could use it to expand a concept like Abilities. Nebula in this case could be an Object created from a "blueprint" that all Abilities share - which can remove a lot of the redundant code, make things MUI, and make variations of the same Ability a breeze.

Oh, and last but not least you're going to want to use both of these systems to save yourself from hours of headache:
 
Last edited:
You can see the coroutine approach at work here:

The author uses it to replace the TriggerSleepAction based wait functions in GUI. The result in theory allows much cleaner looking code than the conventional/classic spaghetti solution that Uncle posted above. I do not know how desync safe it is though, but I do not recall people complaining about it.
 
You can see the coroutine approach at work here:

The author uses it to replace the TriggerSleepAction based wait functions in GUI. The result in theory allows much cleaner looking code than the conventional/classic spaghetti solution that Uncle posted above. I do not know how desync safe it is though, but I do not recall people complaining about it.
As I understood it, avoiding Waits (like the plague) was not just an issue om imprecision, but also invalidating "getters" like Triggering Unit.
For example, one of the reasons why I got into JASS initially, was creating a "bloom" effect for area spells; meaning that effects move outwards smooth motion, rather than everywhere all at once. E.g.

  • Blood Fountain
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
      • (Ability being cast) Equal to Orc: Blood Fountain (Channel)
    • Actions
      • Set VariableSet Fountains_caster = (Triggering unit)
      • Set VariableSet Fountains_lvl = (Real((Level of (Ability being cast) for Fountains_caster)))
      • Set VariableSet Fountains_dmg = ((30.00 + (15.00 x Fountains_lvl)) + ((Real((Strength of Fountains_caster (Include bonuses)))) x 0.40))
      • Set VariableSet Fountains_heal = ((60.00 + (20.00 x Fountains_lvl)) + ((Real((Intelligence of Fountains_caster (Include bonuses)))) x 0.90))
      • Set VariableSet Fountains_center = (Position of Fountains_caster)
      • Set VariableSet Fountains_ang = 45.00
      • -------- Targeting --------
      • Set VariableSet Fountains_targetgroup = (Units within 175.00 of Fountains_center.)
      • For each (Integer A) from 1 to 4, do (Actions)
        • Loop - Actions
          • Set VariableSet Fountains_dist = 100.00
          • For each (Integer B) from 1 to 3, do (Actions)
            • Loop - Actions
              • Set VariableSet Fountains_tempp = (Fountains_center offset by Fountains_dist towards Fountains_ang degrees.)
              • Set VariableSet Fountains_tempug = (Units within 175.00 of Fountains_tempp.)
              • Unit Group - Add all units of Fountains_tempug to Fountains_targetgroup
              • Set VariableSet Fountains_dist = (Fountains_dist + 225.00)
              • Special Effect - Create a special effect at Fountains_tempp using Blood Explosion.mdx
              • Special Effect - Destroy (Last created special effect)
              • Custom script: call RemoveLocation(udg_Fountains_tempp)
              • Custom script: call DestroyGroup(udg_Fountains_tempug)
          • Set VariableSet Fountains_ang = (Fountains_ang + 90.00)
      • ...
        • Else - Actions
        • -------- Final cleanup --------
        • Custom script: call DestroyGroup(udg_Fountains_targetgroup)
        • Custom script: call RemoveLocation(udg_Fountains_center)
The above trigger creates all the "Fountain" effects pretty much instantly. Putting a wait function inside the loop (Loop B) can smooth out the animation, where it looks like it's "emnating" from the center.
But if multiple units tried to initiate the ability simultaneously... They would overwrite eachother, right? :confused:2

Hence why I went with JASS and timer-indexed hashtable handles.

Does Bribe's wait functions change anything about this? :vw_wtf:
 
As I understood it, avoiding Waits (like the plague) was not just an issue om imprecision, but also invalidating "getters" like Triggering Unit.
For example, one of the reasons why I got into JASS initially, was creating a "bloom" effect for area spells; meaning that effects move outwards smooth motion, rather than everywhere all at once. E.g.
As everything is contained within the same coroutine thread, you can store all "getters" in local variables at the start of the function. The smoothness is not an issue because these coroutine wait functions use timers internally so have the same precision as timers and events.

But if multiple units tried to initiate the ability simultaneously... They would overwrite eachother, right? :confused:2
Not if local variables were used to hold the state. Local variables are stored to the thread stack so offer unique storage locations for each thread. As each cast will spawn its own thread, this assures no race conditions or state corruption between casts.

This approach is not possible in Warcraft III JASS and GUI. JASS lacks coroutines while GUI (in Lua mode) lacks local variables to store the state. It is possible in StarCraft II GUI since GUI there does support local variables and although coroutines are not supported, the built in Wait function works as one would expect it to, and the approach is used frequently.

If you are not sure about the difference between a global and local variable, I recommend you familiarise yourself with it as these are core programming concepts you will find very useful.
 
Thanks for all the assistance! Getting started with a new language is always the hard part, as you adopt not only new syntax but often also new ways of thinking about each problem.

In interest of closing off this thread, here's my finished trigger. I've also used Bribe's system for initializing the trigger.
(Of course, it won't work without the GetAbilityField() function I have elsewhere in the map script, but I wanted to just show the lua in general)
Lua:
do

    local function Nebula_Q()
        -- Exit early if this is the wrong ability
        local abilId = GetSpellAbilityId()
        if abilId ~= FourCC("A003") then
            return
        end
 
        -- Getters
        local u = GetTriggerUnit()
        local x = GetSpellTargetX()
        local y = GetSpellTargetY()
        local lvl = GetUnitAbilityLevel(u, abilId) - 1
 
        -- Stats
        local dmg = GetAbilityField(FourCC('A003'), "herodur", lvl) / 2
        local aoe = GetAbilityField(FourCC('A003'), "area", lvl)
        local dur = 7.0
 
        -- Objects
        local ug = CreateGroup()
        local t = CreateTimer()
        local sfx = AddSpecialEffect("Fire Aura.mdx", x , y)
        -- Timers
        local tinterval = 0.5
        -- START --
        TimerStart(t, tinterval, true, function()
            -- Deal AoE damage
            GroupEnumUnitsInRange(ug, x, y, aoe, nil)
            ForGroup(ug, function()
                local enemy = GetEnumUnit()
                if IsUnitEnemy(u, GetOwningPlayer(enemy)) then
                    UnitDamageTarget(u, enemy, dmg, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_NORMAL, nil)
                end
            end)
  
            -- Star sprites --
            if (dur == 1 or dur==3 or dur==5) then
               -- create a "Star Sprite Crystal" at a random point in the aoe
                local new_x = x + aoe * (math.random()+math.random(-1,1))
                local new_y = y + aoe * (math.random()+math.random(-1,1))
                local sprite = CreateUnit(GetOwningPlayer(u), FourCC('o000'), new_x, new_y, 0)
                UnitApplyTimedLifeBJ(5.0, FourCC('BTLF'), sprite)
            end
            if (dur == 5) then
                -- the Special Effect takes ~2 seconds to stop animation, so we destroy it early
                DestroyEffect(sfx)
            end
          
            -- Try to end the spell
            dur = dur - tinterval
            if dur == 0 then
                DestroyGroup(ug)
                PauseTimer(t)
                DestroyTimer(t)
            end
        end)
        -- END --
    end
    local function CreateNebulaTrig()
        local tr = CreateTrigger()
        TriggerRegisterAnyUnitEventBJ(tr, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        TriggerAddAction(tr, Nebula_Q)
    end
    OnInit.trig(CreateNebulaTrig)
end
 
Ready for some annoying news? Real variables lose precision due to a fun thing called floating point error.

So these very specific checks are unsafe:
Lua:
            if (dur == 1 or dur==3 or dur==5) then
               -- create a "Star Sprite Crystal" at a random point in the aoe
                local new_x = x + aoe * (math.random()+math.random(-1,1))
                local new_y = y + aoe * (math.random()+math.random(-1,1))
                local sprite = CreateUnit(GetOwningPlayer(u), FourCC('o000'), new_x, new_y, 0)
                UnitApplyTimedLifeBJ(5.0, FourCC('BTLF'), sprite)
            end
            if (dur == 5) then
                -- the Special Effect takes ~2 seconds to stop animation, so we destroy it early
                DestroyEffect(sfx)
            end
Replace 'dur' with an Integer variable and double all of those values. You can also use modulo to simplify your "if dur == odd number" check.

Something like this could work:
Lua:
local dur = 7.0
local interval = 0.5
local totalTicks = math.floor(dur / interval + 0.5)
local tick = 0

TimerStart(t, interval, true, function()
    tick = tick + 1

    -- AoE damage etc. goes here --

    -- MODULO -> Every 2s, aka every 4 ticks
    if tick %% 4 == 0 then
        local new_x = x + aoe * (math.random() + math.random(-1,1))
        local new_y = y + aoe * (math.random() + math.random(-1,1))
        local sprite = CreateUnit(GetOwningPlayer(u), FourCC("o000"), new_x, new_y, 0)
        UnitApplyTimedLifeBJ(5.0, FourCC("BTLF"), sprite)
    end

    -- Stop SFX at 5 seconds
    -- 5s / 0.5 = 10th tick
    if tick == 10 then
        DestroyEffect(sfx)
    end

    if tick >= totalTicks then
        DestroyGroup(ug)
        PauseTimer(t)
        DestroyTimer(t)
    end
end)
Unsure if the "local totalTicks = math.floor(dur / interval + 0.5)" line works or not, might be smarter to just set it to 14.

Lastly, since you're using Lua and on the latest patch, Special Effects are a much better alternative to using Units as your art effects. Here's something ChatGPT whipped me up (forgive me, I couldn't find any examples from my own code and I am about to get off for the night). This is untested:
Lua:
do
    --============================================================--
    --  Special Effect Utility System
    --  Simple helpers to create, hide, destroy, and time-cleanup FX
    --============================================================--
 
    local FX = {}
    local HIDE_X = -99999 -- WILL CRASH IF TOO FAR OUT OF BOUNDS
    local HIDE_Y = -99999 -- WILL CRASH IF TOO FAR OUT OF BOUNDS
 
    -- Internal registry to avoid double-destroy
    local fx_alive = setmetatable({}, { __mode = "k" }) -- weak keys
 
    --------------------------------------------------------------
    -- Create an effect and optionally schedule timed destruction
    --------------------------------------------------------------
    function FX.Create(modelPath, x, y, z, lifetime)
        local eff = AddSpecialEffect(modelPath, x, y)
        if z then
            BlzSetSpecialEffectZ(eff, z)
        end
        fx_alive[eff] = true
 
        if lifetime and lifetime > 0 then
            FX.DestroyAfter(eff, lifetime)
        end
 
        return eff
    end
 
    --------------------------------------------------------------
    -- Destroy effect safely (no double destroy crash)
    --------------------------------------------------------------
    function FX.Destroy(eff)
        if fx_alive[eff] then
            DestroyEffect(eff)
            fx_alive[eff] = nil
        end
    end
 
    --------------------------------------------------------------
    -- Destroy after X seconds (uses a timer)
    --------------------------------------------------------------
    function FX.DestroyAfter(eff, seconds)
        if not fx_alive[eff] then return end
 
        local t = CreateTimer()
        TimerStart(t, seconds, false, function()
            FX.Destroy(eff)
            PauseTimer(t)
            DestroyTimer(t)
        end)
    end
 
    --------------------------------------------------------------
    -- Hide an effect by moving it out of bounds
    --------------------------------------------------------------
    function FX.Hide(eff)
        if not fx_alive[eff] then return end
        BlzSetSpecialEffectPosition(eff, HIDE_X, HIDE_Y, 5000)
    end
 
    --------------------------------------------------------------
    -- Hide then destroy after X seconds
    --------------------------------------------------------------
    function FX.HideThenDestroy(eff, seconds)
        FX.Hide(eff)
        FX.DestroyAfter(eff, seconds)
    end
 
    --------------------------------------------------------------
    -- Move effect to target over time (with a drift failsafe)
    -- Runs up to "maxIter" cycles (default 1000) preventing
    -- endless loops if coords get stuck due to float precision.
    --------------------------------------------------------------
    function FX.DriftTo(eff, targetX, targetY, targetZ, speed, tick, maxIter)
        if not fx_alive[eff] then return end
 
        speed   = speed   or 30.0
        tick    = tick    or 0.03125
        maxIter = maxIter or 1000
 
        local iter = 0
        local t = CreateTimer()
 
        TimerStart(t, tick, true, function()
            iter = iter + 1
            if iter > maxIter or not fx_alive[eff] then
                PauseTimer(t)
                DestroyTimer(t)
                return
            end
 
            local x = BlzGetLocalSpecialEffectX(eff)
            local y = BlzGetLocalSpecialEffectY(eff)
            local z = BlzGetLocalSpecialEffectZ(eff)
 
            local dx = targetX - x
            local dy = targetY - y
            local dist = dx*dx + dy*dy
 
            if dist <= 1.0 then -- close enough
                BlzSetSpecialEffectPosition(eff, targetX, targetY, targetZ or z)
                PauseTimer(t)
                DestroyTimer(t)
                return
            end
 
            local angle = math.atan(dy, dx)
            local nx = x + speed * math.cos(angle)
            local ny = y + speed * math.sin(angle)
 
            BlzSetSpecialEffectPosition(eff, nx, ny, targetZ or z)
        end)
    end
 
end
Here's an example of using it:
Lua:
local fx = FX.Create("Abilities\\Spells\\Orc\\Bloodlust\\BloodlustTarget.mdl", x, y, 50, 2.0)
The Bloodlust effect will have a Height of 50.0 and will be destroyed after 2.0 seconds.

For hiding effects, the idea is to set HIDE_X and HIDE_Y to some unused part of your map, like the "bounds" area. Just ensure it never goes OUT of bounds or the map will likely crash. This is very useful for Special Effects that have annoying or unnecessarily long Death animations, since you can make them disappear immediately by moving them offscreen.
 
Last edited:
Ready for some annoying news? Real variables lose precision due to a fun thing called floating point error.

Replace 'dur' with an Integer variable and double all of those values. You can also use modulo to simplify your "if dur == odd number" check.
[...]

Lastly, since you're using Lua and on the latest patch, Special Effects are a much better alternative to using Units as your art effects. Here's something ChatGPT whipped me up (forgive me, I couldn't find any examples from my own code and I am about to get off for the night). This is untested:
[...]

Always better to get the annoying news early, rather than later! Thanks for the heads-up.

W.r.t. using Special Effects instead of Units, I guess you're referring to the Sprites created? Those are supposed to be units; they have abilities and health bars and everything.
But yeah, I'm strictly using special effects. With the new functions for changing size, colour, Z-axis, etc. it's all good.
 
Back
Top