[GUI-Friendly] Knockup System v1.1b

=====================
GUI-Friendly Knockup System
=====================

Knockup System

GUI Wrappers

Examples

How to install

Credits

Changelog


Lua:
---@diagnostic disable: undefined-global
if Debug then Debug.beginFile 'KnockupSystem' end --[[
*************************************************************************************
*   ------------
*   version 1.1b
*   ------------
*
*   ------------
*   Description:
*   ------------
*   A simple knock-up system inspired by Mobile Legends & League of Legends.
*   By yours truly, Rheiko.
*
*   ---------
*   Features:
*   ---------
*   - Allows you to apply knockup effect on a unit
*   - Allows you to remove knockup effect from a unit
*   - Allows you to check if a unit is being knocked up
*   - Allows you to give knockup immunity to a unit
*   - Allows you to remove knockup immunity from a unit
*   - Allows you to check if a unit is immune to knockup effect
*   - Allows you to check the remaining time of knockup effect on a unit
*
*   ----
*   API:
*   ----
*   function ApplyKnockup(unit whichUnit, real height, real duration)
*     - Apply knockup effect on a unit
*     - If the unit is already airborne from a previous knockup, the current effect will be **overridden**.
*     - The new knockup will **blend smoothly** with the remaining height/time from the previous one, instead of resetting abruptly.
*     - You can also configure this behavior from global configuration. (No override/Always override/Only stronger effect override)
*
*   function RemoveKnockup(unit whichUnit) -> Returns a boolean value
*     - Remove knockup effect from a unit
*
*   function IsUnitKnockup(unit whichUnit) -> Returns a boolean value
*     - Check if the unit is being knocked up
*
*   function SetKnockupImmune(unit whichUnit, boolean flag) -> Returns a boolean value
*     - Grants a unit immunity against knockup effect
*
*   function IsKnockupImmune(unit whichUnit) -> Returns a boolean value
*     - Check if the unit is immune to knockup
*
*   function GetKnockupRemaining(unit whichUnit) -> Returns a real value
*     - Check the remaining time of knockup effect on a unit
*     - It will always return 0.0 if the unit is not airborne
*
*   --------------
*   Requirements:
*   --------------
*   Knockup System has no requirement whatsoever.
*
*   ----------
*   Optionals:
*   ----------
*   Wrda's PauseUnits (Link: https://www.hiveworkshop.com/threads/pauseunits.340457/)
*       This snippet helps preventing the target from being unpaused early.
*       Very useful if you use BlzPauseUnitEx for many things in your map
*       and not just for this system.
*
*       I highly recommend to use KnockupSystem along PauseUnits -- as it is the equivalent
*       of PauseUnitEx (vJASS) by MyPad -- for the best outcomes.
*
*   Credits to Wrda
*
*   -------------------
*   Import instruction:
*   -------------------
*   Simply copy and paste the Knockup System folder into your map. Easy Peasy Lemon Squeezy.
*   If you want to use the optional library, then simply import it as well.
*   But if you don't, you can simply delete it.
*
**************************************************************************************]]
do
    --======================
    -- System Configuration
    --======================

    ---Default duration value for knockup, used when the parameter value <= 0
    local DEFAULT_KNOCKUP_DURATION = 1.0

    ---Default height value for knockup, used when the parameter value <= 0
    local DEFAULT_KNOCKUP_HEIGHT = 150.0

    ---Max height value for knockup
    local MAX_KNOCKUP_HEIGHT = 500.0

    ---Effect attached on the target during "airborne" state
    local ATTACHMENT_EFFECT = "Abilities\\Spells\\Orc\\StasisTrap\\StasisTotemTarget.mdl"

    ---Unit attachment point (head, origin, overhead, etc)
    local ATTACHMENT_POINT = "overhead"

    ---Effect on the location of the target when they get launched
    local LAUNCH_EFFECT = ""

    ---Effect on the location of the target when they land
    local LANDING_EFFECT = ""

    --- -1 = no override (wait until land), 0 = always override, 1 = only stronger duration override
    local OVERRIDE_MODE = 1

    ---Timer interval used to update target unit fly height
    local TIMEOUT = .03125

    --======================
    -- End of Config
    --======================

    local TIMER ---@type timer

    ---@class KnockupInstance
    ---@field target unit
    ---@field duration number
    ---@field height number
    ---@field counter number
    ---@field isAirborne boolean
    ---@field initialHeight number
    ---@field baseHeight number
    ---@field deltaHeight number
    ---@field sfx effect
    local KnockupInstance = {}
    KnockupInstance.__index = KnockupInstance
    KnockupInstance.__name = 'KnockupInstance'

    ---@type table<unit, KnockupInstance>
    KnockupInstance._instances = {}

    ---@type KnockupInstance[]
    KnockupInstance._list = {}

    ---@type table<unit, integer>
    KnockupInstance._index = {}

    ---Creates a new instance or overrides the current one
    ---@param target unit
    ---@param height number
    ---@param duration number
    ---@return KnockupInstance|nil
    function KnockupInstance.create(target, height, duration)
        if target == nil or KnockupInstance.IsImmune(target) then
            return nil
        end

        ---@type KnockupInstance
        local existing = KnockupInstance._instances[target]
        local shouldOverride = false

        if existing then
            if OVERRIDE_MODE == 0 then
                shouldOverride = true
            elseif OVERRIDE_MODE == 1 and duration > (existing.duration - existing.counter) then
                shouldOverride = true
            elseif OVERRIDE_MODE == -1 then
                return existing
            end

            if shouldOverride then
                existing.duration = duration
                existing.counter = 0.0
                existing.initialHeight = GetUnitFlyHeight(target)
                existing.height = KnockupInstance.calculateApex(existing.initialHeight, height, existing.baseHeight)
                if existing.height > MAX_KNOCKUP_HEIGHT then
                    existing.height = MAX_KNOCKUP_HEIGHT
                end
            end

            return existing
        end

        ---@type KnockupInstance
        local new = setmetatable({}, KnockupInstance)
        new.target = target
        new.duration = duration
        new.isAirborne = true
        new.counter = 0.0
        new.initialHeight = GetUnitFlyHeight(target)
        new.baseHeight = GetUnitDefaultFlyHeight(target)

        new.height = KnockupInstance.calculateApex(new.initialHeight, height, new.baseHeight)

        if new.height > MAX_KNOCKUP_HEIGHT then
            new.height = MAX_KNOCKUP_HEIGHT
        end

        UnitAddAbility(target, FourCC('Amrf'))
        UnitRemoveAbility(target, FourCC('Amrf'))

        if PauseUnits ~= nil then
            PauseUnits.pauseUnit(target, true)
        else
            BlzPauseUnitEx(target, true)
        end

        ---Allows user to catch the event
        _G.udg_KnockupEventTarget = target     --Reminder to self: _G. to reference / set gui globals
        globals.udg_KnockupTakeoffEvent = 1.00 --Reminder to self: globals to make the real event works
        globals.udg_KnockupTakeoffEvent = 0.00
        _G.udg_KnockupEventTarget = target

        new.sfx = AddSpecialEffectTarget(ATTACHMENT_EFFECT, target, ATTACHMENT_POINT)
        if LAUNCH_EFFECT ~= "" then
            local x = GetUnitX(target)
            local y = GetUnitY(target)
            DestroyEffect(AddSpecialEffect(LAUNCH_EFFECT, x, y))
        end

        table.insert(KnockupInstance._list, new)
        KnockupInstance._index[target] = #KnockupInstance._list
        KnockupInstance._instances[target] = new

        if #KnockupInstance._list == 1 then
            TIMER = TIMER or CreateTimer()
            TimerStart(TIMER, TIMEOUT, true, KnockupInstance.loop)
        end

        return new
    end

    ---Loop with checks, immediately calls remove() when target dies or granted immunity
    function KnockupInstance.loop()
        local i = 1

        while i <= #KnockupInstance._list do
            local this = KnockupInstance._list[i]

            if not UnitAlive(this.target) or KnockupInstance.IsImmune(this.target) then
                KnockupInstance.remove(this.target)
            else
                this.counter = this.counter + TIMEOUT
                local t = this.counter / this.duration

                if t >= 1.0 then
                    SetUnitFlyHeight(this.target, this.baseHeight, 0)
                    this.isAirborne = false

                    if PauseUnits ~= nil then
                        PauseUnits.pauseUnit(this.target, false)
                    else
                        BlzPauseUnitEx(this.target, false)
                    end
                   

                    ---Allows user to catch the event
                    _G.udg_KnockupEventTarget = this.target
                    globals.udg_KnockupLandingEvent = 1.00
                    globals.udg_KnockupLandingEvent = 0.00
                    _G.udg_KnockupEventTarget = nil

                    DestroyEffect(this.sfx)
                    this.sfx = nil

                    if LANDING_EFFECT ~= "" then
                        local x = GetUnitX(this.target)
                        local y = GetUnitY(this.target)
                        DestroyEffect(AddSpecialEffect(LANDING_EFFECT, x, y))
                    end

                    KnockupInstance._instances[this.target] = nil
           
                    local index = KnockupInstance._index[this.target]
                    KnockupInstance._index[this.target] = nil

                    if index then
                        local lastIndex = #KnockupInstance._list
                        if index ~= lastIndex then
                            local lastInst = KnockupInstance._list[lastIndex]
                            KnockupInstance._list[index] = lastInst
                            KnockupInstance._index[lastInst.target] = index
                        end
                        KnockupInstance._list[lastIndex] = nil
                    end

                    -- Stop loop if no active knockups
                    if #KnockupInstance._list == 0 then
                        PauseTimer(TIMER)
                    end
                else
                    local a = (1.0 - t)
                    local b = t
                    this.deltaHeight = a * a * this.initialHeight + 2.0 * a * b * this.height + b * b * this.baseHeight
                    SetUnitFlyHeight(this.target, this.deltaHeight, 0)

                    i = i + 1
                end
            end
        end
    end

    ---Remove the instance
    function KnockupInstance.remove(target)
        local instance = KnockupInstance._instances[target]
        if instance then
            SetUnitFlyHeight(target, instance.baseHeight, 0)
            instance.isAirborne = false

            if PauseUnits ~= nil then
                PauseUnits.pauseUnit(target, false)
            else
                BlzPauseUnitEx(target, false)
            end

            if UnitAlive(target) then
                ---Allows user to catch the event
                _G.udg_KnockupEventTarget = target
                globals.udg_KnockupCancelledEvent = 1.00
                globals.udg_KnockupCancelledEvent = 0.00
                _G.udg_KnockupEventTarget = nil
            end

            DestroyEffect(instance.sfx)
            instance.sfx = nil

            if LANDING_EFFECT ~= "" then
                local x = GetUnitX(target)
                local y = GetUnitY(target)
                DestroyEffect(AddSpecialEffect(LANDING_EFFECT, x, y))
            end

            KnockupInstance._instances[target] = nil
           
            local index = KnockupInstance._index[target]
            KnockupInstance._index[target] = nil

            if index then
                local lastIndex = #KnockupInstance._list
                if index ~= lastIndex then
                    local lastInst = KnockupInstance._list[lastIndex]
                    KnockupInstance._list[index] = lastInst
                    KnockupInstance._index[lastInst.target] = index
                end
                KnockupInstance._list[lastIndex] = nil
            end

            -- Stop loop if no active knockups
            if #KnockupInstance._list == 0 then
                PauseTimer(TIMER)
            end

            return true
        end

        return false
    end

    ---@param init number
    ---@param peak number
    ---@param base number
    function KnockupInstance.calculateApex (init, peak, base)
        return (init + peak) * 2.0 - 0.5 * (init + base)
    end

    ---@type table<unit, boolean>
    KnockupInstance._immune = {}

    function KnockupInstance.SetImmune(unit, flag)
        if flag then
            KnockupInstance._immune[unit] = true
        else
            KnockupInstance._immune[unit] = nil
        end
    end

    function KnockupInstance.IsImmune(unit)
        return KnockupInstance._immune[unit] == true
    end

    function KnockupInstance.GetRemaining(unit)
        local inst = KnockupInstance._instances[unit]
        if inst then
            return inst.duration - inst.counter
        end
        return 0.0
    end

    --======================
    -- API
    --======================

    ---@param target unit
    ---@param height number
    ---@param duration number
    ---@return boolean -- true if applied successfully
    ApplyKnockup = function(target, height, duration)
        if height <= 0. then
            height = DEFAULT_KNOCKUP_HEIGHT
        end

        if height > MAX_KNOCKUP_HEIGHT then
            height = MAX_KNOCKUP_HEIGHT
        end

        if duration <= 0. then
            duration = DEFAULT_KNOCKUP_DURATION
        end

        return KnockupInstance.create(target, height, duration) ~= nil
    end

    ---@param target unit
    ---@return boolean -- true if applied successfully
    RemoveKnockup = function(target)
        return KnockupInstance.remove(target) ~= nil
    end

    ---@param target unit
    ---@return boolean
    IsUnitKnockup = function(target)
        local inst = KnockupInstance._instances[target]
        return inst and inst.isAirborne or false
    end

    ---@param target unit
    ---@param flag boolean
    SetKnockupImmune = function(target, flag)
        KnockupInstance.SetImmune(target, flag)
    end

    ---@param target unit
    ---@return boolean
    IsKnockupImmune = function(target)
        return KnockupInstance.IsImmune(target)
    end

    ---@param target unit
    ---@return number
    GetKnockupRemaining = function(target)
        return KnockupInstance.GetRemaining(target)
    end
