• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] Basic Knockback

Status
Not open for further replies.
Level 14
Joined
Feb 7, 2020
Messages
386
Jump to Latest Version

Couldn't find a KB system that can be copy-pasted in Lua maps without a lot of tweaking (still working on getting a translated KB2.5D to work) so I made this as a resource I was going to submit (and as a learning exercise). I'm not a Lua expert, but it works so far :)

However, having jumped into it... there's lots of madness in figuring out pathing in the WC3 engine. Other people have already figured it out. Lua is cool but so many resources already exist in JASS.

If you need KB functionality in a Lua map, this might be useful.

The general skeleton is built around extensible functionality that you could inject as needed. There are some demo spells in the map attached that try to highlight this approach. I haven't quite finished the grand scope of it, but the general idea is there. I think a metatable could better handle the config defaults but I didn't know about them at the time.

edit: fixed the butchered code paste; now is pretty


Requires: Global Initialization by Bribe, Unit Indexer by Bribe (Lua Version), Timer Utils by Bribe
Included in attached map for easy copy + paste (copy the whole folder).

Lua:
--[[-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
   :: a very simple KB system that moves the unit in a direction over time, extensible with custom data and function arguments.
   :: by Planetary @hiveworkshop.com :: CC0 :: no credit required.
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --]]

LUA_KB_DATA     = {}  -- where we store global data for KB instances.
LUA_KB_CONFIG   = {}  -- default configuration data.

LUA_KB_CONFIG.collisionSize         = 32.0          -- x,y offset to check for walkable pathability.
LUA_KB_CONFIG.tickRate              = 0.03          -- KB timer update period. recommend to keep at .03 minimum or bugs may occur. higher values = less accurate collision.
LUA_KB_CONFIG.destroyDest           = true          -- should destructables be destroyed? if false, meeting a destructable will count as a collision using collisionSize.
LUA_KB_CONFIG.destPlayEffect        = true          -- if a destructable is destroyed, should a special effect be played? plays LUA_KB_CONFIG.effect.
LUA_KB_CONFIG.destCheckSize         = 192.0         -- rect width/height which checks for destructables to destroy.
LUA_KB_CONFIG.tickFailsafe          = 256           -- overly-cautious hard-break cap; if this number of timer completions is exceeded, the KB will end.
LUA_KB_CONFIG.effectTickRate        = 6             -- how often the special effect occurs (1 tick = .03 sec) to control for screen quality and efficiency.
LUA_KB_CONFIG.abilCode              = FourCC('Arav')-- crow ability that simulates no collision (added and removed on update).
LUA_KB_CONFIG.effect                = 'Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl'    -- the special effect to use under the KB unit.
LUA_KB_CONFIG.destEffect            = LUA_KB_CONFIG.effect                                                          -- special effect to use on destroyed destructables.

--[[-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

      readme (if not needed, delete to save on file size):

      an example use of a basic KB as a custom script: BasicKnockback(udg_tempunit, udg_tempangle, 300, true, .66, nil, nil)
      alternatively, a basic KB shorthand would read as: BasicKnockback(udg_tempunit, udg_tempangle, 300, true, .66)

      an example use of a more complex KB with custom data: BasicKnockback(udg_tempunit, udg_tempangle, 300, true, .66, nil, customData)
      where customData = { ['collisionSize'] = 44, ['effect'] = 'Objects\\Spawnmodels\\Naga\\NagaDeath\\NagaDeath.mdl' }

      how would you use custom data?
      :: let's say you want a knockback that has a certain velocity and distance based on custom RPG stats: you could override the base KB calcs with
      :: raw values that you calculate and pass in via customData (kb.velocity and kb.distance).
      :: your table would look something like (placeholder math): local myTable = { ['velocity'] = udg_myAgilityStat*.1, ['distance'] = udg_myStrengthStat*10 }

      any piece of kb data can be overwritten with the customData argument. however, these are the key calculated points you may want to alter:
      :: tickRate
      :: velocity
      :: distance
      :: destroyDest
      :: destPlayEffect
      :: destEffect
      :: angle
   
      kb data reference:
      :: kb.collisionSize     int     = radius to detect collision with terrain.
      :: kb.tickRate          real    = KB periodic update cadence.
      :: kb.destroyDest       bool    = should destructables be destroyed.
      :: kb.destPlayEffect    bool    = should an effect be played on destroyed destructables?
      :: kb.destEffect        str     = effect to play on destructables (defaults to kb effect; overwrite with custom data)
      :: kb.destCheckSize     int     = radius to detect destructables to destroy.
      :: kb.tickFailsafe      int     = max timer updates before a force exit.
      :: kb.effectTickRate    real    = how often the special effect plays (every X tickRate updates).
      :: kb.abilCode          ability = ability code added to unit that removes collision.
      :: kb.effect            str     = effect played under KB unit.
      :: kb.rect              rect    = rect to determine destructables in range.
      :: kb.unit              real    = KB unit.
      :: kb.unitX             real    = KB unit current X.
      :: kb.unitY             real    = KB unit current Y.
      :: kb.distance          real    = remaining distance to travel for KB unit.
      :: kb.angle             real    = trajectory angle.
      :: kb.velocity          real    = distance traveled per tickRate.
      :: kb.velocityOffset    real    = collisionSize offset from current velocity.
      :: kb.pathCheckX        real    = coord for pathing check at velocityOffset.
      :: kb.pathCheckY        real    = coord for pathing check at velocityOffset.
      :: kb.ticks             int     = total tickRate completions.
      :: kb.update            bool    = force existing timer to update to a new KB instance.

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --]]

