Arcing Floating Text 1.0.0.3

This bundle is marked as approved. It works and satisfies the submission rules.
This system creates floating texts on units. The text tags change their height throughout their lifespan and they travel in a nice arc.
The system offers a bit nicer method than the standard that is used. Very suitable for damage detection systems.
Creates the texts only for players that have visibility over the target unit, thus the limit of 100 floating texts is less likely to be reached.

JASS:
// Arcing Text Tag v1.0.0.3 by Maker

library FloatingTextArc
    globals
        private constant    real    SIZE_MIN        = 0.018         // Minimum size of text
        private constant    real    SIZE_BONUS      = 0.012         // Text size increase
        private constant    real    TIME_LIFE       = 1.0           // How long the text lasts
        private constant    real    TIME_FADE       = 0.8           // When does the text start to fade
        private constant    real    Z_OFFSET        = 50            // Height above unit
        private constant    real    Z_OFFSET_BON    = 50            // How much extra height the text gains
        private constant    real    VELOCITY        = 2             // How fast the text move in x/y plane
        private constant    real    ANGLE           = bj_PI/2       // Movement angle of the text. Does not apply if
                                                                    // ANGLE_RND is true
        private constant    boolean ANGLE_RND       = true          // Is the angle random or fixed
        private             timer   TMR             = CreateTimer()
    endglobals
    
    struct ArcingTextTag extends array
        private texttag tt
        private real as         // angle, sin component
        private real ac         // angle, cos component
        private real ah         // arc height
        private real t          // time
        private real x          // origin x
        private real y          // origin y
        private string s        // text
        private static integer array next
        private static integer array prev
        private static integer array rn
        private static integer ic           = 0       // Instance count   
        
        private static method update takes nothing returns nothing
            local thistype this=next[0]
            local real p
            loop
                set p = Sin(bj_PI*.t)
                set .t = .t - 0.03125
                set .x = .x + .ac
                set .y = .y + .as
                call SetTextTagPos(.tt, .x, .y, Z_OFFSET + Z_OFFSET_BON * p)
                call SetTextTagText(.tt, .s, SIZE_MIN + SIZE_BONUS * p)
                if .t <= 0 then
                    set .tt = null
                    set next[prev[this]] = next[this]
                    set prev[next[this]] = prev[this]
                    set rn[this] = rn[0]
                    set rn[0] = this
                    if next[0]==0 then
                        call PauseTimer(TMR)
                    endif
                endif
                set this = next[this]
                exitwhen this == 0
            endloop
        endmethod
        
        public static method create takes string s, unit u returns thistype
            local thistype this = rn[0]
            static if ANGLE_RND then
                local real a = GetRandomReal(0, 2*bj_PI)
            else
                local real a = ANGLE
            endif
            if this == 0 then
                set ic = ic + 1
                set this = ic
            else
                set rn[0] = rn[this]
            endif
            
            set next[this] = 0
            set prev[this] = prev[0]
            set next[prev[0]] = this
            set prev[0] = this
            
            set .s = s
            set .x = GetUnitX(u)
            set .y = GetUnitY(u)
            set .t = TIME_LIFE
            set .as = Sin(a)*VELOCITY
            set .ac = Cos(a)*VELOCITY
            set .ah = 0.
            
            if IsUnitVisible(u, GetLocalPlayer()) then
                set .tt = CreateTextTag()
                call SetTextTagPermanent(.tt, false)
                call SetTextTagLifespan(.tt, TIME_LIFE)
                call SetTextTagFadepoint(.tt, TIME_FADE)
                call SetTextTagText(.tt, s, SIZE_MIN)
                call SetTextTagPos(.tt, .x, .y, Z_OFFSET)
            endif
            
            if prev[this] == 0 then
                call TimerStart(TMR, 0.03125, true, function thistype.update)
            endif
            
            return this
        endmethod
    endstruct