end

if Debug then Debug.endFile() end
vJASS:
library KnockupSystem /* version 1.1
*************************************************************************************
*
*   ------------
*   Description:
*   ------------
*   A simple knock-up system inspired by Mobile Legends & League of Legends.
*   By yours truly, Rheiko.
*
*   ---------
*   Features:
*   ---------
*   - Allows you to apply knockup effect on a unit
*   - Allows you to remove knockup effect from a unit
*   - Allows you to check if a unit is being knocked up
*   - Allows you to give knockup immunity to a unit
*   - Allows you to remove knockup immunity from a unit
*   - Allows you to check if a unit is immune to knockup effect
*   - Allows you to check the remaining time of knockup effect on a unit
*
*   ----
*   API:
*   ----
*   function ApplyKnockup(unit whichUnit, real height, real duration)
*     - Apply knockup effect on a unit
*     - If the unit is already airborne from a previous knockup, the current effect will be **overridden**.
*     - The new knockup will **blend smoothly** with the remaining height/time from the previous one, instead of resetting abruptly.
*     - You can also configure this behavior from global configuration. (No override/Always override/Only stronger effect override)
*
*   function RemoveKnockup(unit whichUnit) -> Returns a boolean value
*     - Remove knockup effect from a unit
*
*   function IsUnitKnockup(unit whichUnit) -> Returns a boolean value
*     - Check if the unit is being knocked up
*
*   function SetKnockupImmune(unit whichUnit, boolean flag) -> Returns a boolean value
*     - Grants a unit immunity against knockup effect
*
*   function IsKnockupImmune(unit whichUnit) -> Returns a boolean value
*     - Check if the unit is immune to knockup
*
*   function GetKnockupRemaining(unit whichUnit) -> Returns a real value
*     - Check the remaining time of knockup effect on a unit
*     - It will always return 0.0 if the unit is not airborne
*
*   --------------
*   */ requires /*
*   --------------
*   Knockup System has no requirement whatsoever.
*
*   --------------------------
*   */ optional PauseUnitEx /*
*   --------------------------
*   This snippet helps preventing the target from being unpaused early.
*   Very useful if you use BlzPauseUnitEx for many things in your map
*   and not just for this system.
*
*   I highly recommend to use KnockupSystem along PauseUnitEx for the best outcomes.
*
*   Credits to MyPad
*   Link: https://www.hiveworkshop.com/threads/pauseunitex.326422/
*
*   -------------------
*   Import instruction:
*   -------------------
*   Simply copy and paste the Knockup System folder into your map. Easy Peasy Lemon Squeezy.
*   If you want to use the optional library, then simply import it as well.
*   But if you don't, you can simply delete it.
*
*   ---------------------
*   Global configuration:
*   ---------------------
*/
    globals     
        /*
            Default duration value for knockup, used when the parameter value <= 0
        */
        private constant real DEFAULT_KNOCKUP_DURATION = 1.0
        /*
            Default height value for knockup, used when the parameter value <= 0
        */
        private constant real DEFAULT_KNOCKUP_HEIGHT = 150.0
        /*
            Max height value for knockup
        */
        private constant real MAX_KNOCKUP_HEIGHT = 500.0
        /*
            Effect attached on the target during "airborne" state
        */
        private constant string ATTACHMENT_EFFECT = "Abilities\\Spells\\Orc\\StasisTrap\\StasisTotemTarget.mdl"
        /*
            Unit attachment point (head, origin, overhead, etc)
        */
        private constant string ATTACHMENT_POINT = "overhead"
        /*
            Effect on the location of the target when they get launched
        */
        private constant string LAUNCH_EFFECT = ""
   
        /*
            Effect on the location of the target when they land
        */
        private constant string LANDING_EFFECT = ""
        /*
            -1 = no override (wait until land), 0 = always override, 1 = only stronger duration override
        */
        private constant integer OVERRIDE_MODE = 1
        /*
            Timer interval used to update target unit fly height
        */
        private constant real TIMEOUT = .03125
       
        /* -------------
            END OF CONFIG
            -------------
    */endglobals
    globals
        // Initialize hashtable
        private hashtable TABLE = InitHashtable()
        // Initialize timer
        private timer TIMER = CreateTimer()
    endglobals
    native UnitAlive takes unit id returns boolean
    function ApplyKnockup takes unit u, real height, real duration returns nothing
        if height <= 0. then
            set height = DEFAULT_KNOCKUP_HEIGHT
        endif
        if height > MAX_KNOCKUP_HEIGHT then
            set height = MAX_KNOCKUP_HEIGHT
        endif
        if duration <= 0. then
            set duration = DEFAULT_KNOCKUP_DURATION
        endif
        call KnockupInstance.create(u, height, duration)
    endfunction
    function RemoveKnockup takes unit u returns boolean
        return KnockupInstance.Remove(u)
    endfunction
    function IsUnitKnockup takes unit u returns boolean
        return KnockupInstance.IsKnockup(u)
    endfunction
    function SetKnockupImmune takes unit u, boolean flag returns boolean
        return KnockupInstance.SetImmune(u, flag)
    endfunction
    function IsKnockupImmune takes unit u returns boolean
        return KnockupInstance.IsImmune(u)
    endfunction
    function GetKnockupRemaining takes unit u returns real
        return KnockupInstance.GetRemaining(u)
    endfunction
    private module LinkedList
        thistype next
        thistype prev
       
        static method insert takes thistype this returns nothing
            set this.next = 0
            set this.prev = thistype(0).prev
            set thistype(0).prev.next = this
            set thistype(0).prev = this
        endmethod
        static method pop takes thistype this returns nothing
            set this.next.prev = this.prev
            set this.prev.next = this.next
        endmethod
    endmodule
    struct KnockupInstance  
        implement LinkedList
        boolean isAirborne
        real height
        real duration
        real deltaHeight
        real initialHeight
        real baseHeight
        real counter
        unit target
        effect sfx
        static method IsKnockup takes unit u returns boolean
            local integer key = GetHandleId(u)
            local integer index = LoadInteger(TABLE, 0, key)
            if index != 0 then
                return KnockupInstance(index).isAirborne
            endif
            return false       
        endmethod
        static method GetRemaining takes unit u returns real
            local integer index = LoadInteger(TABLE, 0, GetHandleId(u))
           
            if not (index == 0) then
                if not thistype(index).isAirborne then
                    return 0.0
                endif
                return thistype(index).duration - thistype(index).counter
            endif
            return 0.0
        endmethod
        static method SetImmune takes unit u, boolean flag returns boolean
            call SaveBoolean(TABLE, 1, GetHandleId(u), flag)
            return true
        endmethod
        static method IsImmune takes unit u returns boolean
            return LoadBoolean(TABLE, 1, GetHandleId(u))
        endmethod
        private static method CalculateApex takes real init, real peak, real base returns real
            return (init + peak) * 2.0 - 0.5 * (init + base)
        endmethod
        static method Remove takes unit u returns boolean
            local thistype this
            local integer unitId = GetHandleId(u)
            local integer index = LoadInteger(TABLE, 0, unitId)
            local real x
            local real y
            if not (index == 0) then
                set this = index
               
                if this.isAirborne then
                    set this.isAirborne = false
                    call SetUnitFlyHeight(this.target, this.baseHeight, 0)
                    call RemoveSavedInteger(TABLE, 0, unitId)
                    static if LIBRARY_PauseUnitEx then
                        call PauseUnitEx(this.target, false)
                    else
                        call BlzPauseUnitEx(this.target, false)
                    endif
                    if UnitAlive(this.target) then
                        // Allows user to catch the event
                        set udg_KnockupEventTarget = this.target
                        set udg_KnockupCancelledEvent = 1.00
                        set udg_KnockupCancelledEvent = 0.00
                        set udg_KnockupEventTarget = null
                    endif
                    call DestroyEffect(this.sfx)
                    set this.sfx = null
                    call this.pop(this)
                    call this.destroy()
           
                    if thistype(0).next == 0 then
                        call PauseTimer(TIMER)
                    endif
                    return true
                endif
            endif
            return false
        endmethod
        private static method Loop takes nothing returns nothing
            local thistype this = thistype(0).next
            local real t
            local real a
            local real b
            local real x
            local real y
            loop
                exitwhen this == 0
                if IsImmune(this.target) or not UnitAlive(this.target) then
                    call KnockupInstance.Remove(this.target)
                else
               
                    set this.counter = this.counter + TIMEOUT
                    set t = this.counter / this.duration
                    if t >= 1.0 then
                        call SetUnitFlyHeight(this.target, this.baseHeight, 0)
                        set this.isAirborne = false
                        call RemoveSavedInteger(TABLE, 0, GetHandleId(this.target))
                        set x = GetUnitX(this.target)
                        set y = GetUnitY(this.target)
                   
                        static if LIBRARY_PauseUnitEx then
                            call PauseUnitEx(this.target, false)
                        else
                            call BlzPauseUnitEx(this.target, false)
                        endif
                        // Allows user to catch the event
                        set udg_KnockupEventTarget = this.target
                        set udg_KnockupLandingEvent = 1.00
                        set udg_KnockupLandingEvent = 0.00
                        set udg_KnockupEventTarget = null
   
                        call DestroyEffect(this.sfx)
                        set this.sfx = null
                        if LANDING_EFFECT != "" then
                            call DestroyEffect(AddSpecialEffect(LANDING_EFFECT, x, y))
                        endif
                        call this.pop(this)
                        call this.destroy()
                        if thistype(0).next == 0 then
                            call PauseTimer(TIMER)
                        endif
                        set this = this.next
                    else
                        set a = (1.0 - t)
                        set b = t
                        set this.deltaHeight = a * a * this.initialHeight + 2.0 * a * b * this.height + b * b * this.baseHeight
                        call SetUnitFlyHeight(this.target, this.deltaHeight, 0)
                    endif
                    // debug call BJDebugMsg("Current Fly Height of " + GetUnitName(this.target) + ":" + R2S(GetUnitFlyHeight(this.target)) )
               
                endif
                set this = this.next
            endloop
        endmethod
        static method create takes unit target, real height, real duration returns thistype
            local integer key = GetHandleId(target)
            local thistype existing = LoadInteger(TABLE, 0, key)
            local thistype this
            local real x
            local real y
            local boolean shouldOverride          
            if target == null or IsImmune(target) then
                return 0
            endif
            if not (existing == 0) then
                if OVERRIDE_MODE == 0 then
                    debug call BJDebugMsg("DEBUG | Override always")
                    set shouldOverride = true              
                elseif OVERRIDE_MODE == 1 and duration > existing.duration - existing.counter then
                    debug call BJDebugMsg("DEBUG | Override only if stronger. Remaining duration: " + R2S(existing.duration - existing.counter) + " vs new: " + R2S(duration))
                    set shouldOverride = true
                elseif OVERRIDE_MODE == -1 then
                    debug call BJDebugMsg("DEBUG | No Override")
                    return existing
                endif
                if shouldOverride then
                    set existing.duration = duration
                    set existing.counter = 0.0
                    set existing.initialHeight = GetUnitFlyHeight(existing.target)
                    set existing.height = CalculateApex(existing.initialHeight, height, existing.baseHeight) 
                    if existing.height > MAX_KNOCKUP_HEIGHT then
                        set existing.height = MAX_KNOCKUP_HEIGHT
                    endif
                endif
                return existing
            endif
            set this = allocate()
            call this.insert(this)
            set this.target = target
            set this.duration = duration
            set this.isAirborne = true
            set this.counter = 0.0
            set this.initialHeight = GetUnitFlyHeight(this.target)
            set this.baseHeight = GetUnitDefaultFlyHeight(this.target)
            set this.height = CalculateApex(this.initialHeight, height, this.baseHeight)
            if this.height > MAX_KNOCKUP_HEIGHT then
                set this.height = MAX_KNOCKUP_HEIGHT
            endif
            call UnitAddAbility(this.target, 'Amrf')
            call UnitRemoveAbility(this.target, 'Amrf')
            set x = GetUnitX(this.target)
            set y = GetUnitY(this.target)
            static if LIBRARY_PauseUnitEx then
                call PauseUnitEx(this.target, true)
            else
                call BlzPauseUnitEx(this.target, true)
            endif
            // Allows user to catch the event
            set udg_KnockupEventTarget = this.target
            set udg_KnockupTakeoffEvent = 1.00
            set udg_KnockupTakeoffEvent = 0.00
            set udg_KnockupEventTarget = null
            set this.sfx = AddSpecialEffectTarget(ATTACHMENT_EFFECT, this.target, ATTACHMENT_POINT)
           
            if LAUNCH_EFFECT != "" then
                call DestroyEffect(AddSpecialEffect(LAUNCH_EFFECT, x, y))
            endif
            call SaveInteger(TABLE, 0, key, this)
            // debug call BJDebugMsg("Current Fly Height of " + GetUnitName(this.target) + ":" + R2S(GetUnitFlyHeight(this.target)) )
           
            if thistype(0).next == this then
                call TimerStart(TIMER, TIMEOUT, true, function thistype.Loop)
            endif
            return this
        endmethod
        static method onInit takes nothing returns nothing
            set thistype(0).next = thistype(0)
            set thistype(0).prev = thistype(0)
        endmethod
    endstruct
