1. Updated Resource Submission Rules: All model & skin resource submissions must now include an in-game screenshot. This is to help speed up the moderation process and to show how the model and/or texture looks like from the in-game camera.
    Dismiss Notice
  2. DID YOU KNOW - That you can unlock new rank icons by posting on the forums or winning contests? Click here to customize your rank or read our User Rank Policy to see a list of ranks that you can unlock. Have you won a contest and still havn't received your rank award? Then please contact the administration.
    Dismiss Notice
  3. We have recently started the 16th edition of the Mini Mapping Contest. The theme is mini RPG. Do check it out and have fun.
    Dismiss Notice
  4. Dismiss Notice
  5. Choose your ride to damnation in the 5th Special Effect Contest Poll.
    Dismiss Notice
  6. The winners of the 13th Techtree Contest have been announced!
    Dismiss Notice
  7. Check out the Staff job openings thread.
    Dismiss Notice
Dismiss Notice
60,000 passwords have been reset on July 8, 2019. If you cannot login, read this.

[vJASS] Display Remaining Cooldown

Discussion in 'Submissions' started by Pinzu, Jan 17, 2019.

  1. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,178
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    This is a very short snippet that enables the showing of ability cooldown countdown as part of the extended tooltip, there is really not much more to it than that. To implement simply copy the script and run. Please drop suggestions for how this could be improved.

    It optionally implements:
    [vJASS] - PlayerUtils
    [Snippet] New Table

    Note that changing tooltip for a spell will also change the tooltip of other units with the same spell / level. This mean that this system is not MUI but MPI. It only work in maps with heroes or where players can't have more than one unit of the same type. One remedy is to add additional levels to unit abilities and assigning a unique level to the unit as it enters the map, that way no tooltip will be shared.

    upload_2019-1-17_2-42-2.png

    Code (vJASS):

    /*
        Made by: Pinzu
        Requirements:
            None
         
        Optional
            Table by Bribe
            PlayerUtils by TriggerHappy
         
        Note that changing extended tooltip after a cooldown has started may lead to undesired behaviour.
    */

    library ShowAbilityCooldown requires /*AbilityIndexer,*/ optional Table, optional PlayerUtils
        globals  
            private constant string COOLDOWN_COLOR                = "|c00959697"                
            private constant string COOLDOWN_LABEL                = "|n|n" + COOLDOWN_COLOR + "Cooldown: "
           
            // This will only apply cooldown tooltip locally, meaning only the owning player will see it.
            // The benefit of this is that multiple players can have the same hero type without affecting each other.
            // The drawback is that the cooldown is not shown for shared players.
            private constant boolean LOCAL_CHANGES                = true      
        endglobals
        struct AbilityCooldownManager
            private static constant real MIN_REQ_DELAY     = 0.8 // Must for pitlord spells, 0.7 for keeper of the grove and 0.5 for most other spells
            private static constant real REFRESH_RATE     = 1.0
           
            static if LIBRARY_Table then
                private static Table table
            else
                private static hashtable hash
            endif
           
            private unit u
            private integer abilCode
            private timer t
            private string tooltip
            private integer level
           
            private static method create takes nothing returns thistype
                return .allocate()
            endmethod
           
            /*  
                Here you can filter out units that you wish to exclude from the system
            */

            private static method filter takes nothing returns boolean
                local unit u = GetFilterUnit()
                if not IsUnitType(u, UNIT_TYPE_HERO) then    // example
                    return false
                endif
                return true
            endmethod
     
            private static method formatTime takes real time returns string
                return I2S(R2I(time))    // format time
            endmethod
           
            private method update takes real timeLeft  returns nothing
                local integer lvl = GetUnitAbilityLevel(.u, .abilCode)
                local player p
                static if LIBRARY_PlayerUtil then
                    local User user = User.first
                else        
                    local integer i = 0
                endif
       
                // if new level save text
                if .level != lvl then
                    set .level = lvl
                    set .tooltip = BlzGetAbilityExtendedTooltip(.abilCode, .level)
                endif
             
                // Update
                if timeLeft > 0.1 then
                    if GetOwningPlayer(.u) == GetLocalPlayer() or not LOCAL_CHANGES then
                        call BlzSetAbilityExtendedTooltip(.abilCode, .tooltip + COOLDOWN_LABEL + thistype.formatTime(timeLeft), .level)  
                    endif
                else
                    if GetOwningPlayer(.u) == GetLocalPlayer() or not LOCAL_CHANGES then
                        call BlzSetAbilityExtendedTooltip(.abilCode, .tooltip, .level)
                    endif
                endif
             
                // Refresh unit selection
                static if LIBRARY_PlayerUtil then
                    loop // only loop through players that are playing
                        exitwhen user == User.NULL
                        if IsUnitSelected(.u, user.p) and User.fromLocal().id == user.id then
                            call SelectUnitRemove(.u)
                            call SelectUnitAdd(.u)
                        endif
                        set user = user.next
                    endloop
                else
                    loop
                        exitwhen i == bj_MAX_PLAYER_SLOTS
                        set p = Player(i)
                        if IsUnitSelected(.u, p) and GetLocalPlayer() == p then
                            call SelectUnitRemove(.u)
                            call SelectUnitAdd(.u)
                        endif
                        set i = i + 1
                    endloop
                    set p = null
                endif
             
                // Clean up if finished
                if timeLeft <= 0.1 then
                    static if LIBRARY_Table then
                        call thistype.table.remove(GetHandleId(.t))
                    else
                        call FlushChildHashtable(thistype.hash, GetHandleId(.t))
                    endif
                    // Ability Indexer if its a unit...
                    //call DeindexUnitAbilityLevel(.u, abilCode)
                    call PauseTimer(.t)
                    call DestroyTimer(.t)
                    set .t = null
                    set .u = null
                    call .deallocate()
                endif
            endmethod
         
            private static method onTimerExpires takes nothing returns nothing
                static if LIBRARY_Table then
                    local thistype this = thistype.table[GetHandleId(GetExpiredTimer())]
                else
                    local thistype this = LoadInteger(thistype.hash, GetHandleId(GetExpiredTimer()), 0)
                endif
                local real cooldown = BlzGetUnitAbilityCooldownRemaining(this.u, this.abilCode)
                call this.update(BlzGetUnitAbilityCooldownRemaining(.u, .abilCode))
            endmethod
            private static method addToPool takes nothing returns nothing
                static if LIBRARY_Table then
                    local thistype this = thistype.table[GetHandleId(GetExpiredTimer())]
                else
                    local thistype this = LoadInteger(thistype.hash, GetHandleId(GetExpiredTimer()), 0)
                endif
               call this.update(BlzGetUnitAbilityCooldownRemaining(this.u, this.abilCode))
               call TimerStart(this.t, thistype.REFRESH_RATE, true, function thistype.onTimerExpires)
            endmethod
       
            private static method adjustForOffset takes nothing returns nothing
                static if LIBRARY_Table then
                    local thistype this = thistype.table[GetHandleId(GetExpiredTimer())]
                else
                    local thistype this = LoadInteger(thistype.hash, GetHandleId(GetExpiredTimer()), 0)
                endif
                local real cooldown = BlzGetUnitAbilityCooldownRemaining(this.u, this.abilCode)
                call TimerStart(this.t, cooldown - R2I(cooldown), false, function thistype.addToPool)
            endmethod
       
            public static method start takes unit u, integer abilCode returns nothing
                local thistype this
                local real cooldown = BlzGetUnitAbilityCooldown(u, abilCode, GetUnitAbilityLevel(u, abilCode))
                if cooldown == 0. then
                    return
                endif
                set this = .allocate()
                set this.abilCode = abilCode
                set this.u = u
                set this.level = -1
                set this.t = CreateTimer()
                // Ability Indexer
                //call IndexUnitAbilityLevel(u, abilCode)
                call TimerStart(this.t, thistype.MIN_REQ_DELAY, false, function thistype.adjustForOffset)
                static if LIBRARY_Table then
                    set thistype.table[GetHandleId(this.t)] = this
                else
                    call SaveInteger(thistype.hash, GetHandleId(this.t), 0, this)
                endif
                call this.update(cooldown)
            endmethod
           
            static method onSpellFinish takes nothing returns boolean
                call thistype.start(GetTriggerUnit(), GetSpellAbilityId())
                return false
            endmethod
           
            private static method onInit takes nothing returns nothing
                local trigger trgSpell = CreateTrigger()
                //local trigger trgDeath = CreateTrigger()
                static if LIBRARY_PlayerUtil then
                    local User p = User.first
                    loop // only loop through players that are playing
                        exitwhen p == User.NULL
                        call  TriggerRegisterPlayerUnitEvent(trgSpell, p.p, EVENT_PLAYER_UNIT_SPELL_CAST, null)
                        set p = p.next
                    endloop
                else
                    local integer i = 0
                    local player p
                    loop
                        exitwhen i == bj_MAX_PLAYER_SLOTS
                        set p = Player(i)
                        if (GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING and /*
                        */
    GetPlayerController(p) == MAP_CONTROL_USER) then
                            call  TriggerRegisterPlayerUnitEvent(trgSpell, p, EVENT_PLAYER_UNIT_SPELL_CAST, Filter(function thistype.filter))
                        endif
                        set i = i + 1
                    endloop
                    set p = null
                endif
                call TriggerAddCondition(trgSpell, Condition(function thistype.onSpellFinish))
                static if LIBRARY_Table then
                    set thistype.table = Table.create()
                else
                    set thistype.hash = InitHashtable()
                endif
                set trgSpell = null
               
                // Add Unit Abilities that should be indexed...
                //call AddAbilityToIndexer('Ahea', 100)
               
            endmethod
        endstruct
    endlibrary
     


    Reincarnation is a special case that doesn't fire like other abilities. In order to display it's cooldown properly you need a Unit Event System. In the following example we'll be using Bribe's system GUI Unit Event v2.5.2.0 to detect when a hero is revived using a resurrection ability.

    • On Revival
      • Events
        • Game - DeathEvent becomes Equal to 2.00
      • Conditions
      • Actions
        • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
          • If - Conditions
            • IsUnitReincarnating[UDex] Equal to True
          • Then - Actions
            • Set u = UDexUnits[UDex]
            • Set a = Reincarnation
            • Game - Display to (All players) the text: (UnitName[UDex] + has finished reincarnating)
            • Custom script: call AbilityCooldownManager.start(udg_u, udg_a)
          • Else - Actions
            • Game - Display to (All players) the text: (UnitName[UDex] + has come back to life)
     
    Last edited: Jan 17, 2019
  2. Overfrost

    Overfrost

    Joined:
    Jan 9, 2019
    Messages:
    102
    Resources:
    0
    Resources:
    0
    Just pointing some things that I do know:
    - Use library instead of scope;
    library ShowAbilityCooldown requires optional Table, PlayerUtils
    .
    - TWO timers per instance is a huge no. Even only one/instance is a big no already.

    I only skimmed through the code, there're ways to use less timers here.
     
  3. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,178
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    I didn't find a native for getting the remaining cooldown time so I had to start a timer (it might exists but I couldn't find it).

    The 1 second refresh timer I suppose could be replaced with some forEach loop with use of some data structure to store the instances, though I don't think the timer count will get that bad, we are talking about probably less than a 100 in total even for 24 players.
     
  4. Tasyen

    Tasyen

    Joined:
    Jul 18, 2010
    Messages:
    1,442
    Resources:
    18
    Tools:
    2
    Maps:
    3
    Spells:
    8
    Tutorials:
    4
    JASS:
    1
    Resources:
    18
    There is the get remaining cooldown native.
    Code (vJASS):

    native BlzGetUnitAbilityCooldownRemaining          takes unit whichUnit, integer abilId returns real
     
     
  5. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,178
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    There is a problem with using this. mainly that it returns 0... ^^

    • Test
      • Events
        • Unit - A unit owned by Player 1 (Red) Begins casting an ability
      • Conditions
      • Actions
        • Set u = (Triggering unit)
        • Set a = (Ability being cast)
        • Custom script: set udg_cd = BlzGetUnitAbilityCooldownRemaining(udg_u, udg_a)
        • Game - Display to (All players) the text: (String(cd))


    it works if i add a 0.5 second delay though, any shorter than that and it returns 0.

    _________________

    Updated the code. I kept one timer for each instance as i think it's preferable to having one timer that is synchronized exactly when one second has elapsed for a given cooldown than iterating over all periodically and updating all at unsynchronized times, increasing the execution overhead and requiring a list data structure to be maintained.

    Added a filter method in case anyone wish to exclude dummy units or any such things...

    Apperently some spells don't work with the system (I haven't tested all but many):
    Ensnare
    Resurrection
    Entangle
    Tranquility


    __________________

    The solution I believe is making start(unit u, integer abilCode) public and that way allow users to manually start cooldown when reincarnation is detected. I'm not sure exactly what would be the best way to automate it.

    To account for resurrection the user would have to do this manually (wont work properly if the hero is resurrected through an altar though):
    • Resurrection
      • Events
        • Time - Every 1.00 seconds of game time
      • Conditions
      • Actions
        • Set u = Tauren Chieftain 0007 <gen>
        • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
          • If - Conditions
            • (u is alive) Equal to True
          • Then - Actions
            • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
              • If - Conditions
                • prevAlive Equal to False
              • Then - Actions
                • Set a = Reincarnation (c)
                • Custom script: set udg_time = BlzGetUnitAbilityCooldown(udg_u, udg_a, GetUnitAbilityLevel(udg_u, udg_a))
                • Custom script: call AbilityCooldownManager.start(udg_u, udg_a)
                • Game - Display to (All players) the text: started resurrection
              • Else - Actions
            • Set prevAlive = True
          • Else - Actions
            • Set prevAlive = False
     
    Last edited: Jan 17, 2019
  6. Tasyen

    Tasyen

    Joined:
    Jul 18, 2010
    Messages:
    1,442
    Resources:
    18
    Tools:
    2
    Maps:
    3
    Spells:
    8
    Tutorials:
    4
    JASS:
    1
    Resources:
    18
    Cooldown starts 0s after starts the event: "starts effect of an ability".
    At "begins casting" event the cooldown yet not started.​
    "BlzGetUnitAbilityCooldownRemaining" returns the amount of seconds left of a running cooldown.
     
  7. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,178
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    Okay, can't change the event though due to blizzard not working properly otherwise.

    Update: Added example on how to start a cooldown manually using Bribe's Unit Event system to solve the resurrection problem.

    Please submit UnitEvent systems that can handle Reincarnation and I'll consider including them as optional libraries.

    *Edit just realized the tooltips are not unit independant, so units that don't cast spells will still have their tooltips changed, and there is nothing really that I can do about that. So this system doesn't work for all cases.
     
    Last edited: Jan 19, 2019
  8. Overfrost

    Overfrost

    Joined:
    Jan 9, 2019
    Messages:
    102
    Resources:
    0
    Resources:
    0
    What Tasyen meant is:
    - On "a unit begins casting" event, only the order has been done (this includes animations), but mana cost hasn't been paid, the cooldown hasn't even started, and the spell effect hasn't yet been applied.
    - Due to above, BlzGetUnitAbilityCooldownRemaining returns 0 on such events.
    - On the contrary "a unit starts casting" event happens right after the mana cost and cooldown have been resolved. Though I haven't tested how that function would fare in this event.

    So the new tooltip changing feature works per item and not per instance of each item. Seems fair.
     
  9. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,178
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    Per player item as i can still use GetLocalPlayer(). Speaking of items, I should probably extend this to include them... ^^
     
  10. Overfrost

    Overfrost

    Joined:
    Jan 9, 2019
    Messages:
    102
    Resources:
    0
    Resources:
    0
    True, per player-instance of each item.

    It's waay better if you just make a library to handle the new tooltips instead, in a very basic manner. It's simpler, more useful, and you need 0 timer for this. Plus, I might use this lib (heheh, less trouble for me).

    If you're not going to make such lib then I'll probably make one myself, in time. I mean, handling them GetLocalPlayer calls manually will be such a real pain, no?
     
  11. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,178
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    What you are asking for is just LocalWrappers? Don't think its that painful to do them manually right now ^^

    BlzSetAbilityExtendedTooltip(abilitycode, tooltip, level) --> SetPlayerAbilityExtendedTooltip(player, abilitycode, tooltip, level)

    If I got the SpellIndexer to work I could consider it as then you could also have

    SetUnitAbilityExtendedTooltip(unit, abilitycode, tooltip)

    Which might warrent a library.


    Also, you don't have to fear the timers, it's an irrational fear you carry, really. :d
     
    Last edited: Jan 19, 2019
  12. Overfrost

    Overfrost

    Joined:
    Jan 9, 2019
    Messages:
    102
    Resources:
    0
    Resources:
    0
    Not exactly like that, I'd say that's still painful to write. It's more in organized and simplified, yet accurate, struct format. I haven't dabbled much in this new tooltip stuffs, so I can't say much on the details.

    Timer usage is not something to be taken lightly by system-designers. Multiple systems that don't care about timer efficiency can stack up their number and cause unnecessary lags/slowdowns. And other than timer efficiency, I believe there's also trigger efficiency. But I'm sure it's pretty much about keeping the number of handle-type instances as low as possible.

    Speaking of fear, I'm not afraid of inefficiency for non-public codes. But for public uses, codes should always be as optimized as possible. When I wrote my Vector library, I actually stumbled on 2 different ways of implementing a rotation algorithm. In the end, I used the more complex algorithm which netted me with 1 less native call, 1 less addition/subtraction, and 2 less multiplications, compared to the other straightforward solution (I did count for this, no kidding).
     
  13. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,178
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    What about this?
    Code (vJASS):

    library TooltipManager initializer Init
        struct Ability
            private static Table table
            private integer abilCode
            private static method create takes integer abilCode returns thistype
                local thistype this = .allocate()
                set this.abilCode = abilCode
                return this
            endmethod
     
            static method operator [] takes integer abilCode returns thistype
                local thistype a = .table[abilCode]
                if a == 0 then
                    set a = thistype.create(abilCode)
                    set .table[abilCode] = a
                endif
                return a
            endmethod
         
            method setTooltip takes string s, integer lvl returns nothing
                call BlzSetAbilityTooltip(.abilCode, s, lvl)              
            endmethod
         
            method getTooltip takes integer lvl returns string
                return BlzGetAbilityTooltip(.abilCode, lvl)
            endmethod
         
            method setLocalTooltip takes player p, string s, integer lvl returns nothing
             
            endmethod
         
            method getLocalTooltip takes player p, integer lvl returns string
                return ""
            endmethod
         
            private static method onInit takes nothing returns nothing
                set thistype.table = Table.create()
            endmethod
     
        endstruct
     
       // Example usage
        private function Init takes nothing returns nothing
            call Ability['Aply'].setTooltip("Hello World", 1)

            // it isn't obvious that this is an improvement over simply using the native
           call BlzSetAbilityTooltip('Aply'], "Hello World", 1)
         
          // and
          if GetLocalPlayer() == Player(0) then
              call BlzSetAbilityTooltip('Aply'], "Hello World", 1)
          endif    

         // or even this
         set Ability['Aply'].tooltip[1] = "Hello World"

        endfunction
     
     
    endlibrary
     
     
    Last edited: Jan 19, 2019
  14. Overfrost

    Overfrost

    Joined:
    Jan 9, 2019
    Messages:
    102
    Resources:
    0
    Resources:
    0
    Now we're talking!

    I actually wanted to write some API samples until something popped in my head. Won't GetLocalPlayer only affect ONE player? So, in order to accommodate say, 10 players that run the same abilities, it'd be needed for the map to have 5 different copies of the same original abilities to achieve perfect multi-player-instanceability. Which then leads to the conclusion of not even trying to incorporate such complications into the system.

    And therefore I'd agree with having:
    - Full tooltip control if all player only use abilities unique to themselves.
    - Global tooltip control that changes ability tooltip by game-state. (e.g. abilities based on global gold income, global game events, global weather, etc)
    - No hope of achieving perfect multi-player-instanceable tooltip system.

    What say you?
     
  15. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,178
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    No but you would need to save whenever a local configuration is done inside a table, and if you'd ever want to clean that stuff up when a player leaves, you'd have to loop through all abilities and purge based on playerId. Did something similar in my Spell Menu for options (without the purging part).

    I'm not sure how pluging in events would work on a player basis

    i mean lets say you do:

    ability.tooltip[1] = "This is a tooltip {observer[1]}"

    ability.setPlayerTooltip(Player(5), 1, "This is a player 5 tooltip watching: {observer[5]}")

    My observable pattern was never approved but would fit in here for what you'd want to acomplish... ^^
    The above is bassed on that observer [index] is global. One could however have a manual setup...

    ability.setObserver(index, whichobserver) and then the index given in the tooltip would check a internal list.
     
    Last edited: Jan 19, 2019
  16. Overfrost

    Overfrost

    Joined:
    Jan 9, 2019
    Messages:
    102
    Resources:
    0
    Resources:
    0
    Wait, wait. I said my previous post only by referring to my very old dusty memory of using the function this way:
    Code (vJASS):
    local string s = ""
    if (GetLocalPlayer() == Player(0)) then
        set s = "myCustomSE.mdl"
    endif
    ...  // create special effect, blah blah blah

    Have you tested the new functions directly inside the local-if block? If that won't cause multiplayer desyncs then... I'm in.
     
  17. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,178
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    I've only tested changing tooltips and such directly inside local blocks without causing desyncs.
     
  18. Overfrost

    Overfrost

    Joined:
    Jan 9, 2019
    Messages:
    102
    Resources:
    0
    Resources:
    0
    Code (vJASS):
    function SetLocalAbilityTooltip takes player p, integer id, integer lv, string s returns nothing
        if (GetLocalPlayer() == p) then
            call BlzSetAbilityTooltip(id, s, lv)
        endif
    endfunction

    Will that code above work without causing desyncs?
     
  19. Pinzu

    Pinzu

    Joined:
    Nov 30, 2007
    Messages:
    1,178
    Resources:
    3
    Spells:
    2
    Tutorials:
    1
    Resources:
    3
    I wouldn't have made my Spell Menu otherwise, as I use something like that to change spell icon. ^^
     
  20. busterkomo

    busterkomo

    Joined:
    Jun 17, 2007
    Messages:
    1,423
    Resources:
    1
    Tutorials:
    1
    Resources:
    1
    I made a similar system a few weeks ago, but I use BlzGetUnitAbilityCooldownRemaining. What issue are you having with that, exactly?