• 🏆 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!

[vJASS] Display Remaining Cooldown

Level 15
Joined
Nov 30, 2007
Messages
1,202
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


JASS:
/*
    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:
Level 6
Joined
Jan 9, 2019
Messages
102
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.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
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.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
There is the get remaining cooldown native.
JASS:
native BlzGetUnitAbilityCooldownRemaining          takes unit whichUnit, integer abilId returns real

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:
Level 15
Joined
Nov 30, 2007
Messages
1,202
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.
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:
Level 6
Joined
Jan 9, 2019
Messages
102
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.
 
Level 6
Joined
Jan 9, 2019
Messages
102
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?
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
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?

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:
Level 6
Joined
Jan 9, 2019
Messages
102
What you are asking for is just LocalWrappers?
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.

Also, you don't have to fear the timers, it's an irrational fear you carry, really. :d
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).
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
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.

What about this?
JASS:
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:
Level 6
Joined
Jan 9, 2019
Messages
102
What about this?
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?
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
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.

What say you?

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:
Level 6
Joined
Jan 9, 2019
Messages
102
Wait, wait. I said my previous post only by referring to my very old dusty memory of using the function this way:
JASS:
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.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
Wait, wait. I said my previous post only by referring to my very old dusty memory of using the function this way:
JASS:
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.

I've only tested changing tooltips and such directly inside local blocks without causing desyncs.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
JASS:
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?
I wouldn't have made my Spell Menu otherwise, as I use something like that to change spell icon. ^^
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
I made a similar system a few weeks ago, but I use BlzGetUnitAbilityCooldownRemaining. What issue are you having with that, exactly?
I'm using it now. The issue is that Blizzard cooldown doesn't start until it finishes casting (or something like that). Though now i have a 1s timer that updates using GetUnitAbilityCooldown and it's working fine.
 
Level 15
Joined
Nov 30, 2007
Messages
1,202
I think it's better if you run everything off a single timer then.

I'm still not convinced because you'd still have to run the timer on a relatively short interval 0.1 - 0.2 or so to be somewhat accurate, and you'd have to cycle all instances 10 to 5 times per second, instead of each one having their own timer and updated exactly once per second at the exact time.

You are reducing timer allocation at the expense of execution efficiency, 50 timers is nothing. But I'm considering it and would listen to arguments.

Also there is already a 0.8 minimum delay between when the event fires and before it's added to the update group to account for different spell behavior, and I'm quite happy with the current accuracy. I'm simply reusing the delay timer for the rest of the cooldown duration.
 
Last edited:
Top