endlibrary

Lua:
---@diagnostic disable: undefined-global
if Debug then Debug.beginFile 'KnockupSystemGUI' end
--[[
*************************************************************************************
*
*   ------------------
*   Knockup System GUI
*   ------------------
*   This is a library containing GUI Wrappers for Knockup System by Rheiko.
*   Intended for GUIers.
*
*   --------
*   Requires
*   -------------
*   KnockupSystem
*   -------------
*
*   --------------
*   How to import:
*   --------------
*   1. You need to first import KnockupSystem library
*   2. Copy the GUI API folder to your map
*   3. You are done!
*
*   -----------
*   How to use:
*   -----------
*   - Fill the variables that have _param prefix
*     i.e: GKS_Param_Flag (boolean), GKS_Param_Height (real), GKS_Param_Target (unit), GKS_Param_Duration (real)
*
*   - Run the trigger you want
*     i.e:
*         - GKS_ApplyKnockup (requirements: GKS_Param_Target, GKS_Param_Height, GKS_Param_Duration)
*           Run this trigger to apply knockup effect on a unit
*
*         - GKS_RemoveKnockup (requirements: GKS_Param_Target)
*           Run this trigger to remove knockup effect from a unit
*
*         - GKS_CheckKnockup (requirements: GKS_Param_Target)
*           Run this trigger to check if a unit is being knocked up
*
*         - GKS_CheckKnockupImmunity (requirements: GKS_Param_Target)
*           Run this trigger to check if a unit is immune to knockup effects
*
*         - GKS_SetKnockupImmunity (requirements: GKS_Param_Target, GKS_Param_Flag)
*           Run this trigger to grant or remove knockup immunity from a unit
*
*         - GKS_GetRemainingTime (requirements: GKS_Param_Target)
*           Run this trigger to get the remaining time of knockup effect on a unit
*
*
*   - Some that has return value will be stored inside these variables
*     i.e:
*         - GKS_IsUnitKnockup (boolean)
*           Response to GKS_CheckKnockup trigger
*
*         - GKS_IsKnockupImmune (boolean)
*           Response to GKS_CheckKnockupImmunity trigger
*
*         - GKS_RemainingTime (real)
*           Response to GKS_GetRemainingTime trigger
*
*    Note: Please check the GUI examples to understand further!
*
*************************************************************************************]]
do
    function GUI_GetRemainingTime ()
        udg_GKS_RemainingTime = GetKnockupRemaining(udg_GKS_Param_Target)
    end
    function GUI_SetKnockupImmunity ()
        SetKnockupImmune(udg_GKS_Param_Target, udg_GKS_Param_Flag)
    end
    function GUI_CheckKnockupImmunity ()
        udg_GKS_IsKnockupImmune = IsKnockupImmune(udg_GKS_Param_Target)
    end
    function GUI_CheckKnockup ()
        udg_GKS_IsUnitKnockup = IsUnitKnockup(udg_GKS_Param_Target)
    end
    function GUI_RemoveKnockup ()
        RemoveKnockup(udg_GKS_Param_Target)
    end
    function GUI_ApplyKnockup ()
        ApplyKnockup(udg_GKS_Param_Target, udg_GKS_Param_Height, udg_GKS_Param_Duration)
    end
 
    OnInit(function()
        udg_GKS_ApplyKnockup = CreateTrigger()
        udg_GKS_RemoveKnockup = CreateTrigger()
        udg_GKS_CheckKnockup = CreateTrigger()
        udg_GKS_CheckKnockupImmunity = CreateTrigger()
        udg_GKS_SetKnockupImmunity = CreateTrigger()
        udg_GKS_GetRemainingTime = CreateTrigger()
        TriggerAddAction(udg_GKS_ApplyKnockup, GUI_ApplyKnockup)
        TriggerAddAction(udg_GKS_RemoveKnockup, GUI_RemoveKnockup)
        TriggerAddAction(udg_GKS_CheckKnockup, GUI_CheckKnockup)
        TriggerAddAction(udg_GKS_CheckKnockupImmunity, GUI_CheckKnockupImmunity)
        TriggerAddAction(udg_GKS_SetKnockupImmunity, GUI_SetKnockupImmunity)
        TriggerAddAction(udg_GKS_GetRemainingTime, GUI_GetRemainingTime)
    end)