-- @KBunit = unit to kb.
-- @KBdirectionAngle = angle of KB movement.
-- @KBdistance = distance to travel.
-- @KBpause = should it pause the unit.
-- @KBduration = (seconds) how long it takes to get from point A to B. determines velocity; velocity = (KBdistance/KBduration)/(1.0/kb.tickRate)
-- @KBcollisionFunc = [optional] function to pass that does things to the unit when they collide with terrain. pass 'id' as an argument e.g. myFunc(id).
-- @KBcustomData = [optional] = override any 'kb' data (e.g. 'velocity', 'effect', etc.) with a data table, using kb data index values as the lookup.
function BasicKnockback(KBunit, KBdirectionAngle, KBdistance, KBpause, KBduration, KBcollisionFunc, KBcustomData)

    if (not IsUnitType(KBunit, UNIT_TYPE_STRUCTURE) and not IsUnitType(KBunit, UNIT_TYPE_ANCIENT)) then -- KB system hard cancellations.
        local id = GetUnitUserData(KBunit)      -- fetch unit index data.
        local kb = {}                           -- temp table for readability.

        if (LUA_KB_DATA[id] == nil) then        -- if no KB instance is active, instantiate one.
            LUA_KB_DATA[id] = {}
        else
            LUA_KB_DATA[id].update = true       -- flag the kb timer to overwrite its local data with the new KB instance.
            kb = LUA_KB_DATA[id]                -- with a global table, we can simply overwrite existing KB timer data for a new trajectory.
        end

        if (KBcustomData ~= nil) then -- if custom data is passed, assign where matches occur.
            for index, value in pairs(LUA_KB_CONFIG) do
                if ( DoesTableContainIndex(KBcustomData,index) ) then
                    kb[index] = KBcustomData[index]
                else
                    kb[index] = value -- assigns kb.collisionSize, kb.effect, etc.
                end
            end
        else -- custom data does not exist, initialize defaults
            for index, value in pairs(LUA_KB_CONFIG) do
                kb[index] = value -- assigns kb.collisionSize, kb.effect, etc.
            end
        end

        if (not kb.rect) then kb.rect = Rect(0, 0, kb.destCheckSize, kb.destCheckSize) end -- create destructables check rect if one doesn't already exist.
        kb.unit = KBunit
        kb.unitX = GetUnitX(KBunit)
        kb.unitY = GetUnitY(KBunit)
        kb.distance = KBdistance
        kb.angle = KBdirectionAngle
        kb.velocity = (KBdistance/KBduration)/(1.0/kb.tickRate) -- convert tickrate timer to distance traveled per sec (velocity).
        kb.velocityOffset = kb.velocity + kb.collisionSize -- precalc pathing/dest check for efficiency.
        kb.pathCheckX,kb.pathCheckY = PolarProjectionXY(kb.unitX,kb.unitY,kb.velocityOffset,kb.angle) -- check for pathing and destructables.
        kb.ticks = 0 -- the number of times the timer has completed (to control special effect cadence and the forced-break safety limit).

        if (KBcollisionFunc ~= nil) then
            kb.collisionFunc = KBcollisionFunc  -- if the collision function exists, initialize it.
        else
            kb.collisionFunc = nil      -- default to nil.
        end

        if (KBpause) then
            PauseUnit(kb.unit,true)
        end

         -- if custom data is passed for calculation overwrites, assign the new values.
         -- since few overwrites are expected, we allow inefficient replacement.
        if (KBcustomData ~= nil) then
            for index, value in pairs(KBcustomData) do
                if ( DoesTableContainIndex(kb,index) ) then
                    kb[index] = value
                end
            end
        end

        LUA_KB_DATA[id] = kb    -- update the global kb instance data.

        -- -----------------------------------------------------------------------------------------------------------------------------------------------
        -- [[ KB TIMER ]] --------------------------------------------------------------------------------------------------------------------------------
        -- -----------------------------------------------------------------------------------------------------------------------------------------------

        if (kb.timer == nil and not LUA_KB_DATA[id].update) then -- if no timer exists, generate one.

            kb.timer = TimerStart(NewTimer(),kb.tickRate,true,function()

                if (LUA_KB_DATA[id].update) then    -- if an overwrite event occurred, update the local timer data.
                    kb = LUA_KB_DATA[id]
                    LUA_KB_DATA[id].update = false  -- allow future updates again.
                end

                kb.pathCheckX,kb.pathCheckY = PolarProjectionXY(kb.unitX,kb.unitY,kb.velocityOffset,kb.angle) -- increment offset to check for upcoming pathing check.
                kb.ticks = kb.ticks + 1

                if ( IsTerrainPathable(kb.pathCheckX,kb.pathCheckY,PATHING_TYPE_WALKABILITY) and not IsUnitType(kb.unit,UNIT_TYPE_FLYING) ) then -- check pathing.
                    kb.hitTerrainBool = true
                    if (kb.collisionFunc ~= nil) then
                        kb.collisionFunc(id)
                    end
                end

                -- run appropriate checks to destroy the KB instance:
                if (not IsUnitType(kb.unit, UNIT_TYPE_DEAD) and not kb.hitTerrainBool and kb.distance > 0 and kb.ticks < kb.tickFailsafe ) then

                    -- ---------------------------- --
                    -- [[ run effect, move unit  ]] --
                    -- ---------------------------- --
                    BasicKnockbackCheckDest(kb.pathCheckX,kb.pathCheckY,id)

                    kb.distance = kb.distance - kb.velocity
                    kb.unitX,kb.unitY = PolarProjectionXY(kb.unitX,kb.unitY,kb.velocity,kb.angle)

                    if (ModuloInteger(kb.ticks,kb.effectTickRate) == 0) then -- limit special effect creation for performance.
                        DestroyEffect(AddSpecialEffect(kb.effect,kb.unitX,kb.unitY))
                    end

                    UnitAddAbility(kb.unit,kb.abilCode)
                    SetUnitX(kb.unit,kb.unitX)
                    SetUnitY(kb.unit,kb.unitY)
                    UnitRemoveAbility(kb.unit,kb.abilCode)
                    -- ---------------------------- --
                    -- ---------------------------- --

                else -- end effect
                    if (IsUnitPaused(kb.unit)) then
                        PauseUnit(kb.unit,false)
                    end
                    RemoveRect(kb.rect)     -- cleanup
                    kb = nil                -- cleanup
                    LUA_KB_DATA[id] = nil   -- cleanup
                    ReleaseTimer()
                end

            end)
        end

        -- -----------------------------------------------------------------------------------------------------------------------------------------------
        -- [[ END KB TIMER ]] ----------------------------------------------------------------------------------------------------------------------------
        -- -----------------------------------------------------------------------------------------------------------------------------------------------
    end

