• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

[vJASS] Movespeed

Level 23
Joined
Feb 6, 2014
Messages
2,466
JASS:
library Movespeed /*

                    Movespeed v1.21
                       by Flux
       
        Applies a stacking movespeed modification to a unit
        through code.
       
        Formula:
        New Movespeed = (Default Movespeed + Total FlatBonus)*(1 + Total PercentBonus)
   
    */ requires /*
       (nothing)
   
    */ optional Table /*
       If not found, the system will create 1 hashtable. Hashtables are
       limited to 255 per map.
       
    */ optional TimerUtils /*
       If found, timers for duration will be recycled.
       
   
    ******************************
                   API
    ******************************
   
    struct Movespeed
   
        static method create(unit, percentBonus, flatBonus)
            - Create a Movespeed modification.
            EXAMPLE: local Movespeed ms = Movespeed.create(GetTriggerUnit(), 0.15, 0)
           
        method operator duration= 
            - Sets the current duration of the Movespeed instace.
            EXAMPLE: set ms.duration = 5
       
        method operator duration
            - Reads the current duration of the Movespeed instance.
            - Returns zero if the instance has no duration
            EXAMPLE: call BJDebugMsg("Time left: " + R2S(ms.duration))
           
        method change(newPercentBonus, newFlatBonus)
            - Change the movespeed modification of a certain instance
            EXAMPLE: call ms.change(0.20, 0)
       
        method destroy()
            - Remove an instance of movespeed modification.
            - Not needed if thhe Movespeed instance has a duration.
   
    -------------------
           NOTE: 
    -------------------
        All in-game movespeed modifiers such as Boots of Speed, Endurance Aura, Slow Aura, etc.
        will still work with this system, but all of them are always applied last.  
       
        Formula:
        New Movespeed = ((Default Movespeed + Total FlatBonus)*(1 + Total PercentBonus) + Total in-game FlatBonus)*(1 + Total in-game PercentBonus)
       
    -----------
      CREDITS
    -----------
        Bribe    - Table
        Vexorian - TimerUtils
        Aniki    - For the movespeed formula used by Warcraft 3

*/    
    struct Movespeed
       
        readonly real pb
        readonly real fb
        readonly unit u
        private real default
        private timer t
       
        private thistype head
        private integer count
       

        static if LIBRARY_Table then
            private static Table tb
        else
            private static hashtable hash = InitHashtable()
        endif
       
       
        method destroy takes nothing returns nothing
            local thistype head = this.head
            set head.pb = head.pb - this.pb
            set head.fb = head.fb - this.fb
            set head.count = head.count - 1
            if this.t != null then
                static if LIBRARY_TimerUtils then
                    call ReleaseTimer(this.t)
                else
                    static if LIBRARY_Table then
                        call thistype.tb.remove(GetHandleId(this.t))
                    else
                        call RemoveSavedInteger(thistype.hash, GetHandleId(this.t), 0)
                    endif
                    call DestroyTimer(this.t)
                endif
                set this.t = null
            endif
            if head.count == 0 then
                static if LIBRARY_Table then
                    call thistype.tb.remove(GetHandleId(this.u))
                else
                    call RemoveSavedInteger(thistype.hash, GetHandleId(this.u), 0)
                endif
                call head.deallocate()
            endif
            call SetUnitMoveSpeed(this.u, (head.default + head.fb)*(1 + head.pb))
            set this.u = null
            call this.deallocate()
        endmethod
       
        method change takes real newPercentBonus, integer newFlatBonus returns nothing
            local thistype head = this.head
            set head.pb = head.pb + newPercentBonus - this.pb
            set head.fb = head.fb + newFlatBonus - this.fb
            set this.pb = newPercentBonus
            set this.fb = newFlatBonus
            call SetUnitMoveSpeed(u, (head.default + head.fb)*(1 + head.pb))
        endmethod
       
        static method create takes unit u, real percentBonus, integer flatBonus returns thistype
            local thistype this = thistype.allocate()
            local integer id = GetHandleId(u)
            local thistype head
            static if LIBRARY_Table then
                if thistype.tb.has(id) then
                    set head = thistype.tb[id]
                    set head.count = head.count + 1
                else
                    set head = thistype.allocate()
                    set head.pb = 0
                    set head.fb = 0
                    set head.count = 1
                    set head.default = GetUnitDefaultMoveSpeed(u)
                    set thistype.tb[id] = head
                endif
            else
                if HaveSavedInteger(thistype.hash, id, 0) then
                    set head = LoadInteger(thistype.hash, id, 0)
                    set head.count = head.count + 1
                else
                    set head = thistype.allocate()
                    set head.pb = 0
                    set head.fb = 0
                    set head.count = 1
                    set head.default = GetUnitDefaultMoveSpeed(u)
                    call SaveInteger(thistype.hash, id, 0, head)
                endif
            endif
            set this.u = u
            set this.pb = percentBonus
            set this.fb = flatBonus
            set this.head = head
            set head.pb = head.pb + this.pb
            set head.fb = head.fb + this.fb
            call SetUnitMoveSpeed(u, (head.default + head.fb)*(1 + head.pb))
            return this
        endmethod
       
        private static method expired takes nothing returns nothing
            static if LIBRARY_TimerUtils then
                call thistype(GetTimerData(GetExpiredTimer())).destroy()
            elseif LIBRARY_Table then
                call thistype(thistype.tb[GetHandleId(GetExpiredTimer())]).destroy()
            else
                call thistype(LoadInteger(thistype.hash, GetHandleId(GetExpiredTimer()), 0)).destroy()
            endif
        endmethod
       
        method operator duration takes nothing returns real
            if this.t == null then
                return 0.0
            endif
            return TimerGetRemaining(this.t)
        endmethod
       
        method operator duration= takes real time returns nothing
            if this.t == null then
                static if LIBRARY_TimerUtils then
                    set this.t = NewTimerEx(this)
                else
                    set this.t = CreateTimer()
                    static if LIBRARY_Table then
                        set thistype.tb[GetHandleId(t)] = this
                    else
                        call SaveInteger(thistype.hash, GetHandleId(t), 0, this)
                    endif
                endif
            endif
            call TimerStart(this.t, time, false, function thistype.expired)
        endmethod
       
       
        static if LIBRARY_Table then
            private static method onInit takes nothing returns nothing
                set thistype.tb = Table.create()
            endmethod
        endif
       
    endstruct