end
if Debug then Debug.endFile() end
vJASS:
scope KnockupSystemGUI initializer Init /*
*************************************************************************************
*
*   ------------------
*   Knockup System GUI
*   ------------------
*   This is a library containing GUI Wrappers for Knockup System by Rheiko.
*   Intended for GUIers.
*
*   --------
*   Requires
*   -------------
*   KnockupSystem
*   -------------
*
*   --------------
*   How to import:
*   --------------
*   1. You need to first import KnockupSystem library
*   2. Copy the GUI API folder to your map
*   3. You are done!
*
*   -----------
*   How to use:
*   -----------
*   - Fill the variables that have _param prefix
*     i.e: GKS_Param_Flag (boolean), GKS_Param_Height (real), GKS_Param_Target (unit), GKS_Param_Duration (real)
*
*   - Run the trigger you want
*     i.e:
*         - GKS_ApplyKnockup (requirements: GKS_Param_Target, GKS_Param_Height, GKS_Param_Duration)
*           Run this trigger to apply knockup effect on a unit
*
*         - GKS_RemoveKnockup (requirements: GKS_Param_Target)
*           Run this trigger to remove knockup effect from a unit
*
*         - GKS_CheckKnockup (requirements: GKS_Param_Target)
*           Run this trigger to check if a unit is being knocked up
*
*         - GKS_CheckKnockupImmunity (requirements: GKS_Param_Target)
*           Run this trigger to check if a unit is immune to knockup effects
*
*         - GKS_SetKnockupImmunity (requirements: GKS_Param_Target, GKS_Param_Flag)
*           Run this trigger to grant or remove knockup immunity from a unit
*
*         - GKS_GetRemainingTime (requirements: GKS_Param_Target)
*           Run this trigger to get the remaining time of knockup effect on a unit
*
*
*   - Some that has return value will be stored inside these variables
*     i.e:
*         - GKS_IsUnitKnockup (boolean)
*           Response to GKS_CheckKnockup trigger
*
*         - GKS_IsKnockupImmune (boolean)
*           Response to GKS_CheckKnockupImmunity trigger
*
*         - GKS_RemainingTime (real)
*           Response to GKS_GetRemainingTime trigger
*
*    Note: Please check the GUI examples to understand further!
*
*************************************************************************************
*/
    private function GUI_GetRemainingTime takes nothing returns nothing
        set udg_GKS_RemainingTime = GetKnockupRemaining(udg_GKS_Param_Target)
    endfunction

    private function GUI_SetKnockupImmunity takes nothing returns nothing
        call SetKnockupImmune(udg_GKS_Param_Target, udg_GKS_Param_Flag)
    endfunction

    private function GUI_CheckKnockupImmunity takes nothing returns nothing
        set udg_GKS_IsKnockupImmune = IsKnockupImmune(udg_GKS_Param_Target)
    endfunction

    private function GUI_CheckKnockup takes nothing returns nothing
        set udg_GKS_IsUnitKnockup = IsUnitKnockup(udg_GKS_Param_Target)
    endfunction

    private function GUI_RemoveKnockup takes nothing returns nothing
        call RemoveKnockup(udg_GKS_Param_Target)
    endfunction

    private function GUI_ApplyKnockup takes nothing returns nothing
        call ApplyKnockup(udg_GKS_Param_Target, udg_GKS_Param_Height, udg_GKS_Param_Duration)
    endfunction

    private function Init takes nothing returns nothing
        set udg_GKS_ApplyKnockup = CreateTrigger()
        set udg_GKS_RemoveKnockup = CreateTrigger()
        set udg_GKS_CheckKnockup = CreateTrigger()
        set udg_GKS_CheckKnockupImmunity = CreateTrigger()
        set udg_GKS_SetKnockupImmunity = CreateTrigger()
        set udg_GKS_GetRemainingTime = CreateTrigger()

        call TriggerAddAction(udg_GKS_ApplyKnockup, function GUI_ApplyKnockup)
        call TriggerAddAction(udg_GKS_RemoveKnockup, function GUI_RemoveKnockup)
        call TriggerAddAction(udg_GKS_CheckKnockup, function GUI_CheckKnockup)
        call TriggerAddAction(udg_GKS_CheckKnockupImmunity, function GUI_CheckKnockupImmunity)
        call TriggerAddAction(udg_GKS_SetKnockupImmunity, function GUI_SetKnockupImmunity)
        call TriggerAddAction(udg_GKS_GetRemainingTime, function GUI_GetRemainingTime)
    endfunction