end


-- @x,y = check this coord.
-- @id = for this KB instance.
function BasicKnockbackCheckDest(x,y,id)
    MoveRectTo(LUA_KB_DATA[id].rect,x,y)
    if (LUA_KB_DATA[id].destroyDest) then
        EnumDestructablesInRect(LUA_KB_DATA[id].rect, nil, function() BasicKnockbackDest(id) end)
    else
        EnumDestructablesInRect(LUA_KB_DATA[id].rect, nil, function() BasicKnockbackDestCollision(id) end)
    end
end


-- destroy enum destructables.
function BasicKnockbackDest(id)
    bj_destRandomCurrentPick = GetEnumDestructable() -- for efficiency, hook a disposable blizzard global.
    if (GetWidgetLife(bj_destRandomCurrentPick) > .405) then -- add any desired special conditions.
        if (LUA_KB_DATA[id].destPlayEffect and LUA_KB_DATA[id].destEffect ~= nil) then
            DestroyEffect(AddSpecialEffect( LUA_KB_DATA[id].destEffect,
                GetWidgetX(bj_destRandomCurrentPick),
                GetWidgetY(bj_destRandomCurrentPick) ) )
        end
        SetWidgetLife(bj_destRandomCurrentPick,0)
    end
end


-- find out if there is a nearby destructable to collide with.
function BasicKnockbackDestCollision(id)
    bj_destRandomCurrentPick = GetEnumDestructable() -- for efficiency, hook a disposable blizzard global.
    if (bj_destRandomCurrentPick ~= nil and GetWidgetLife(bj_destRandomCurrentPick) > .405) then
        local x = GetWidgetX(bj_destRandomCurrentPick)
        local y = GetWidgetY(bj_destRandomCurrentPick)
        if (IsUnitInRangeXY(LUA_KB_DATA[id].unit,x,y,LUA_KB_DATA[id].collisionSize)) then -- only stop if collisionSize met.
            LUA_KB_DATA[id].hitTerrainBool = true   -- set the flag to end the KB instance.
            LUA_KB_DATA[id].update = true           -- pass in the flag to update the running timer.
        end
    end