endlibrary


v1.00 - [18 August 2016]
- Initial Release

v1.10 - [19 August 2016]
- Changed movespeed formula.

v1.11 - [19 September 2016]
- Fixed inconsistency between having Table and using a hashtable.

v1.20 - [29 November 2016]
- Changed API name. (Removed static method createTimed and added method operator duration)
- Fixed possible double free upon manually destroying a timed instance.
- Added TimerUtils as an optional requirement.
- Made instance duration dynamic. It can now be overwritten.

v1.21 - [2 December 2016]
- Added reading of instance duration.
- Removed unused struct attributes.
- Cached default unit movespeed.
 

Attachments

  • Movespeed v1.21.w3x
    41.5 KB · Views: 180
Last edited:
Level 13
Joined
Nov 7, 2014
Messages
571
I think the formula used by wc3 is:

move-speed = (default-move-speed + total-flat-bonuses) * (1 + total-percent-bonuses)

e.g:
JASS:
default-move-speed = 200.0
total-flat-bonuses = 60 // boots of speed item
endurance-aura-percent-bonus = 0.10 // level 1 endurance aura from TC
unholy-aura-percent-bonus = 0.20 // level 2 unholy aura from DK

move-speed = (200 + 60) * (1 + 0.10 + 0.20) = 260 * (1.30) = 338

slowing percent-bonuses are negative, e.g:
JASS:
default-move-speed = 200.0
total-flat-bonuses = 60 // boots of speed item
endurance-aura-percent-bonus = 0.10 // level 1 endurance aura from TC
unholy-aura-percent-bonus = 0.20 // level 2 unholy aura from DK
slow-poison = -0.50 // dryad's slow poison