endscope

Example 1

Example 2

Example 3

Example 4

Example 5

Custom Takeoff Effect

Custom Landing Effect

Airborne Cancelled Effect


  • Test
    • Events
      • Unit - A unit Starts the effect of an ability
    • Conditions
      • (Ability being cast) Equal to Thunder Clap
    • Actions
      • Set TempPoint = (Position of (Triggering unit))
      • Custom script: set bj_wantDestroyGroup = true
      • Unit Group - Pick every unit in (Units within (250.00 + (50.00 x (Real((Level of Thunder Clap for (Triggering unit)))))) of TempPoint) and do (Actions)
        • Loop - Actions
          • Set TempUnit = (Picked unit)
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • (TempUnit is dead) Equal to False
              • (TempUnit belongs to an enemy of (Triggering player)) Equal to True
              • (TempUnit is A structure) Equal to False
            • Then - Actions
              • -------- - --------
              • -------- Apply a knockup effect on a unit --------
              • -------- - --------
              • Set GKS_Param_Target = TempUnit
              • Set GKS_Param_Height = 150.00
              • Set GKS_Param_Duration = 0.80
              • Trigger - Run GKS_ApplyKnockup (ignoring conditions)
            • Else - Actions
      • Custom script: call RemoveLocation(udg_TempPoint)

  • Test2
    • Events
      • Player - Player 1 (Red) skips a cinematic sequence
    • Conditions
    • Actions
      • Custom script: set bj_wantDestroyGroup = true
      • Unit Group - Pick every unit in (Units in (Playable map area)) and do (Actions)
        • Loop - Actions
          • Set TempUnit = (Picked unit)
          • -------- - --------
          • -------- Check if a unit is being knocked up --------
          • -------- - --------
          • Set GKS_Param_Target = TempUnit
          • Trigger - Run GKS_CheckKnockup (ignoring conditions)
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • GKS_IsUnitKnockup Equal to True
            • Then - Actions
              • Game - Display to (All players) the text: ((Name of TempUnit) + is getting knocked up!!!)
            • Else - Actions
          • -------- - --------
          • -------- Check if a unit is immune from knockup effects --------
          • -------- - --------
          • Set GKS_Param_Target = TempUnit
          • Trigger - Run GKS_CheckKnockupImmunity (ignoring conditions)
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • GKS_IsKnockupImmune Equal to True
            • Then - Actions
              • Game - Display to (All players) the text: ((Name of TempUnit) + is immune from getting knocked up!!!)
            • Else - Actions
          • -------- - --------

  • Test3
    • Events
      • Player - Player 1 (Red) Selects a unit
    • Conditions
    • Actions
      • -------- - --------
      • -------- Remove a knockup effect from a unit --------
      • -------- - --------
      • Set GKS_Param_Target = (Triggering unit)
      • Trigger - Run GKS_RemoveKnockup (ignoring conditions)

  • Test4
    • Events
      • Player - Player 1 (Red) types a chat message containing -i as An exact match
      • Player - Player 1 (Red) types a chat message containing -u as An exact match
      • Player - Player 1 (Red) types a chat message containing -t as An exact match
    • Conditions
    • Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • (Entered chat string) Equal to -i
        • Then - Actions
          • Custom script: set bj_wantDestroyGroup = true
          • Unit Group - Pick every unit in (Units currently selected by Player 1 (Red)) and do (Actions)
            • Loop - Actions
              • Set TempUnit = (Picked unit)
              • -------- - --------
              • -------- Grant immunity to picked unit --------
              • -------- - --------
              • Set GKS_Param_Target = TempUnit
              • Set GKS_Param_Flag = True
              • Trigger - Run GKS_SetKnockupImmunity (ignoring conditions)
              • -------- - --------
              • Game - Display to (All players) the text: ((Name of TempUnit) + is now immune to knockup effects!)
        • Else - Actions
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • (Entered chat string) Equal to -u
            • Then - Actions
              • Custom script: set bj_wantDestroyGroup = true
              • Unit Group - Pick every unit in (Units currently selected by Player 1 (Red)) and do (Actions)
                • Loop - Actions
                  • Set TempUnit = (Picked unit)
                  • -------- - --------
                  • -------- Release immunity from picked unit --------
                  • -------- - --------
                  • Set GKS_Param_Target = TempUnit
                  • Set GKS_Param_Flag = False
                  • Trigger - Run GKS_SetKnockupImmunity (ignoring conditions)
                  • -------- - --------
                  • Game - Display to (All players) the text: ((Name of TempUnit) + is no longer immune to knockup effects!)
            • Else - Actions
              • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                • If - Conditions
                  • (Entered chat string) Equal to -t
                • Then - Actions
                  • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                    • If - Conditions
                      • (Test5 <gen> is on) Equal to True
                    • Then - Actions
                      • Trigger - Turn off Test5 <gen>
                      • Game - Display to (All players) the text: Periodic Timer to c...
                    • Else - Actions
                      • Trigger - Turn on Test5 <gen>
                      • Game - Display to (All players) the text: Periodic Timer to c...
                • Else - Actions

  • Test5
    • Events
      • Time - Every 0.03 seconds of game time
    • Conditions
    • Actions
      • Custom script: set bj_wantDestroyGroup = true
      • Unit Group - Pick every unit in (Units in (Playable map area)) and do (Actions)
        • Loop - Actions
          • Set TempUnit = (Picked unit)
          • -------- - --------
          • -------- Check if a unit is being knocked up --------
          • -------- - --------
          • Set GKS_Param_Target = TempUnit
          • Trigger - Run GKS_CheckKnockup (ignoring conditions)
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • GKS_IsUnitKnockup Equal to True
            • Then - Actions
              • -------- - --------
              • -------- Check the remaining time of the knockup effect --------
              • -------- - --------
              • Trigger - Run GKS_GetRemainingTime (ignoring conditions)
              • Game - Display to (All players) the text: (Knockup Remaining Time for + ((Name of TempUnit) + ( is + (String(GKS_RemainingTime)))))
            • Else - Actions

  • Custom Takeoff
    • Events
      • Game - KnockupTakeoffEvent becomes Equal to 1.00
    • Conditions
    • Actions
      • Set TempPoint = (Position of KnockupEventTarget)
      • Special Effect - Create a special effect at TempPoint using Abilities\Spells\Human\Feedback\ArcaneTowerAttack.mdl
      • Special Effect - Destroy (Last created special effect)
      • Custom script: call RemoveLocation(udg_TempPoint)

  • Custom Landing
    • Events
      • Game - KnockupLandingEvent becomes Equal to 1.00
    • Conditions
    • Actions
      • Set TempPoint = (Position of KnockupEventTarget)
      • Special Effect - Create a special effect at TempPoint using Abilities\Spells\Human\ThunderClap\ThunderClapCaster.mdl
      • Special Effect - Destroy (Last created special effect)
      • Custom script: call RemoveLocation(udg_TempPoint)

  • Airborne Cancelled
    • Events
      • Game - KnockupCancelledEvent becomes Equal to 1.00
    • Conditions
    • Actions
      • Special Effect - Create a special effect attached to the origin of KnockupEventTarget using Abilities\Spells\Human\DispelMagic\DispelMagicTarget.mdl
      • Special Effect - Destroy (Last created special effect)