end


-- :: PolarProjectionBJ converted to x,y.
-- @x1,y1 = origin coord.
-- @d = distance between origin and direction.
-- @a = angle to project.
function PolarProjectionXY(x1,y1,d,a)
    local x = x1 + d * Cos(a * bj_DEGTORAD)
    local y = y1 + d * Sin(a * bj_DEGTORAD)

    return x,y
end


-- :: helper function to check if table has index value.
-- @t = table to search.
-- @v = value to search for, returns true if it exists; false if not.
function DoesTableContainIndex(t,v)
    for index, value in pairs(t) do
        if index == v then
            return true
        end
    end
    return false
end
 
Last edited:
Level 14
Joined
Feb 7, 2020
Messages
386
Jump to Latest Version

Update (v2.0):
I forgot I had reworked this into a cleaner metatable class a while ago. Looks like it continues to work in 1.34!

It did not change functionality or gain features, but is easier to use and configure for spells/effects. There are new functions to bootstrap fast spell creation (single unit or group, player-owned group, or pull; and you can set duration). I recall there were a few bug fixes.

It is primitive, but might be a useful prototype for a Lua project which desires the feature.

(It includes a custom unit indexer and group utils bundle, if you also desire those).

Lua:
function demokb1()
    local unit   = GetTriggerUnit()
    local x, y   = utils.unitxy(unit)
    local grp    = g:newbyxy(utils.powner(unit), g_type_dmg, x, y, 300.0)
    kb:new_group(grp, x, y)
    grp:destroy()
end


function demokb2()
    local x, y   = utils.unitxy(GetTriggerUnit())
    local x2, y2 = utils.unitxy(GetSpellTargetUnit())
    local angle  = utils.anglexy(x, y, x2, y2)
    local k = kb:new(GetSpellTargetUnit(), angle, 1200, 1.25)
    k.destroydest = false
    k.colfunc = function(k)
        k.interrupt = true
        kb:new(k.unit, k.angle - math.random(160,200), math.floor(k.dist*0.9), 1.25)
    end
end
Lua:
do
    kb = {
        --[[
            configurable:
        --]]
        colradius   = 32.0,     -- detect terrain collision
        tickrate    = 0.03,     -- timer cadence.
        defaultvel  = 20,       -- default velocity for basic kb calls.
        defaultdist = 500,      -- default distance ``.
        destroydest = true,     -- destroy dustructibles? (note: system collides with invul dest.).
        destradius  = 192.0,    -- search this range for dest.
        destplayeff = true,     -- play effect on dest. collision?
        efftickrate = 6,        -- stagger effect creation.
        colfunc     = nil,      -- run on collision when set (NOTE: do not recursively run kb here, update existing one instead).
        updfunc     = nil,      -- `` on update.
        interrupt   = false,    -- pass this flag to force-cancel a kb instance.
        effect      = 'Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl',
        desteff     = 'Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl',
        abil        = FourCC('Arav'),
        debug       = false,
        --[[
            non-configurable:
        --]]
        total       = 0,
        stack       = {},
    }
    kb.rect    = Rect(0, 0, kb.destradius, kb.destradius)
    kb.tmr     = NewTimer()
    kb.__index = kb
end