endlibrary


  • Create Floating Text
    • Events
      • Game - DamageEvent becomes Equal to 1.00
    • Conditions
    • Actions
      • Custom script: if GetOwningPlayer(udg_DamageEventSource) == GetLocalPlayer() then
      • Set str = (|c00FFFF00 + ((String((Integer(DamageEventAmount)))) + |r))
      • Custom script: else
      • Set str = (|c00FF0000 + ((String((Integer(DamageEventAmount)))) + |r))
      • Custom script: endif
      • Custom script: call ArcingTextTag.create( udg_str, udg_DamageEventTarget)



v1.0.0.0 Uploaded 14th Jan 2013
v1.0.0.1 Uploaded 14th Jan 2013
-Remove a static struct member that counted the amount of active instances, used to turn the timer off and on. Using another method for that now
v1.0.0.2 Uploaded 14th Jan 2013
-Struct member s is no longer nulled
-Inlined destroy method
v1.0.0.3 Uploaded 14th Jan 2013
-Rearranged some code
-Added an option to give the text fixed angle



Thanks for Magtheridon96 for suggestions on arranging code


Keywords:
floating, text, tag, maker, damage
Contents

Just another Warcraft III map (Map)

Reviews
17:02, 14th Jan 2013 Magtheridon96: Approved. This thing is awesome.

Moderator

M

Moderator

17:02, 14th Jan 2013
Magtheridon96: Approved.
This thing is awesome.
 