The steps are quite straightforward and I make sure it is as simple as it needs to be:
  • Copy and paste the folder Knockup System into your map
  • If you are a GUI user, make sure to copy and paste the folder GUI API into your map as well
  • You are done!
There is an optional library for both vJASS and Lua version, they are:
  • PauseUnitEx by MyPad (vJASS)
  • PauseUnits by Wrda (Lua)
You can delete the script file if you choose not to use them. But if you do, this one is for Lua version, make sure you do this:
  • Create a new custom defend ability in object editor
  • Register the ability ID in PauseUnits script file. You can look for this line: local CUSTOM_DEFEND = FourCC('A000') -- Your custom defend ability
  • Change 'A000' to the Ability ID / rawcode of your custom defend ability in object editor

  • Thanks to MyPad for his PauseUnitEx snippet.
  • Thanks to Wrda for his PauseUnits snippet.

v1.1b
  • Removed an unnecessary line of code.
  • Adjusted the code for timer creation, it's no longer in Lua root.
v1.1
  • Added optional library (PauseUnitEx by MyPad) to vJASS version
  • Added optional library (PauseUnits by Wrda) to Lua version
  • Added GUI Custom Event Support (KnockupTakeoffEvent, KnockupLandingEvent, KnockupCancelledEvent)
  • Added GUI Event Response (KnockupEventTarget)