-- @unit    = unit to knock back.
-- @angle   = towards this angle.
-- [@_dist] = set a distance.
-- [@_dur]  = set a duration (required with @_dist!).
function kb:new(unit, angle, _dist, _dur)
    if unit and not IsUnitType(unit, UNIT_TYPE_STRUCTURE) then
        local o = setmetatable({}, self)
        o.udex  = GetUnitUserData(unit)
        o.unit  = unit
        o.angle = angle
        o.dist  = _dist or kb.defaultdist
        o.dur   = _dur or nil
        o.tick  = 0
        o.trav  = 0
        if IsUnitType(unit, UNIT_TYPE_FLYING) then
            o.isflying = true
        end
        kb.stack[o] = o
        if kb.total == 0 then
            TimerStart(kb.tmr, kb.tickrate, true, function()
                utils.debugfunc(function()
                    for _,obj in pairs(kb.stack) do
                        obj:update()
                    end
                end, "kb update")
                if kb.debug then print("running timer") end
            end)
        end
        kb.total = kb.total + 1
        if kb.debug then print("total kb instances: "..kb.total) end
        o:init()
        return o
    end
end


-- @unit    = unit to knock back.
-- @angle   = towards this angle.
-- @x,y     = from this origin coord.
-- [@_dist] = set a distance.
-- [@_dur]  = set a duration (required with @_dist!).
function kb:new_origin(unit, x, y, _dist, _dur)
    return kb:new(unit, utils.anglexy(x, y, GetUnitX(unit), GetUnitY(unit), _dist, _dur))
end


-- @grp     = unit group to knock back.
-- @x,y     = from this origin coord.
function kb:new_group(grp, x, y)
    grp:action(function()
        kb:new(grp.unit, utils.anglexy(x, y, GetUnitX(grp.unit), GetUnitY(grp.unit)))
    end)
end


function kb:init()
    self.x = GetUnitX(self.unit)
    self.y = GetUnitY(self.unit)
    if self.dur and self.dist then
        self.vel = (self.dist/self.dur)/(1.0/self.tickrate)
    else
        self.vel = self.defaultvel
    end
    self.tickmax = math.floor(self.dist/self.vel)
    if self.pause then
        PauseUnit(self.unit, true)
    end
    if kb.debug then
        print("init debug:")
        print("self.unit = "..tostring(self.unit))
        print("self.x = "..self.x)
        print("self.y = "..self.y)
        print("self.vel = "..self.vel)
        print("self.dist = "..self.dist)
        print("self.tickmax = "..self.tickmax)
    end
end


function kb:update()
    if self:collision() then
        self:destroy()
        return
    else
        self.tick = self.tick + 1
    end
    if self.updfunc then
        self.updfunc(self)
    end
    if not self.interrupt and utils.isalive(self.unit) and self.tick < self.tickmax and self.trav < self.dist then
        self.trav = self.trav + self.vel
        self.x, self.y = utils.projectxy(self.x, self.y, self.vel, self.angle)
        -- special effect staggered play for performance/aesthetic:
        if ModuloInteger(self.tick, self.efftickrate) == 0 then
            DestroyEffect(AddSpecialEffect(self.effect, self.x, self.y))
        end
        -- add/remove storm crow for pathing:
        UnitAddAbility(self.unit, self.abil)
        utils.setxy(self.unit, self.x, self.y)
        UnitRemoveAbility(self.unit, self.abil)
    else
        self.delete = true
        self:destroy()
    end
    if kb.debug and ModuloInteger(self.tick, 21) then
        print("attempting to update for unit index "..self.udex)
        print("self.unit = "..tostring(self.unit))
        print("self.x = "..self.x)
        print("self.y = "..self.y)
        print("self.vel = "..self.vel)
        print("self.dist = "..self.dist)
        print("self.tickmax = "..self.tickmax)
    end
end


function kb:destroy()
    -- hook self.delete to force remove or prevent removal:
    if self.delete then
        if self.pause then
            PauseUnit(self.unit, false)
        end
        for i,v in pairs(self) do
            v = nil
            i = nil
        end
        kb.stack[self] = nil
        kb.total = kb.total - 1
        if kb.total <= 0 then
            kb.total = 0
            PauseTimer(kb.tmr)
        end
        if kb.total < 10 then
            -- recycle indeces when size is low:
            utils.tablecollapse(kb.stack)
        end
    elseif kb.debug then
        print("caution: self.delete hooked, instance not removed")
    end
    if kb.debug then print("total kb instances: "..kb.total) end