move-speed = (200 + 60) * (1 + 0.10 + 0.20 + (-0.50)) = 260 * (0.80) = 208
 
Level 13
Joined
Nov 7, 2014
Messages
571
JASS:
static if LIBRARY_Table then
    if thistype.tb.has(id) then
        set head = thistype.tb[id]
        set head.count = head.count + 1
    else
        set head = thistype.allocate()
        set head.pb = 0
        set head.fb = 0
        set head.count = 1
        set thistype.tb[id] = head
    endif
else
    if HaveSavedInteger(thistype.hash, id, 0) then
        set head = LoadInteger(thistype.hash, id, 0)
    else
        set head = thistype.allocate()
        set head.pb = 0
        set head.fb = 0
        call SaveInteger(thistype.hash, id, 0, head)
    endif
endif

It seems that the code path where Table is missing is not equivalent to the path where it's present.
Why not make Table and some timer library (TimerUtils, for example) hard requirements, and get rid of all the bug prone static ifs?

PS: You made me realize (duh...) that units don't need a linked list of "movement speed modifiers" to do scripted movement speed similar to wc3's. Thank you! =)
 
Level 23
Joined
Feb 6, 2014
Messages
2,466
JASS:
static if LIBRARY_Table then
    if thistype.tb.has(id) then
        set head = thistype.tb[id]
        set head.count = head.count + 1
    else
        set head = thistype.allocate()
        set head.pb = 0
        set head.fb = 0
        set head.count = 1
        set thistype.tb[id] = head
    endif
else
    if HaveSavedInteger(thistype.hash, id, 0) then
        set head = LoadInteger(thistype.hash, id, 0)
    else
        set head = thistype.allocate()
        set head.pb = 0
        set head.fb = 0
        call SaveInteger(thistype.hash, id, 0, head)
    endif
endif

It seems that the code path where Table is missing is not equivalent to the path where it's present.
Why not make Table and some timer library (TimerUtils, for example) hard requirements, and get rid of all the bug prone static ifs?
You're right, I careless mistake. I can easily fix that.
I don't want to force people into using other requirements, but instead I like them to know what are the pros/cons of having/not having the said library.

PS: You made me realize (duh...) that units don't need a linked list of "movement speed modifiers" to do scripted movement speed similar to wc3's. Thank you! =)
Yeah, but now it's too simple. I don't know if this one is good enough for the JASS section due to its simplicity.
(Also not that fun to make anymore, no challenge in making it).
 

Submission:
Movespeed v1.1

Date:
28 November 2016

Status:
Approved
Note:

A useful library to manipulate a unit's movement speed in percent, and flat increase.
What was mentioned in posts above could be added.
You need to null the local timer in function "createTimed". I know you will fix the leak as soon as possible,
so I will approve it anyways already, and just check it after some days. Cheers.

edit:

Hm, actually not right away, sorry. I see createTimed also returns thistype, and the destroy has no double free protection.
It seems dangerous if the user has access to manualy destroy his timed Movespeed, when at same time there is no safete actions to also stop/destroy the running timer.
 