v1.0
  • First public release

Git: Wc3KnockupSystem
Previews
Contents

Knockup System Lua v1.1b (Map)

Knockup System vJASS v1.1 (Map)

Reviews
Antares
A nice light-weight system. Everything seems to be working as intended. One way to make it more flavorful maybe could be to add a debuff upon landing that reduces strength and agility over time. Approved

Rheiko

Spell Reviewer
Level 26
Joined
Aug 27, 2013
Messages
4,214
My first ever vJASS resource! Please do let me know if you find bugs or anything I can improve upon.
I am sure there are lots of them. (not the bugs, ofc!)

Also, just in case any of you are about to ask me why I didn't use Table, I actually wanted to. I tried to implement it but I got lost in the process so I kinda gave up. I will probably try again for the future update. Help me pls.

That's it for now, enjoy! Cheers!
 

Rheiko

Spell Reviewer
Level 26
Joined
Aug 27, 2013
Messages
4,214
Updated to v1.1
  • Added optional library (PauseUnitEx by MyPad) to vJASS version
  • Added optional library (PauseUnits by Wrda) to Lua version
  • Added GUI Custom Event Support (KnockupTakeoffEvent, KnockupLandingEvent, KnockupCancelledEvent)
  • Added GUI Event Response (KnockupEventTarget)