end


function kb:collision()
    if not self.isflying and (self:terraincollision() or self:destcollision()) then
        -- hook delete in collision func to prevent deletion, etc.
        self.delete = true
        if self.colfunc then
            self.colfunc(self)
        end
        return true
    end
    return false
end


function kb:terraincollision()
    self.offsetcheck = self.vel + self.colradius
    self.offsetx, self.offsety = utils.projectxy(self.x, self.y, self.offsetcheck, self.angle)
    if IsTerrainPathable(self.offsetx, self.offsety, PATHING_TYPE_WALKABILITY) then
        return true
    end
    return false
end


function kb:destcollision()
    self:updaterect(self.offsetx, self.offsety)
    if self.destroydest then
        EnumDestructablesInRect(kb.rect, nil, function() self:hitdestdestroy() end)
    else
        EnumDestructablesInRect(kb.rect, nil, function() self:hitdestcollide() end)
    end
    if self.destcol then
        return true
    else
        return false
    end
end


-- destroy destructables.
function kb:hitdestdestroy()
    bj_destRandomCurrentPick = GetEnumDestructable()
    -- check destructable life to ignore any that are set to invulnerable:
    if GetWidgetLife(bj_destRandomCurrentPick) > 0.405 and GetWidgetLife(bj_destRandomCurrentPick) < 99999.0 then
        if self.destplayeff and self.desteff then
            DestroyEffect(AddSpecialEffect(self.desteff, GetWidgetX(bj_destRandomCurrentPick), GetWidgetY(bj_destRandomCurrentPick)))
        end
        SetWidgetLife(bj_destRandomCurrentPick, 0)
    elseif GetWidgetLife(bj_destRandomCurrentPick) > 99998.0 then
        self.destcol = true
    end
end


function kb:updaterect(x, y)
    local w = self.destradius/2
    SetRect(kb.rect, x - w, y - w, x + w, y + w)
end


-- find out if there is a nearby destructable to collide with.
function kb:hitdestcollide()
    bj_destRandomCurrentPick = GetEnumDestructable()
    if bj_destRandomCurrentPick and GetWidgetLife(bj_destRandomCurrentPick) > 0.405 then
        if IsUnitInRangeXY(
            self.unit,
            GetWidgetX(bj_destRandomCurrentPick),
            GetWidgetY(bj_destRandomCurrentPick),
            self.destradius)
        then
            self.destcol = true
        end
    end
end


function kb:reset()
    -- e.g. for recursive updates of a kb instance.
    self.trav    = 0
    self.tickmax = 0
    self.tick    = 0
    self.delete  = false
end


local oldglobals = InitGlobals
function InitGlobals()
    utils.debugfunc(function()
        oldglobals()
        g:init()
    end, "InitGlobals")
end
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
This looks really interesting/good.

Could you explain more what's going on with the "destructable life" thing? Are bridges/floating platforms set to that kind of HP? If so, then that's a way saner approach than what I have seen/used in Knockback systems before.

The bj_destRandomCurrentPick thing is a little out of place. You can just wrap your whole script in a do/end block and just use a local "d" that points to it.

I am not sure why your assignments to kb are wrapped in a do...end block at the top, but it currently functions exactly the same even without them.

'kb' is not really a great name for the public API. I would propose Knockback. You can use a local variable 'kb' if you prefer it for simplicity, and then just set Knockback = kb at the bottom of your script declarations in order to declare it as a global.

You may want to look into Emmy Annotation syntax. You would change '-- @type = description' to '---@param type name description'.

The API for 'utils' such as "debug" is missing from being included here, but you've obviously got them somewhere in your map. It would be nice if you could please share them here as well, so that it's possible to analyze them without needing to use World Editor.

I am not sure where 'g' is being defined, but your InitGlobals block seems to think it's important.
 
Level 14
Joined
Feb 7, 2020
Messages
386
Thanks for the feedback.

I reworked the names, removed arbitrary/unused stuff, and added Emmy notation for VS Code.

As for the life comparisons, I can't quite recall. I think I researched that a widget will be enumerated when dead, and it has a minimum life value (.405 or something close). I added an invulnerable check to the knockback code, but left the life ceiling in (I can't remember why I did that, but I remember it caused an issue in one of my live projects). I'll leave it in for now and make it configurable with a note.

v2.1 attached

Lua:
-- Knockback v2.1 by Planetary @ hiveworkshop.com
-- A very primitive knockback system for bootstrapping Lua maps in WC3 Reforged.
-- Latest game version supported: 1.34