Level 13
Joined
Nov 7, 2014
Messages
571
I think not using a hashtable and requiring units to have SetUnitUserData() that is a valid struct instance is better (although I've hardcoded a struct name here):
JASS:
library ModifyMoveSpeed requires Timers /* http://www.hiveworkshop.com/threads/timer-easier-than-timerutils.287441/ */

struct ModifyMoveSpeed
    Unit u
    real base_amount
    real factor_amount
    Timer t

    static method modify takes Unit u, real base_amount, real factor_amount returns nothing
        local real new_ms
        set u.ms_base = u.ms_base + base_amount
        set u.ms_factor = u.ms_factor + factor_amount
        set new_ms = (u.ms_default + u.ms_base) * (1.0 + u.ms_factor)
        call SetUnitMoveSpeed(u.uu, new_ms)
    endmethod

    method destroy takes nothing returns nothing
        call modify(this.u, -this.base_amount, -this.factor_amount)

        if this.t != 0 then
            call this.t.stop()
        endif

        call this.deallocate()
    endmethod

    static method create takes Unit u, real base_amount, real factor_amount returns thistype
        local thistype this = allocate()

        set this.u = u
        set this.base_amount = base_amount
        set this.factor_amount = factor_amount
        set this.t = 0

        call this.modify(u, base_amount, factor_amount)

        return this
    endmethod

    private static method on_duration_end takes nothing returns nothing
        call thistype(Timer.get_expired_data()).destroy()
    endmethod

    static method create_timed takes Unit u, real base_amount, real factor_amount, real duration returns thistype
        local thistype this = create(u, base_amount, factor_amount)
        set this.t = Timer.start(this, duration, function thistype.on_duration_end)
        return this
    endmethod

endstruct

endlibrary

library ModifyMoveSpeedDemo requires ModifyMoveSpeed

struct Unit
    unit uu

    real ms_default = 0.0
    real ms_base = 0.0
    real ms_factor = 0.0

    static method from_existing takes unit uu returns thistype
        local thistype this = allocate()

        set this.uu = uu
        call SetUnitUserData(uu, this)

        set this.ms_default = GetUnitDefaultMoveSpeed(uu)

        return this
    endmethod
endstruct

endlibrary

function main takes nothing returns nothing
    local Unit u = Unit.from_existing(CreateUnit(Player(0), 'hfoo', 0.0, 0.0, 270.0))
    call ModifyMoveSpeed.modify(u, 0.0, 0.10)
    call ModifyMoveSpeed.create_timed(u, 0.0, 0.50, 5.0)
endfunction
 
Level 23
Joined
Feb 6, 2014
Messages
2,466
Hm, actually not right away, sorry. I see createTimed also returns thistype, and the destroy has no double free protection.
It seems dangerous if the user has access to manualy destroy his timed Movespeed, when at same time there is no safete actions to also stop/destroy the running timer.
Right. I can fix this by saving the timer to a struct instance and moving the timer destruction on method destroy. Something like:
JASS:
if this.t != null then
    call DestroyTimer(this.t)
    set this.t = null
endif

I think not using a hashtable and requiring units to have
SetUnitUserData()
that is a valid struct instance is better (although I've hardcoded a struct name here):
Except that will mess up if users uses a Unit Indexer. Besides, Table is an optional requirement so if you're close to the hashtable limit, just import Table in your map.
 
Level 13
Joined
Nov 7, 2014
Messages
571
Except that will mess up if users uses a Unit Indexer.
My point was that you can require units to have a GetUnitUserData that is a valid struct instance.
I guess you would have to require some UI (unit indexing) system to initialize the "ms_base = ms_factor = 0.0 and ms_default = GetUnitDefaultMoveSpeed(...)" when a unit gets indexed.

The "Unit struct" approach is not as "community oriented", I guess, i.e it has to be coded specifically for a map's requirements, although I guess using a module could work as well.

Of course you can stick to the hashtable approach, if you want.
 
Level 13
Joined
Nov 7, 2014
Messages
571
I don't get the hate towards hashtables though.
I don't hate them. I admire your diligence to support both the hashtable and table even at the cost of the many static ifs that kind of reduce the readability (in my opinion).

Anyway... you might want to remove these unused attributes:
JASS:
    private thistype next
    private thistype prev

Could also cache the default move speed:
JASS:
struct Movespeed
...
    readonly real default
...
            set head = thistype.allocate()
            set head.default = GetUnitDefaultMoveSpeed(u)
...
endstruct
 

Submission:
MoveSpeed 1.21

Date:
5 December 2016

Status:
Approved
Note:

Very good snippet to manage movement speed manipulations for units.

Critique.
The system can apply temporary changes, and infinite changes, but it does not truely allow you
to change it from temporary to infinite, because the timer will be still in use. How ever you can
just set it to a very large amount so it would just *never* expire.
 
Top