Also updated the description with new examples on how to use the Custom Events as well as how to install.

If you're using this system in your map, I highly recommend to use this newer version as you are now allowed to customize whatever you want to happen during takeoff, landing, or when the knockup is cancelled midair.

The optional libraries are very much recommended in order to reduce the possibility of stun effect being removed earlier than it should as they have internal counters. Thanks to Wrda and MyPad for the awesome snippets!

Have a great day, cheers!
 
Bump

Added Lua version. First Lua resource as well!
Mind checking it, @Antares ?
I'm not too sure since I'm not very familiar with the language.

Feedbacks are welcomed!
Lua:
 Require.optional 'PauseUnits'
This line doesn't do anything. You cannot yield from the Lua root. If you want to make it so that PauseUnit can be below your system in the map script, you have to set PauseUnits in an OnInit function. However, there's no reason to do any of this at all. You can just check if the global exists at runtime.

Lua:
    local TIMER = CreateTimer()
Creating a timer in the Lua root might be fine here, but it should generally be avoided. You can just do
Lua:
TIMER = TIMER or CreateTimer()
before you start it.
 

Rheiko

Spell Reviewer
Level 26
Joined
Aug 27, 2013
Messages
4,214
This line doesn't do anything. You cannot yield from the Lua root. If you want to make it so that PauseUnit can be below your system in the map script, you have to set PauseUnits in an OnInit function. However, there's no reason to do any of this at all. You can just check if the global exists at runtime.
Understood, that line shall be removed.

Should I just check the global right away without saving them to local first? Since I kinda did this in my code
Lua:
local PauseUnits = _G.PauseUnits

I could just do this right?
Lua:
if _G.PauseUnits ~= nil then
    PauseUnits.pauseUnit(target, true)
else
    BlzPauseUnitEx(target, true)
end

...Creating a timer in the Lua root might be fine here, but it should generally be avoided. You can just do
...before you start it.
So, is something like this correct?
Lua:
local TIMER =~ nil ---@type timer
function ()
    TIMER = TIMER or CreateTimer()
    TimerStart(TIMER, ...)
end

and Thanks for checking the code!
 
Top