Knockback = {
    --configurable:
    colradius   = 32.0,     -- detect terrain collision
    tickrate    = 0.03,     -- timer cadence.
    defaultvel  = 20,       -- default velocity for basic kb calls.
    defaultdist = 500,      -- default distance ``.
    destroydest = true,     -- destroy dustructibles? (note: destructibles that should be invulnerable should have their life set very high e.g. 999,999.).
    destplayeff = true,     -- play effect on dest. collision?
    destradius  = 192.0,    -- search this range for dest.
    destmaxlife = 99999.0,  -- destructibles will only die if they have less than this life amount (else, they are considered invulnerable).
    efftickrate = 6,        -- stagger effect creation.
    colfunc     = nil,      -- run on collision when set (NOTE: do not recursively run kb here, update existing one instead).
    updfunc     = nil,      -- `` on update.
    interrupt   = false,    -- pass this flag to force-cancel a kb instance.
    effect      = 'Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl',
    desteff     = 'Abilities\\Weapons\\AncientProtectorMissile\\AncientProtectorMissile.mdl',
    abil        = FourCC('Arav'),
    --non-configurable:
    total       = 0,
    stack       = {},
    r_dest      = nil,
    -- misc:
    debug       = false,    -- will spam debug information at you during kb update timers.
}
Knockback.rect    = Rect(0, 0, Knockback.destradius, Knockback.destradius)
Knockback.tmr     = NewTimer()
Knockback.__index = Knockback


--- initiate a knockback instance for a single unit.
---@param unit unit     -- target unit
---@param angle number    -- direction
---@param _dist? number   -- distance
---@param _dur? number    -- duration to complete over @_dist
---@return table        -- the knockback instance
function Knockback:new(unit, angle, _dist, _dur)
    if unit and not IsUnitType(unit, UNIT_TYPE_STRUCTURE) then
        local o = setmetatable({}, self)
        o.udex  = GetUnitUserData(unit)
        o.unit  = unit
        o.angle = angle
        o.dist  = _dist or Knockback.defaultdist
        o.dur   = _dur or nil
        o.tick  = 0
        o.trav  = 0
        if IsUnitType(unit, UNIT_TYPE_FLYING) then
            o.isflying = true
        end
        Knockback.stack[o] = o
        if Knockback.total == 0 then
            TimerStart(Knockback.tmr, Knockback.tickrate, true, function()
                debugfunc(function()
                    for _,obj in pairs(Knockback.stack) do
                        obj:update()
                    end
                end, "kb update")
                if Knockback.debug then print("running timer") end
            end)
        end
        Knockback.total = Knockback.total + 1
        if Knockback.debug then print("total kb instances: "..Knockback.total) end
        o:init()
        return o
    end
end


--- initiate a knockback for a target unit from an origin point (pushing them away from that origin)
---@param unit unit
---@param x number
---@param y number
---@param _dist? number
---@param _dur? number
---@return table
function Knockback:new_origin(unit, x, y, _dist, _dur)
    return Knockback:new(unit, anglexy(x, y, GetUnitX(unit), GetUnitY(unit)), _dist, _dur)
end


--- initiate knockback for a group of units
---@param grp UnitGroup
---@param x number
---@param y number
function Knockback:new_group(grp, x, y)
    grp:action(function()
        Knockback:new(grp.unit, anglexy(x, y, GetUnitX(grp.unit), GetUnitY(grp.unit)))
    end)
end


function Knockback:init()
    self.x = GetUnitX(self.unit)
    self.y = GetUnitY(self.unit)
    if self.dur and self.dist then
        self.vel = (self.dist/self.dur)/(1.0/self.tickrate)
    else
        self.vel = self.defaultvel
    end
    self.tickmax = math.floor(self.dist/self.vel)
    if self.pause then
        PauseUnit(self.unit, true)
    end
    if Knockback.debug then
        print("init debug:")
        print("self.unit = "..tostring(self.unit))
        print("self.x = "..self.x)
        print("self.y = "..self.y)
        print("self.vel = "..self.vel)
        print("self.dist = "..self.dist)
        print("self.tickmax = "..self.tickmax)
    end
end