Level 6
Joined
Apr 16, 2011
Messages
158
My suggestion (not sure if it's possible) is:
- Use different colors for different types of injury:
Damage spells
Damage heroes
Damage units
...
- Perhaps support for healing too?
Anyway, I loved your code.
5/5
 

Kusanagi Kuro

Hosted Project: SC
Level 10
Joined
Mar 11, 2012
Messages
708
The motion of the arc changed when you change the TIME_LIFE variable.

If you want to keep the arc going up then down, then this line:

set p = Sin(bj_PI*.t)

in the update trigger, should become this:

set p = Sin(bj_PI*(.t / TIME_LIFE))

Also, the attribute "ah" seems meaningless? I don't see you using it in the update trigger at all.
 

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,958
Is it possible to dynamically increase the text size when spell damage is done?

I know how to recognize if spell damage is done, but not how to increase the text afterwards...

I just made the below in order to enable that feature. I tested it and plan to use this variant the next time I update the Damage Engine map because it will help certain things to be more subdued or to pop better.

JASS:
// Arcing Text Tag v1.0.1.0 by Maker with added API by Bribe
// 
// Added API in 1.0.1.0:
//   public static ArcingTextTag lastCreated
//   - Get the last created ArcingTextTag
//   public real scaling
//   - Set the size ratio of the texttag - 1.00 is the default
//   public real timeScaling
//   - Set the duration ratio of the texttag - 1.00 is the default
library FloatingTextArc
    globals
        private constant    real    SIZE_MIN        = 0.018         // Minimum size of text
        private constant    real    SIZE_BONUS      = 0.012         // Text size increase
        private constant    real    TIME_LIFE       = 1.0           // How long the text lasts
        private constant    real    TIME_FADE       = 0.8           // When does the text start to fade
        private constant    real    Z_OFFSET        = 50            // Height above unit
        private constant    real    Z_OFFSET_BON    = 50            // How much extra height the text gains
        private constant    real    VELOCITY        = 2             // How fast the text move in x/y plane
        private constant    real    ANGLE           = bj_PI/2       // Movement angle of the text. Does not apply if
                                                                    // ANGLE_RND is true
        private constant    boolean ANGLE_RND       = true          // Is the angle random or fixed
        private             timer   TMR             = CreateTimer()
    endglobals
    
    struct ArcingTextTag extends array        
        private texttag tt
        private real as         // angle, sin component
        private real ac         // angle, cos component
        private real ah         // arc height
        private real t          // time
        private real x          // origin x
        private real y          // origin y
        private string s        // text
        private static integer array next
        private static integer array prev
        private static integer array rn
        private static integer ic           = 0       // Instance count   
        
        private real scale
        public method operator scaling takes nothing returns real
            return scale
        endmethod
        public method operator scaling= takes real r returns nothing
            if .tt != null then
                call SetTextTagText(.tt, .s, SIZE_MIN*r)
            endif
            set .scale = r
        endmethod
        private real timeScale
        public method operator timeScaling takes nothing returns real
            return timeScale
        endmethod
        public method operator timeScaling= takes real r returns nothing
            local real p = Sin(bj_PI*r)
            if .tt != null then
                call SetTextTagLifespan(.tt, TIME_LIFE*r)
                call SetTextTagFadepoint(.tt, TIME_FADE*r)
                call SetTextTagPos(.tt, .x, .y, Z_OFFSET + Z_OFFSET_BON*p)
                call SetTextTagText(.tt, .s, (SIZE_MIN + SIZE_BONUS*p)*.scale)
                set .t = r
            endif
            set .t = r
        endmethod
        
        public static thistype lastCreated = 0
        
        private static method update takes nothing returns nothing
            local thistype this=next[0]
            local real p
            loop
                set p = Sin(bj_PI*.t)
                set .t = .t - 0.03125
                set .x = .x + .ac
                set .y = .y + .as
                call SetTextTagPos(.tt, .x, .y, Z_OFFSET + Z_OFFSET_BON*p)
                call SetTextTagText(.tt, .s, (SIZE_MIN + SIZE_BONUS*p)*.scale)
                if .t <= 0 then
                    set .tt = null
                    set next[prev[this]] = next[this]
                    set prev[next[this]] = prev[this]
                    set rn[this] = rn[0]
                    set rn[0] = this
                    if next[0]==0 then
                        call PauseTimer(TMR)
                    endif
                endif
                set this = next[this]
                exitwhen this == 0
            endloop
        endmethod
        
        public static method create takes string s, unit u returns thistype
            local thistype this = rn[0]
            static if ANGLE_RND then
                local real a = GetRandomReal(0, 2*bj_PI)
            else
                local real a = ANGLE
            endif
            if this == 0 then
                set ic = ic + 1
                set this = ic
            else
                set rn[0] = rn[this]
            endif
            
            set .scale = 1.00
            
            set next[this] = 0
            set prev[this] = prev[0]
            set next[prev[0]] = this
            set prev[0] = this
            
            set .s = s
            set .x = GetUnitX(u)
            set .y = GetUnitY(u)
            set .t = TIME_LIFE
            set .as = Sin(a)*VELOCITY
            set .ac = Cos(a)*VELOCITY
            set .ah = 0.
            
            if IsUnitVisible(u, GetLocalPlayer()) then
                set .tt = CreateTextTag()
                call SetTextTagPermanent(.tt, false)
                call SetTextTagLifespan(.tt, TIME_LIFE)
                call SetTextTagFadepoint(.tt, TIME_FADE)
                call SetTextTagText(.tt, s, SIZE_MIN)
                call SetTextTagPos(.tt, .x, .y, Z_OFFSET)
            else
                set .tt = null
            endif
            
            if prev[this] == 0 then
                call TimerStart(TMR, 0.03125, true, function thistype.update)
            endif
            
            set .lastCreated = this
            
            return this
        endmethod
    endstruct
endlibrary
 
Level 11
Joined
May 16, 2020
Messages
655
I just made the below in order to enable that feature. I tested it and plan to use this variant the next time I update the Damage Engine map because it will help certain things to be more subdued or to pop better.

JASS:
// Arcing Text Tag v1.0.1.0 by Maker with added API by Bribe
//
// Added API in 1.0.1.0:
//   public static ArcingTextTag lastCreated
//   - Get the last created ArcingTextTag
//   public real scaling
//   - Set the size ratio of the texttag - 1.00 is the default
//   public real timeScaling
//   - Set the duration ratio of the texttag - 1.00 is the default
library FloatingTextArc
    globals
        private constant    real    SIZE_MIN        = 0.018         // Minimum size of text
        private constant    real    SIZE_BONUS      = 0.012         // Text size increase
        private constant    real    TIME_LIFE       = 1.0           // How long the text lasts
        private constant    real    TIME_FADE       = 0.8           // When does the text start to fade
        private constant    real    Z_OFFSET        = 50            // Height above unit
        private constant    real    Z_OFFSET_BON    = 50            // How much extra height the text gains
        private constant    real    VELOCITY        = 2             // How fast the text move in x/y plane
        private constant    real    ANGLE           = bj_PI/2       // Movement angle of the text. Does not apply if
                                                                    // ANGLE_RND is true
        private constant    boolean ANGLE_RND       = true          // Is the angle random or fixed
        private             timer   TMR             = CreateTimer()
    endglobals
  
    struct ArcingTextTag extends array      
        private texttag tt
        private real as         // angle, sin component
        private real ac         // angle, cos component
        private real ah         // arc height
        private real t          // time
        private real x          // origin x
        private real y          // origin y
        private string s        // text
        private static integer array next
        private static integer array prev
        private static integer array rn
        private static integer ic           = 0       // Instance count 
      
        private real scale
        public method operator scaling takes nothing returns real
            return scale
        endmethod
        public method operator scaling= takes real r returns nothing
            if .tt != null then
                call SetTextTagText(.tt, .s, SIZE_MIN*r)
            endif
            set .scale = r
        endmethod
        private real timeScale
        public method operator timeScaling takes nothing returns real
            return timeScale
        endmethod
        public method operator timeScaling= takes real r returns nothing
            local real p = Sin(bj_PI*r)
            if .tt != null then
                call SetTextTagLifespan(.tt, TIME_LIFE*r)
                call SetTextTagFadepoint(.tt, TIME_FADE*r)
                call SetTextTagPos(.tt, .x, .y, Z_OFFSET + Z_OFFSET_BON*p)
                call SetTextTagText(.tt, .s, (SIZE_MIN + SIZE_BONUS*p)*.scale)
                set .t = r
            endif
            set .t = r
        endmethod
      
        public static thistype lastCreated = 0
      
        private static method update takes nothing returns nothing
            local thistype this=next[0]
            local real p
            loop
                set p = Sin(bj_PI*.t)
                set .t = .t - 0.03125
                set .x = .x + .ac
                set .y = .y + .as
                call SetTextTagPos(.tt, .x, .y, Z_OFFSET + Z_OFFSET_BON*p)
                call SetTextTagText(.tt, .s, (SIZE_MIN + SIZE_BONUS*p)*.scale)
                if .t <= 0 then
                    set .tt = null
                    set next[prev[this]] = next[this]
                    set prev[next[this]] = prev[this]
                    set rn[this] = rn[0]
                    set rn[0] = this
                    if next[0]==0 then
                        call PauseTimer(TMR)
                    endif
                endif
                set this = next[this]
                exitwhen this == 0
            endloop
        endmethod
      
        public static method create takes string s, unit u returns thistype
            local thistype this = rn[0]
            static if ANGLE_RND then
                local real a = GetRandomReal(0, 2*bj_PI)
            else
                local real a = ANGLE
            endif
            if this == 0 then
                set ic = ic + 1
                set this = ic
            else
                set rn[0] = rn[this]
            endif
          
            set .scale = 1.00
          
            set next[this] = 0
            set prev[this] = prev[0]
            set next[prev[0]] = this
            set prev[0] = this
          
            set .s = s
            set .x = GetUnitX(u)
            set .y = GetUnitY(u)
            set .t = TIME_LIFE
            set .as = Sin(a)*VELOCITY
            set .ac = Cos(a)*VELOCITY
            set .ah = 0.
          
            if IsUnitVisible(u, GetLocalPlayer()) then
                set .tt = CreateTextTag()
                call SetTextTagPermanent(.tt, false)
                call SetTextTagLifespan(.tt, TIME_LIFE)
                call SetTextTagFadepoint(.tt, TIME_FADE)
                call SetTextTagText(.tt, s, SIZE_MIN)
                call SetTextTagPos(.tt, .x, .y, Z_OFFSET)
            else
                set .tt = null
            endif
          
            if prev[this] == 0 then
                call TimerStart(TMR, 0.03125, true, function thistype.update)
            endif
          
            set .lastCreated = this
          
            return this
        endmethod
    endstruct
endlibrary

Thanks Bribe as always. But how do I take advantage of the added functions? I guess I need to create a Variable?
 
Level 11
Joined
May 16, 2020
Messages
655
Mmm I did the following:
  1. Updated the JASS code to what you wrote above and
  2. Took the below code over from your map and adjusted it to my needs.
  3. Then I added "set att.scaling = 1.50" below one of the Text which want to increase in text size (the "Instant kill" text)
...But I'm getting the error "att is not of a type that allows . syntax":

  • Damage Tag
    • Events
      • Game - DamageEvent becomes Equal to 1.00
    • Conditions
    • Actions
      • Set VariableSet DmgStr = |cffffffff
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • DamageEventAmount Not equal to 0.00
        • Then - Actions
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • DamageEventAmount Less than -0.99
            • Then - Actions
              • -------- HEALING --------
              • Set VariableSet DmgStr = |cff00ff00+
              • Custom script: call ArcingTextTag.create(udg_DmgStr + I2S(R2I(-udg_DamageEventAmount)) + "|r", udg_DamageEventTarget)
            • Else - Actions
              • -------- Critical Strike --------
              • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                • If - Conditions
                  • DamageScalingUser Greater than or equal to 1.50
                • Then - Actions
                  • Set VariableSet DmgStr = (|cffff0000 + ((String((Integer(DamageEventAmount)))) + !|r))
                  • Custom script: call ArcingTextTag.create(udg_DmgStr, udg_DamageEventTarget)
                • Else - Actions
                  • -------- PLAYER DAMAGE --------
                  • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                    • If - Conditions
                      • DamageEventAmount Greater than 0.99
                      • ((Owner of DamageEventSource) is in (All players controlled by a User player).) Equal to True
                    • Then - Actions
                      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                        • If - Conditions
                          • DamageEventAttackT Equal to ATTACK_TYPE_CHAOS
                        • Then - Actions
                          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                            • If - Conditions
                              • DamageEventAmount Greater than or equal to 10000000.00
                            • Then - Actions
                              • Custom script: call ArcingTextTag.create("|cff7D26CD" + "Instant Kill" + "|r", udg_DamageEventTarget)
                              • Custom script: set att.scaling = 1.50
                              • Skip remaining actions
                            • Else - Actions
                              • Set VariableSet DmgStr = |cffF5F5DC
                        • Else - Actions
                          • -------- PHYSICAL DAMAGE --------
                          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                            • If - Conditions
                              • IsDamageAttack Equal to True
                            • Then - Actions
                              • Set VariableSet DmgStr = |cffffff00
                            • Else - Actions
                              • -------- SPELL DAMAGE --------
                              • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                                • If - Conditions
                                  • IsDamageSpell Equal to True
                                • Then - Actions
                                  • Set VariableSet DmgStr = |cff248dce
                                • Else - Actions
                      • Custom script: call ArcingTextTag.create(udg_DmgStr + I2S(R2I(udg_DamageEventAmount)) + "|r", udg_DamageEventTarget)
                    • Else - Actions
        • Else - Actions
 

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,958
Yes it would require some JASS knowledge in order to utilize local variables in that sense. I haven't yet published my demo script showcasing that feature, so there's nothing I can refer you to yet (which is why I hadn't replied).

Instead of "set att.scaling = 1.50" do "set ArcingTextTag.lastCreated.scaling = 1.50"
 

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,958
What if i want the floating text to display only to a specific player? How do I adjust it so it doesn't desync?
Is it possible to dynamically increase the text size when spell damage is done?

I know how to recognize if spell damage is done, but not how to increase the text afterwards...
The motion of the arc changed when you change the TIME_LIFE variable.

If you want to keep the arc going up then down, then this line:

set p = Sin(bj_PI*.t)

in the update trigger, should become this:

set p = Sin(bj_PI*(.t / TIME_LIFE))

Also, the attribute "ah" seems meaningless? I don't see you using it in the update trigger at all.
These requests have all been added in the new version, below:

JASS:
// Arcing Text Tag v1.0.2.0 by Maker with added API by Bribe with features proposed by Ugabunda and Kusanagi Kuro

library FloatingTextArc
    globals
        private constant    real    SIZE_MIN        = 0.018         // Minimum size of text
        private constant    real    SIZE_BONUS      = 0.012         // Text size increase
        private constant    real    TIME_LIFE       = 1.0           // How long the text lasts
        private constant    real    TIME_FADE       = 0.8           // When does the text start to fade
        private constant    real    Z_OFFSET        = 50            // Height above unit
        private constant    real    Z_OFFSET_BON    = 50            // How much extra height the text gains
        private constant    real    VELOCITY        = 2             // How fast the text move in x/y plane
        private constant    real    ANGLE           = bj_PI/2       // Movement angle of the text. Does not apply if
                                                                    // ANGLE_RND is true
        private constant    boolean ANGLE_RND       = true          // Is the angle random or fixed
        private             timer   TMR             = CreateTimer()
    endglobals
    
    struct ArcingTextTag extends array        
        private texttag tt
        private real as         // angle, sin component
        private real ac         // angle, cos component
        private real t          // time
        private real x          // origin x
        private real y          // origin y
        private string s        // text
        private static integer array next
        private static integer array prev
        private static integer array rn
        private static integer ic           = 0       // Instance count   
        
        private real scale
        private real timeScale
        
        public static thistype lastCreated = 0
        
        private static method update takes nothing returns nothing
            local thistype this=next[0]
            local real p
            loop
                set p = Sin(bj_PI*(.t / timeScale))
                set .t = .t - 0.03125
                set .x = .x + .ac
                set .y = .y + .as
                call SetTextTagPos(.tt, .x, .y, Z_OFFSET + Z_OFFSET_BON*p)
                call SetTextTagText(.tt, .s, (SIZE_MIN + SIZE_BONUS*p)*.scale)
                if .t <= 0 then
                    set .tt = null
                    set next[prev[this]] = next[this]
                    set prev[next[this]] = prev[this]
                    set rn[this] = rn[0]
                    set rn[0] = this
                    if next[0]==0 then
                        call PauseTimer(TMR)
                    endif
                endif
                set this = next[this]
                exitwhen this == 0
            endloop
        endmethod
        
        public static method createEx takes string s, unit u, real duration, real size, player p returns thistype
            local thistype this = rn[0]
            static if ANGLE_RND then
                local real a = GetRandomReal(0, 2*bj_PI)
            else
                local real a = ANGLE
            endif
            if this == 0 then
                set ic = ic + 1
                set this = ic
            else
                set rn[0] = rn[this]
            endif
            
            set .scale = size
            set .timeScale = RMaxBJ(duration, 0.001)
            
            set next[this] = 0
            set prev[this] = prev[0]
            set next[prev[0]] = this
            set prev[0] = this
            
            set .s = s
            set .x = GetUnitX(u)
            set .y = GetUnitY(u)
            set .t = TIME_LIFE
            set .as = Sin(a)*VELOCITY
            set .ac = Cos(a)*VELOCITY
            
            if IsUnitVisible(u, p) then
                set .tt = CreateTextTag()
                call SetTextTagPermanent(.tt, false)
                call SetTextTagLifespan(.tt, TIME_LIFE*duration)
                call SetTextTagFadepoint(.tt, TIME_FADE*duration)
                call SetTextTagText(.tt, s, SIZE_MIN*size)
                call SetTextTagPos(.tt, .x, .y, Z_OFFSET)
            else
                set .tt = null
            endif
            
            if prev[this] == 0 then
                call TimerStart(TMR, 0.03125, true, function thistype.update)
            endif
            
            set .lastCreated = this
            
            return this
        endmethod
        public static method create takes string s, unit u returns thistype
            return thistype.createEx(s, u, TIME_LIFE, 1.00, GetLocalPlayer())
        endmethod
    endstruct
endlibrary
 
Level 28
Joined
Feb 27, 2007
Messages
4,105
For direct healing spells yes. Kind of. For mana gain/loss though direct spells yes, kind of. For passive healing/mana drain or regen no because there is no event that would fire it.

Look at how the example trigger called “Create Floating Text” functions.
 

Bribe

Code Moderator
Level 44
Joined
Sep 26, 2009
Messages
8,958
For direct healing spells yes. Kind of. For mana gain/loss though direct spells yes, kind of. For passive healing/mana drain or regen no because there is no event that would fire it.

Look at how the example trigger called “Create Floating Text” functions.
Heal Event uses this for rapid regen and Heal detection.
 
Top