function Knockback:update()
    if self:collision() then
        self:destroy()
        return
    else
        self.tick = self.tick + 1
    end
    if self.updfunc then
        self.updfunc(self)
    end
    if not self.interrupt and isalive(self.unit) and self.tick < self.tickmax and self.trav < self.dist then
        self.trav = self.trav + self.vel
        self.x, self.y = projectxy(self.x, self.y, self.vel, self.angle)
        -- special effect staggered play for performance/aesthetic:
        if ModuloInteger(self.tick, self.efftickrate) == 0 then
            DestroyEffect(AddSpecialEffect(self.effect, self.x, self.y))
        end
        -- add/remove storm crow for pathing:
        UnitAddAbility(self.unit, self.abil)
        setxy(self.unit, self.x, self.y)
        UnitRemoveAbility(self.unit, self.abil)
    else
        self.delete = true
        self:destroy()
    end
    if Knockback.debug and ModuloInteger(self.tick, 21) then
        print("attempting to update for unit index "..self.udex)
        print("self.unit = "..tostring(self.unit))
        print("self.x = "..self.x)
        print("self.y = "..self.y)
        print("self.vel = "..self.vel)
        print("self.dist = "..self.dist)
        print("self.tickmax = "..self.tickmax)
    end
end


function Knockback:destroy()
    -- hook self.delete to force remove or prevent removal:
    if self.delete then
        if self.pause then
            PauseUnit(self.unit, false)
        end
        for i,v in pairs(self) do
            v = nil
            i = nil
        end
        Knockback.stack[self] = nil
        Knockback.total = Knockback.total - 1
        if Knockback.total <= 0 then
            Knockback.total = 0
            PauseTimer(Knockback.tmr)
        end
        if Knockback.total < 10 then
            -- recycle indeces when size is low:
            tablecollapse(Knockback.stack)
        end
    elseif Knockback.debug then
        print("caution: self.delete hooked, instance not removed")
    end
    if Knockback.debug then print("total kb instances: "..Knockback.total) end
end


function Knockback:collision()
    if not self.isflying and (self:terraincollision() or self:destcollision()) then
        -- hook delete in collision func to prevent deletion, etc.
        self.delete = true
        if self.colfunc then
            self.colfunc(self)
        end
        return true
    end
    return false
end


function Knockback:terraincollision()
    self.offsetcheck = self.vel + self.colradius
    self.offsetx, self.offsety = projectxy(self.x, self.y, self.offsetcheck, self.angle)
    if IsTerrainPathable(self.offsetx, self.offsety, PATHING_TYPE_WALKABILITY) then
        return true
    end
    return false
end


function Knockback:destcollision()
    self:updaterect(self.offsetx, self.offsety)
    if self.destroydest then
        EnumDestructablesInRect(Knockback.rect, nil, function() self:hitdestdestroy() end)
    else
        EnumDestructablesInRect(Knockback.rect, nil, function() self:hitdestcollide() end)
    end
    if self.destcol then
        return true
    else
        return false
    end
end


-- destroy destructables.
function Knockback:hitdestdestroy()
    self.r_dest = GetEnumDestructable()
    -- check destructable life to ignore any that are set to invulnerable:
    if GetWidgetLife(self.r_dest) > 0.405 and GetWidgetLife(self.r_dest) < self.destmaxlife and not IsDestructableInvulnerable(self.r_dest) then
        if self.destplayeff and self.desteff then
            DestroyEffect(AddSpecialEffect(self.desteff, GetWidgetX(self.r_dest), GetWidgetY(self.r_dest)))
        end
        SetWidgetLife(self.r_dest, 0)
    elseif GetWidgetLife(self.r_dest) >= self.destmaxlife or IsDestructableInvulnerable(self.r_dest) then
        self.destcol = true
    end
end


function Knockback:updaterect(x, y)
    local w = self.destradius/2
    SetRect(Knockback.rect, x - w, y - w, x + w, y + w)
end


-- find out if there is a nearby destructable to collide with.
function Knockback:hitdestcollide()
    self.r_dest = GetEnumDestructable()
    if self.r_dest and GetWidgetLife(self.r_dest) > 0.405 then
        if IsUnitInRangeXY(
            self.unit,
            GetWidgetX(self.r_dest),
            GetWidgetY(self.r_dest),
            self.destradius)
        then
            self.destcol = true
        end
    end
end


function Knockback:reset()
    -- e.g. for recursive updates of a kb instance.
    self.trav    = 0
    self.tickmax = 0
    self.tick    = 0
    self.delete  = false
end
 

Attachments

  • lua_basic_knockback_2.1.w3m
    35.5 KB · Views: 6
Status
Not open for further replies.
Top