]> git.rm.cloudns.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'master' into Juhu/strafehud-server
authorJuhu <5894800-Juhu_@users.noreply.gitlab.com>
Tue, 28 Mar 2023 21:11:37 +0000 (23:11 +0200)
committerJuhu <5894800-Juhu_@users.noreply.gitlab.com>
Tue, 28 Mar 2023 21:12:07 +0000 (23:12 +0200)
Fixed merge conflict caused by the new speed unit cvar

1  2 
_hud_common.cfg
qcsrc/client/hud/panel/strafehud.qc
qcsrc/client/hud/panel/strafehud.qh

diff --cc _hud_common.cfg
index de7ead2df484d1b7defae8a63d549c5a40918bc2,6339895eafd312488b037769801bb5611e7e5854..a3b4a5c2026f78843a99b0aa63874abb83243d58
@@@ -150,37 -156,27 +156,36 @@@ seta hud_panel_scoreboard_itemstats_sho
  
  seta _hud_panel_strafehud_demo "0" "strafehud changes angle during configure"
  seta hud_panel_strafehud_mode "0" "strafehud mode which controls whether the strafehud is centered at \"0\" = view angle, \"1\" = velocity angle"
 -seta hud_panel_strafehud_range "0" "the angle range up to 360 degrees displayed on the strafehud, \"0\" = dynamic (chooses the minimum range required to still see the whole area needed for accelerating at once)"
 -seta hud_panel_strafehud_style "1" "\"0\" = no styling, \"1\" = progress bar style for the strafe bar, \"2\" = gradient for the strafe bar"
 +seta hud_panel_strafehud_range "90" "the angle range up to 360 degrees displayed on the strafehud, \"-1\" = current fov, \"0\" = dynamic (chooses the minimum range required to still see the whole area needed for accelerating)"
 +seta hud_panel_strafehud_style "2" "\"0\" = no styling, \"1\" = progress bar style for the strafe bar, \"2\" = gradient for the strafe bar"
- seta hud_panel_strafehud_unit "1" "speed unit (1 = qu/s, 2 = m/s, 3 = km/h, 4 = mph, 5 = knots), length unit (1 = qu, 2 = m, 3 = km, 4 = mi, 5 = nmi)"
  seta hud_panel_strafehud_unit_show "1" "show units"
 +seta hud_panel_strafehud_uncapped "0" "set to \"1\" to remove some safety restrictions, useful to set thinner indicator lines down to 1px or for trying out higher values for some performance degrading operations (warning: elements may turn invisible if too thin, other configurations may crash your game or look horribly ugly)"
 +seta hud_panel_strafehud_bar_preaccel "1" "set to \"1\" to extend the acceleration zone by the strafe meter zone before full acceleration can be achieved"
  seta hud_panel_strafehud_bar_neutral_color "1 1 1" "color of the strafe meter neutral zone"
 -seta hud_panel_strafehud_bar_neutral_alpha "0.3" "opacity of the strafe meter neutral zone"
 +seta hud_panel_strafehud_bar_neutral_alpha "0.1" "opacity of the strafe meter neutral zone"
  seta hud_panel_strafehud_bar_accel_color "0 1 0" "color of the strafe meter acceleration zone"
 -seta hud_panel_strafehud_bar_accel_alpha "0.3" "opacity of the strafe meter acceleration zone"
 +seta hud_panel_strafehud_bar_accel_alpha "0.5" "opacity of the strafe meter acceleration zone"
  seta hud_panel_strafehud_bar_overturn_color "1 0 1" "color of the strafe meter overturn zone"
 -seta hud_panel_strafehud_bar_overturn_alpha "0.3" "opacity of the strafe meter overturn zone"
 +seta hud_panel_strafehud_bar_overturn_alpha "0.5" "opacity of the strafe meter overturn zone"
 +seta hud_panel_strafehud_angle_style "0" "set the angle indicator style: 0 = none, 1 = solid line, 2 = dashed line"
 +seta hud_panel_strafehud_angle_dashes "4" "determines the amount of dashes if the angle indicator uses a dashed line"
  seta hud_panel_strafehud_angle_alpha "0.8" "opacity of the indicator showing the player's current angle"
 -seta hud_panel_strafehud_angle_height "1.5" "height of the indicator showing the player's current angle (relative to the panel height)"
 -seta hud_panel_strafehud_angle_width "0.005" "width of the indicator showing the player's current angle (relative to the panel width)"
 -seta hud_panel_strafehud_angle_neutral_color "1 1 0" "color of the indicator showing the player's current angle if the player's angle is within the neutral zone"
 -seta hud_panel_strafehud_angle_accel_color "0 1 1" "color of the indicator showing the player's current angle if the player's angle is within the acceleration zone"
 -seta hud_panel_strafehud_angle_overturn_color "1 0 1" "color of the indicator showing the player's current angle if the player's angle is within the overturn zone"
 -seta hud_panel_strafehud_switch_minspeed "-1" "minimum speed in qu/s at which switch indicators which are used to aid changing strafe direction will be shown (uses physics maxspeed + antiflicker speed if negative)"
 -seta hud_panel_strafehud_switch_active_color "0 1 0" "color of the switch indicator on the current side"
 -seta hud_panel_strafehud_switch_active_alpha "1" "opacity of the switch indicator on the current side"
 -seta hud_panel_strafehud_switch_inactive_color "1 1 0" "color of the switch indicator on the opposite side"
 -seta hud_panel_strafehud_switch_inactive_alpha "1" "opacity of the switch indicator on the opposite side"
 -seta hud_panel_strafehud_switch_width "0.0075" "width of the strafe angle indicators (relative to the strafe bar width)"
 +seta hud_panel_strafehud_angle_height "1" "height of the indicator showing the player's current angle (relative to the panel height)"
 +seta hud_panel_strafehud_angle_width "0.001" "width of the indicator showing the player's current angle (relative to the panel width)"
 +seta hud_panel_strafehud_angle_neutral_color "1 1 0" "color of the indicator showing the player's current angle if it is within the neutral zone"
 +seta hud_panel_strafehud_angle_accel_color "0 1 1" "color of the indicator showing the player's current angle if it is within the acceleration zone"
 +seta hud_panel_strafehud_angle_overturn_color "1 0 1" "color of the indicator showing the player's current angle if it is within the overturn zone"
 +seta hud_panel_strafehud_angle_arrow "1" "set the angle indicator's arrow style: 0 = none, 1 = top, 2 = bottom, 3 = both"
 +seta hud_panel_strafehud_angle_arrow_size "0.5" "size of the arrow (relative to the panel height)"
 +seta hud_panel_strafehud_bestangle "1" "set to \"1\" to enable a ghost angle indicator showing the best angle to gain maximum acceleration"
 +seta hud_panel_strafehud_bestangle_color "1 1 1" "color of the indicator showing the best angle to gain maximum acceleration"
 +seta hud_panel_strafehud_bestangle_alpha "0.5" "opacity of the indicator showing the best angle to gain maximum acceleration"
 +seta hud_panel_strafehud_switch "1" "set to \"1\" to enable the switch indicator showing the angle to move to when switching sides"
 +seta hud_panel_strafehud_switch_minspeed "-1" "minimum speed in qu/s at which switch indicator(s) which are used to aid changing strafe direction will be shown (set to -1 for dynamic minspeed)"
 +seta hud_panel_strafehud_switch_color "1 1 0" "color of the switch indicator"
 +seta hud_panel_strafehud_switch_alpha "1" "opacity of the switch indicator"
 +seta hud_panel_strafehud_switch_width "0.003" "width of the strafe angle indicator(s) (relative to the strafe bar width)"
 +seta hud_panel_strafehud_direction "0" "set to \"1\" to enable the direction caps to see in which direction you are currently strafing"
  seta hud_panel_strafehud_direction_color "0 0.5 1" "color of the direction caps which indicate the direction the player is currently strafing towards"
  seta hud_panel_strafehud_direction_alpha "1" "opacity of the direction caps which indicate the direction the player is currently strafing towards"
  seta hud_panel_strafehud_direction_width "0.25" "stroke width of the direction caps which indicate the direction the player is currently strafing towards (relative to the panel height)"
index 126a7514ab5a1de57db7f6e673d2d6837770d260,46e8b7631b4eb7d3529f622c491cf9222ae00e86..45901c9229c3d990fec67dc1a666e49a68afd380
@@@ -99,69 -96,31 +99,69 @@@ void HUD_StrafeHUD(
      // draw strafehud
      if(csqcplayer && strafeplayer)
      {
 +        float strafe_waterlevel;
 +
 +        // check the player waterlevel without affecting the player entity, this way we can fetch waterlevel even if client prediction is disabled
 +        {
 +            // store old values
 +            void old_contentstransition(int, int) = strafeplayer.contentstransition;
 +            float old_watertype = strafeplayer.watertype;
 +            float old_waterlevel = strafeplayer.waterlevel;
 +
 +            strafeplayer.contentstransition = func_null; // unset the contentstransition function if present
 +            _Movetype_CheckWater(strafeplayer);
 +            strafe_waterlevel = strafeplayer.waterlevel; // store the player waterlevel
 +
 +            // restore old values
 +            strafeplayer.contentstransition = old_contentstransition;
 +            strafeplayer.watertype = old_watertype;
 +            strafeplayer.waterlevel = old_waterlevel;
 +        }
 +
 +        // persistent
 +        static float onground_lasttime       = 0;
 +        static bool  onslick_last            = false;
 +        static float turn_lasttime           = 0;
 +        static bool  turn                    = false;
 +        static float turnangle;
 +        static float dt_update               = 0;
 +        static int   dt_time                 = 0;
 +        static float dt_sum                  = 0;
 +        static float dt                      = 0;
 +
          // physics
 -        bool   onground                      = islocal ? IS_ONGROUND(strafeplayer) : !(strafeplayer.anim_implicit_state & ANIMIMPLICITSTATE_INAIR);
 +        int    keys                          = STAT(PRESSED_KEYS);
 +        bool   jumpheld                      = (islocal ? ((PHYS_INPUT_BUTTON_JUMP(strafeplayer) || PHYS_INPUT_BUTTON_JETPACK(strafeplayer))) && !PHYS_CL_TRACK_CANJUMP(strafeplayer) : (keys & KEY_JUMP)) && !PHYS_TRACK_CANJUMP(strafeplayer); // try to ignore if track_canjump is enabled, doesn't work in spectator mode if spectated player uses +jetpack or cl_movement_track_canjump
 +        bool   real_onground                 = islocal ? IS_ONGROUND(strafeplayer) : !(strafeplayer.anim_implicit_state & ANIMIMPLICITSTATE_INAIR); // doesn't get changed by ground timeout and isn't affected by jump input
 +        bool   real_onslick                  = false; // doesn't get changed by ground timeout
 +        bool   onground                      = real_onground && !jumpheld; // if jump is held assume we are in air, avoids flickering of the hud when hitting the ground
 +        bool   onslick                       = real_onslick;
 +        bool   onground_expired;
          bool   strafekeys;
 -        bool   swimming                      = strafeplayer.waterlevel >= WATERLEVEL_SWIMMING;
 -        bool   spectating                    = entcs_GetSpecState(strafeplayer.sv_entnum) == ENTCS_SPEC_PURE;
 +        bool   swimming                      = strafe_waterlevel >= WATERLEVEL_SWIMMING; // the hud will not work well while swimming
          float  speed                         = !autocvar__hud_configure ? vlen(vec2(csqcplayer.velocity)) : 1337; // use local csqcmodel entity for this even when spectating, flickers too much otherwise
 -        float  maxspeed_crouch_mod           = IS_DUCKED(strafeplayer) && !swimming ? .5 : 1;
 -        float  maxspeed_water_mod            = swimming ? .7 : 1; // very simplified water physics, the hud will not work well (and is not supposed to) while swimming
 +        float  maxspeed_mod                  = IS_DUCKED(csqcplayer) ? .5 : 1; // only the local csqcplayer entity contains this information even when spectating
          float  maxspeed_phys                 = onground ? PHYS_MAXSPEED(strafeplayer) : PHYS_MAXAIRSPEED(strafeplayer);
 -        float  maxspeed                      = !autocvar__hud_configure ? maxspeed_phys * maxspeed_crouch_mod * maxspeed_water_mod : 320;
 -        float  vel_angle                     = vectoangles(strafeplayer.velocity).y;
 -        float  view_angle                    = PHYS_INPUT_ANGLES(strafeplayer).y + 180;
 +        float  maxspeed                      = !autocvar__hud_configure ? maxspeed_phys * maxspeed_mod : 320;
 +        float  movespeed;
 +        float  bestspeed;
 +        float  maxaccel_phys                 = onground ? PHYS_ACCELERATE(strafeplayer) : PHYS_AIRACCELERATE(strafeplayer);
 +        float  maxaccel                      = !autocvar__hud_configure ? maxaccel_phys : 1;
 +        float  vel_angle                     = vectoangles(strafeplayer.velocity).y - (vectoangles(strafeplayer.velocity).y > 180 ? 360 : 0); // change the range from 0° - 360° to -180° - 180° to match how view_angle represents angles
 +        float  view_angle                    = PHYS_INPUT_ANGLES(strafeplayer).y;
          float  angle;
          vector movement                      = PHYS_INPUT_MOVEVALUES(strafeplayer);
 -        int    keys                          = STAT(PRESSED_KEYS);
 +        bool   fwd;
          int    keys_fwd;
 -        float  wishangle                     = 0;
 +        float  wishangle;
 +        int    direction;
  
          // HUD
 -        int    mode                          = autocvar_hud_panel_strafehud_mode >= 0 && autocvar_hud_panel_strafehud_mode <= 1 ? autocvar_hud_panel_strafehud_mode : 0;
 +        int    mode                          = autocvar_hud_panel_strafehud_mode >= 0 && autocvar_hud_panel_strafehud_mode <= 1 ? autocvar_hud_panel_strafehud_mode : STRAFEHUD_MODE_VIEW_CENTERED;
-         float  speed_conversion_factor       = GetSpeedUnitFactor(autocvar_hud_panel_strafehud_unit);
-         float  length_conversion_factor      = GetLengthUnitFactor(autocvar_hud_panel_strafehud_unit);
-         int    length_decimals               = autocvar_hud_panel_strafehud_unit >= 3 && autocvar_hud_panel_strafehud_unit <= 5 ? 6 : 2; // use more decimals when displaying km or miles
+         float  speed_conversion_factor       = GetSpeedUnitFactor(autocvar_hud_speed_unit);
+         float  length_conversion_factor      = GetLengthUnitFactor(autocvar_hud_speed_unit);
+         int    length_decimals               = autocvar_hud_speed_unit >= 3 && autocvar_hud_speed_unit <= 5 ? 6 : 2; // use more decimals when displaying km or miles
          float  antiflicker_angle             = bound(0, autocvar_hud_panel_strafehud_antiflicker_angle, 180);
 -        float  antiflicker_speed             = max(0, autocvar_hud_panel_strafehud_antiflicker_speed);
          float  minspeed;
          float  shift_offset                  = 0;
          bool   straight_overturn             = false;
                  // bottom horizontal line
                  drawfill(panel_pos + eY * panel_size.y, slickdetector_size, autocvar_hud_panel_strafehud_slickdetector_color, autocvar_hud_panel_strafehud_slickdetector_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
              }
 +
 +            text_offset_top = text_offset_bottom = slickdetector_height;
          }
  
 -        draw_beginBoldFont();
 -        // show speed when crossing the start trigger
 -        if(autocvar_hud_panel_strafehud_startspeed_fade > 0)
 +        if(autocvar_hud_panel_strafehud_direction && direction != STRAFEHUD_DIRECTION_NONE && direction_size_vertical.x > 0 && autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha > 0)
          {
 -            float text_alpha = 0;
 -            if(race_checkpoint == 254) // checkpoint 254 is the start trigger
 -            {
 -                if(starttime != race_checkpointtime)
 -                {
 -                    starttime = race_checkpointtime;
 -                    startspeed = speed;
 -                }
 -            }
 -            if(startspeed >= 0)
 +            bool indicator_direction = direction == STRAFEHUD_DIRECTION_LEFT;
 +            // invert left/right when strafing backwards or when strafing towards the opposite side indicated by the direction variable
 +            // if both conditions are true then it's inverted twice hence not inverted at all
 +            if(!fwd != odd_angles)
              {
 -                text_alpha = cos(((time - starttime) / autocvar_hud_panel_strafehud_startspeed_fade) * 90 * DEG2RAD); // fade non-linear like the physics panel does
 -                if((time - starttime) > autocvar_hud_panel_strafehud_startspeed_fade)
 -                {
 -                    startspeed = -1;
 -                }
 -            }
 -            if(startspeed >= 0 && text_alpha > 0 && autocvar_hud_panel_strafehud_startspeed_size > 0)
 -            {
 -                vector startspeed_size = panel_size;
 -                startspeed_size.y = panel_size.y * min(autocvar_hud_panel_strafehud_startspeed_size, 5);
 -                string speed_unit = GetSpeedUnit(autocvar_hud_speed_unit);
 -                drawstring_aspect(panel_pos + eY * panel_size.y, strcat(ftos_decimals(startspeed * speed_conversion_factor, 2), autocvar_hud_panel_strafehud_unit_show ? speed_unit : ""), startspeed_size, autocvar_hud_panel_strafehud_startspeed_color, text_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
 +                indicator_direction = !indicator_direction;
              }
 +            // draw the direction indicator caps at the sides of the hud
 +            // vertical line
 +            if(direction_size_vertical.y > 0) drawfill(panel_pos - eY * direction_size_horizontal.y + eX * (indicator_direction ? -direction_size_vertical.x : panel_size.x), direction_size_vertical, autocvar_hud_panel_strafehud_direction_color, autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
 +            // top horizontal line
 +            drawfill(panel_pos + eX * (indicator_direction ? 0 : panel_size.x - direction_size_horizontal.x) - eY * direction_size_horizontal.y, direction_size_horizontal, autocvar_hud_panel_strafehud_direction_color, autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
 +            // bottom horizontal line
 +            drawfill(panel_pos + eX * (indicator_direction ? 0 : panel_size.x - direction_size_horizontal.x) + eY * panel_size.y, direction_size_horizontal, autocvar_hud_panel_strafehud_direction_color, autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
          }
 -        else
 -        {
 -            starttime = 0;
 -            startspeed = -1;
 +
 +        string newsound = autocvar_hud_panel_strafehud_sonar_audio;
 +        static string cursound = string_null;
 +        static string sonarsound = string_null;
 +        if(newsound == "") {
 +            cursound = sonarsound = string_null;
 +        }
 +        else if(newsound != cursound) {
 +            strfree(cursound);
 +            cursound = strzone(newsound);
 +
 +            strfree(sonarsound);
 +            sonarsound = _Sound_fixpath(newsound);
 +            if(sonarsound) {
 +                sonarsound = strzone(sonarsound);
 +                precache_sound(sonarsound);
 +            }
          }
  
 -        // show height achieved by a single jump
 -        if(autocvar_hud_panel_strafehud_jumpheight_fade > 0 && autocvar_hud_panel_strafehud_jumpheight_size > 0)
 -        {
 -            static float height_min = 0, height_max = 0; // ground and peak of jump z coordinates
 -            static float jumpheight = 0, jumptime = 0;   // displayed value and timestamp for fade out
 +        // draw the actual strafe angle
 +        if(!immobile) {
 +            float moveangle = fabs(angle + wishangle);
 +            float strafe_ratio = 0;
  
 -            // tries to catch kill and spectate but those are not reliable, should just hook to kill/spectate/teleport and reset jump height there
 -            if((strafeplayer.velocity.z <= 0 && height_max >= strafeplayer.origin.z) || IS_ONGROUND(strafeplayer) || swimming || IS_DEAD(strafeplayer) || spectating)
 +            // player is overturning
 +            if(moveangle >= 90)
              {
 -                height_min = height_max = strafeplayer.origin.z;
 +                currentangle_color = autocvar_hud_panel_strafehud_angle_overturn_color;
 +                strafe_ratio = (moveangle - 90) / 90;
 +                if(strafe_ratio > 1) strafe_ratio = 2 - strafe_ratio;
 +                strafe_ratio *= -1;
              }
 -            else if(strafeplayer.origin.z > height_max)
 +            // player gains speed by strafing
 +            else if(moveangle >= real_bestangle)
              {
 -                height_max = strafeplayer.origin.z;
 -                jumpheight = (height_max - height_min) * length_conversion_factor;
 -
 -                if(jumpheight > max(autocvar_hud_panel_strafehud_jumpheight_min, 0))
 -                    jumptime = time;
 +                currentangle_color = autocvar_hud_panel_strafehud_angle_accel_color;
 +                strafe_ratio = (90 - moveangle) / (90 - real_bestangle);
 +            }
 +            else if(moveangle >= real_prebestangle)
 +            {
 +                if(autocvar_hud_panel_strafehud_bar_preaccel)
 +                    currentangle_color = autocvar_hud_panel_strafehud_angle_accel_color;
 +                strafe_ratio = (moveangle - real_prebestangle) / (real_bestangle - real_prebestangle);
              }
  
 -            if((time - jumptime) <= autocvar_hud_panel_strafehud_jumpheight_fade)
 +            if(autocvar_hud_panel_strafehud_style == STRAFEHUD_STYLE_GRADIENT)
              {
 -                float text_alpha = cos(((time - jumptime) / autocvar_hud_panel_strafehud_jumpheight_fade) * 90 * DEG2RAD); // fade non-linear like the physics panel does
 -                vector jumpheight_size = panel_size;
 -                jumpheight_size.y = panel_size.y * min(autocvar_hud_panel_strafehud_jumpheight_size, 5);
 -                string length_unit = GetLengthUnit(autocvar_hud_speed_unit);
 -                drawstring_aspect(panel_pos - eY * jumpheight_size.y, strcat(ftos_decimals(jumpheight, length_decimals), autocvar_hud_panel_strafehud_unit_show ? length_unit : ""), jumpheight_size, autocvar_hud_panel_strafehud_jumpheight_color, text_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
 +                currentangle_color = StrafeHUD_mixColors(autocvar_hud_panel_strafehud_angle_neutral_color, currentangle_color, fabs(strafe_ratio));
              }
 -        }
 -        draw_endBoldFont();
  
 -        if(speed < (maxspeed + antiflicker_speed) && !immobile)
 -        {
 -            bestangle_anywhere = true; // moving forward should suffice to gain speed
 -        }
 +            // reuse strafe ratio for strafe sonar
 +            static float sonar_time = 0;
  
 -        // draw the actual strafe angle
 -        if(!bestangle_anywhere && !immobile) // player gains speed with strafing
 -        {
 -            if((direction > 0 && (angle >= bestangle || angle <= -(bestangle + wishangle*2))) ||
 -               (direction < 0 && (angle <= bestangle || angle >= -(bestangle + wishangle*2))))
 -            currentangle_color = autocvar_hud_panel_strafehud_angle_accel_color;
 +            float sonar_start = bound(0, autocvar_hud_panel_strafehud_sonar_start, 1);
 +            float sonar_ratio = strafe_ratio - sonar_start;
 +            if(sonar_start != 1)
 +                sonar_ratio /= 1 - sonar_start;
 +            else
 +                sonar_ratio = 1;
 +
 +            float sonar_interval = max(0, autocvar_hud_panel_strafehud_sonar_interval_start);
 +            sonar_interval += autocvar_hud_panel_strafehud_sonar_interval_range * sonar_ratio ** max(1, autocvar_hud_panel_strafehud_sonar_interval_exponent);
 +            bool sonar_ready = (sonar_time == 0) || ((time - sonar_time) >= sonar_interval);
 +            if(autocvar_hud_panel_strafehud_sonar && sonar_ready && (strafe_ratio >= sonar_start)) {
 +                sonar_time = time;
 +
 +                float sonar_volume = bound(0, autocvar_hud_panel_strafehud_sonar_volume_start, 1);
 +                sonar_volume += autocvar_hud_panel_strafehud_sonar_volume_range * sonar_ratio ** max(1, autocvar_hud_panel_strafehud_sonar_volume_exponent);
 +
 +                float sonar_pitch = max(0, autocvar_hud_panel_strafehud_sonar_pitch_start);
 +                sonar_pitch += autocvar_hud_panel_strafehud_sonar_pitch_range * sonar_ratio ** max(1, autocvar_hud_panel_strafehud_sonar_pitch_exponent);
 +
 +                if(sonarsound && (sonar_volume > 0)) {
 +                    sound7(csqcplayer, CH_INFO, sonarsound, bound(0, sonar_volume, 1) * VOL_BASE, ATTN_NONE, max(0.000001, sonar_pitch * 100), 0);
 +                }
 +            }
          }
  
 -        if(fabs(angle + wishangle) > 90) // player is overturning
 +        if(mode == STRAFEHUD_MODE_VIEW_CENTERED || straight_overturn)
          {
 -            currentangle_color = autocvar_hud_panel_strafehud_angle_overturn_color;
 +            currentangle_offset = panel_size.x/2;
          }
 -        else if(bestangle_anywhere) // player gains speed without strafing
 +
 +        float angleheight_offset = currentangle_size.y;
 +        float ghost_offset = 0;
 +        if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE)
          {
 -            currentangle_color = autocvar_hud_panel_strafehud_angle_accel_color;
 +            ghost_offset = bound(0, (odd_angles ? odd_bestangle_offset : bestangle_offset), panel_size.x);
          }
  
 -        if(mode == 0 || straight_overturn)
 +        currentangle_offset = StrafeHUD_projectOffset(currentangle_offset, hudangle);
 +        ghost_offset = StrafeHUD_projectOffset(ghost_offset, hudangle);
 +
 +        switch(autocvar_hud_panel_strafehud_angle_style)
          {
 -            currentangle_offset = panel_size.x/2;
 +            case STRAFEHUD_INDICATOR_SOLID:
 +                if(currentangle_size.x > 0 && currentangle_size.y > 0)
 +                {
 +                    if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE) drawfill(panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2) + eX * (ghost_offset - currentangle_size.x/2), currentangle_size, autocvar_hud_panel_strafehud_bestangle_color, autocvar_hud_panel_strafehud_bestangle_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
 +                    drawfill(panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2) + eX * (currentangle_offset - currentangle_size.x/2), currentangle_size, currentangle_color, autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
 +                }
 +                break;
 +            case STRAFEHUD_INDICATOR_DASHED:
 +                if(currentangle_size.x > 0 && currentangle_size.y > 0)
 +                {
 +                    vector line_size = currentangle_size;
 +                    line_size.y = currentangle_size.y / (bound(2, autocvar_hud_panel_strafehud_angle_dashes, currentangle_size.y)*2-1);
 +                    for(float i = 0; i < currentangle_size.y; i += line_size.y*2)
 +                    {
 +                        if(i + line_size.y*2 >= currentangle_size.y) line_size.y = currentangle_size.y - i;
 +                        if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE) drawfill(panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2 - i) + eX * (ghost_offset - line_size.x/2), line_size, autocvar_hud_panel_strafehud_bestangle_color, autocvar_hud_panel_strafehud_bestangle_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
 +                        drawfill(panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2 - i) + eX * (currentangle_offset - line_size.x/2), line_size, currentangle_color, autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
 +                    }
 +                }
 +                break;
 +            case STRAFEHUD_INDICATOR_NONE:
 +            default:
 +                // don't offset text and arrows if the angle indicator line isn't drawn
 +                angleheight_offset = panel_size.y;
 +                currentangle_size = '0 0 0';
          }
  
 -        if(autocvar_hud_panel_strafehud_style == 2 && !immobile)
 +        float angle_offset_top = 0, angle_offset_bottom = 0;
 +
 +        // offset text if any angle indicator is drawn
 +        if((autocvar_hud_panel_strafehud_angle_alpha > 0) || (autocvar_hud_panel_strafehud_bestangle && autocvar_hud_panel_strafehud_bestangle_alpha > 0))
 +            angle_offset_top = angle_offset_bottom = (angleheight_offset - panel_size.y) / 2; // offset text by amount the angle indicator extrudes from the strafehud bar
 +
 +        if(autocvar_hud_panel_strafehud_angle_arrow > 0)
          {
 -            float moveangle = angle + wishangle;
 -            float strafeangle = (bestangle + wishangle) * (direction < 0 ? -1 : 1);
 -            float strafe_ratio = 0;
 -            if(fabs(moveangle) > 90)
 +            if(arrow_size > 0)
              {
 -                strafe_ratio = -((fabs(moveangle) - 90) / 90);
 -                if(strafe_ratio < -1) strafe_ratio = -2 - strafe_ratio;
 -            }
 -            else
 -            {
 -                if(moveangle >= strafeangle)
 +                if(autocvar_hud_panel_strafehud_angle_arrow == 1 || autocvar_hud_panel_strafehud_angle_arrow >= 3)
                  {
 -                    strafe_ratio = 1 - (moveangle - strafeangle) / (90 - strafeangle);
 +                    if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE) StrafeHUD_drawStrafeArrow(panel_pos + eY * ((panel_size.y - angleheight_offset) / 2) + eX * ghost_offset, arrow_size, autocvar_hud_panel_strafehud_bestangle_color, autocvar_hud_panel_strafehud_bestangle_alpha * panel_fg_alpha, true, currentangle_size.x);
 +                    StrafeHUD_drawStrafeArrow(panel_pos + eY * ((panel_size.y - angleheight_offset) / 2) + eX * currentangle_offset, arrow_size, currentangle_color, autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha, true, currentangle_size.x);
 +
 +                    angle_offset_top += arrow_size; // further offset the top text offset if the top arrow is drawn
                  }
 -                else if(moveangle <= -strafeangle)
 +                if(autocvar_hud_panel_strafehud_angle_arrow >= 2)
                  {
 -                    strafe_ratio = 1 - (moveangle + strafeangle) / (-90 + strafeangle);
 +                    if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE) StrafeHUD_drawStrafeArrow(panel_pos + eY * ((panel_size.y - angleheight_offset) / 2 + angleheight_offset) + eX * ghost_offset, arrow_size, autocvar_hud_panel_strafehud_bestangle_color, autocvar_hud_panel_strafehud_bestangle_alpha * panel_fg_alpha, false, currentangle_size.x);
 +                    StrafeHUD_drawStrafeArrow(panel_pos + eY * ((panel_size.y - angleheight_offset) / 2 + angleheight_offset) + eX * currentangle_offset, arrow_size, currentangle_color, autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha, false, currentangle_size.x);
 +
 +                    angle_offset_bottom += arrow_size; // further offset the bottom text offset if the bottom arrow is drawn
                  }
              }
 -            if(strafe_ratio < 0)
 +        }
 +
 +        // make sure text doesn't draw inside the strafehud bar
 +        text_offset_top = max(angle_offset_top, text_offset_top);
 +        text_offset_bottom = max(angle_offset_bottom, text_offset_bottom);
 +
 +        // vertical angle for weapon jumps
 +        {
 +            if(autocvar_hud_panel_strafehud_vangle)
              {
 -                currentangle_color = StrafeHUD_mixColors(autocvar_hud_panel_strafehud_angle_neutral_color, autocvar_hud_panel_strafehud_angle_overturn_color, -strafe_ratio);
 +                float vangle = -PHYS_INPUT_ANGLES(strafeplayer).x;
 +                float vangle_height = autocvar_hud_panel_strafehud_vangle_size * panel_size.y;
 +                string vangle_text = strcat(ftos_decimals(vangle, 2), "°");
 +
 +                if(StrafeHUD_drawTextIndicator(vangle_text, vangle_height, autocvar_hud_panel_strafehud_vangle_color, 1, time, text_offset_bottom, STRAFEHUD_TEXT_BOTTOM))
 +                    text_offset_bottom += vangle_height;
              }
 -            else
 +        }
 +
 +        draw_beginBoldFont();
 +
 +        // show speed when crossing the start trigger
 +        {
 +            static float startspeed = 0, starttime = 0; // displayed value and timestamp for fade out
 +
 +            if((race_nextcheckpoint == 1) || (race_checkpoint == 254 && race_nextcheckpoint == 255)) // check if the start trigger was hit (will also trigger if the finish trigger was hit if those have the same ID)
              {
 -                currentangle_color = StrafeHUD_mixColors(autocvar_hud_panel_strafehud_angle_neutral_color, autocvar_hud_panel_strafehud_angle_accel_color, strafe_ratio);
 +                if((race_checkpointtime > 0) && (starttime != race_checkpointtime))
 +                {
 +                    starttime = race_checkpointtime;
 +                    startspeed = speed;
 +                }
 +            }
 +
 +            if(autocvar_hud_panel_strafehud_startspeed)
 +            {
 +                float startspeed_height = autocvar_hud_panel_strafehud_startspeed_size * panel_size.y;
 +                string startspeed_text = ftos_decimals(startspeed * speed_conversion_factor, 2);
 +                if(autocvar_hud_panel_strafehud_unit_show)
-                     startspeed_text = strcat(startspeed_text, GetSpeedUnit(autocvar_hud_panel_strafehud_unit));
++                    startspeed_text = strcat(startspeed_text, GetSpeedUnit(autocvar_hud_speed_unit));
 +
 +                if(StrafeHUD_drawTextIndicator(startspeed_text, startspeed_height, autocvar_hud_panel_strafehud_startspeed_color, autocvar_hud_panel_strafehud_startspeed_fade, starttime, text_offset_bottom, STRAFEHUD_TEXT_BOTTOM))
 +                    text_offset_bottom += startspeed_height;
              }
          }
  
 -        if(currentangle_size.x > 0 && currentangle_size.y > 0 && autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha > 0)
 +        // show height achieved by a single jump
 +        // FIXME: checking z position differences is unreliable (warpzones, teleporter, kill, etc) but using velocity to calculate jump height would be
 +        //        inaccurate in hud code (possibly different tick rate than physics, doesn't run when hud isn't drawn, rounding errors)
          {
 -            drawfill(panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2) + eX * (currentangle_offset - currentangle_size.x/2), currentangle_size, currentangle_color, autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
 +            static float height_min = 0, height_max = 0; // ground and peak of jump z coordinates
 +            static float jumpheight = 0, jumptime = 0;   // displayed value and timestamp for fade out
 +
 +            // tries to catch kill and spectate but those are not reliable
 +            if((strafeplayer.velocity.z <= 0) || real_onground || swimming || IS_DEAD(strafeplayer) || !IS_PLAYER(strafeplayer))
 +            {
 +                height_min = height_max = strafeplayer.origin.z;
 +            }
 +            else if(strafeplayer.origin.z > height_max)
 +            {
 +                height_max = strafeplayer.origin.z;
 +                float jumpheight_new = height_max - height_min;
 +
 +                if((jumpheight_new * length_conversion_factor) > max(autocvar_hud_panel_strafehud_jumpheight_min, 0))
 +                {
 +                    jumpheight = jumpheight_new;
 +                    jumptime = time;
 +                }
 +            }
 +
 +            if(autocvar_hud_panel_strafehud_jumpheight)
 +            {
 +                float jumpheight_height = autocvar_hud_panel_strafehud_jumpheight_size * panel_size.y;
 +                string jumpheight_text = ftos_decimals(jumpheight * length_conversion_factor, length_decimals);
 +                if(autocvar_hud_panel_strafehud_unit_show)
-                     jumpheight_text = strcat(jumpheight_text, GetLengthUnit(autocvar_hud_panel_strafehud_unit));
++                    jumpheight_text = strcat(jumpheight_text, GetLengthUnit(autocvar_hud_speed_unit));
 +
 +                if(StrafeHUD_drawTextIndicator(jumpheight_text, jumpheight_height, autocvar_hud_panel_strafehud_jumpheight_color, autocvar_hud_panel_strafehud_jumpheight_fade, jumptime, text_offset_top, STRAFEHUD_TEXT_TOP))
 +                    text_offset_top += jumpheight_height;
 +            }
          }
 +
 +        draw_endBoldFont();
      }
 +    hud_lasttime = time;
 +}
 +
 +float StrafeHUD_projectOffset(float offset, float range) {
 +    range *= DEG2RAD / 2;
 +    float angle = (offset - (panel_size.x/2)) / (panel_size.x/2) * range;
 +    switch(autocvar_hud_panel_strafehud_projection) {
 +        default:
 +        case STRAFEHUD_PROJECTION_LINEAR:
 +            return offset;
 +        case STRAFEHUD_PROJECTION_PERSPECTIVE:
 +            offset = tan(angle) / tan(range);
 +            break;
 +        case STRAFEHUD_PROJECTION_PANORAMIC:
 +            offset = tan(angle/2) / tan(range/2);
 +            break;
 +    }
 +    offset = offset * (panel_size.x/2) + (panel_size.x/2);
 +    return offset;
 +}
 +
 +float StrafeHUD_projectWidth(float offset, float width, float range) {
 +    return StrafeHUD_projectOffset(offset + width, range) - StrafeHUD_projectOffset(offset, range);
  }
  
  // functions to make hud elements align perfectly in the hud area
index 1650ab61cc0b27d72677ea012c9e018a49e2f0a7,64a78c981466ec9a8eda3a631c63f2561694b119..894515fce69f3ed1881358b14dcaf7c537902043
  #pragma once
  #include "../panel.qh"
  
 -int autocvar_hud_panel_strafehud = 3;
 -bool autocvar__hud_panel_strafehud_demo = false;
 -bool autocvar_hud_panel_strafehud_dynamichud    = true;
 -int autocvar_hud_panel_strafehud_mode = 0;
 -float autocvar_hud_panel_strafehud_range = 0;
 -int autocvar_hud_panel_strafehud_style = 1;
 -bool autocvar_hud_panel_strafehud_unit_show = true;
 -vector autocvar_hud_panel_strafehud_bar_neutral_color = '1 1 1';
 -float autocvar_hud_panel_strafehud_bar_neutral_alpha = 0.3;
 -vector autocvar_hud_panel_strafehud_bar_accel_color = '0 1 0';
 -float autocvar_hud_panel_strafehud_bar_accel_alpha = 0.3;
 -vector autocvar_hud_panel_strafehud_bar_overturn_color = '1 0 1';
 -float autocvar_hud_panel_strafehud_bar_overturn_alpha = 0.3;
 -float autocvar_hud_panel_strafehud_angle_alpha = 0.8;
 -float autocvar_hud_panel_strafehud_angle_height = 1.5;
 -float autocvar_hud_panel_strafehud_angle_width = 0.005;
 -vector autocvar_hud_panel_strafehud_angle_neutral_color = '1 1 0';
 -vector autocvar_hud_panel_strafehud_angle_accel_color = '0 1 1';
 -vector autocvar_hud_panel_strafehud_angle_overturn_color = '1 0 1';
 -float autocvar_hud_panel_strafehud_switch_minspeed = -1;
 -vector autocvar_hud_panel_strafehud_switch_active_color = '0 1 0';
 -float autocvar_hud_panel_strafehud_switch_active_alpha = 1;
 -vector autocvar_hud_panel_strafehud_switch_inactive_color = '1 1 0';
 -float autocvar_hud_panel_strafehud_switch_inactive_alpha = 1;
 -float autocvar_hud_panel_strafehud_switch_width = 0.0075;
 -vector autocvar_hud_panel_strafehud_direction_color = '0 0.5 1';
 -float autocvar_hud_panel_strafehud_direction_alpha = 1;
 -float autocvar_hud_panel_strafehud_direction_width = 0.25;
 -float autocvar_hud_panel_strafehud_direction_length = 0.02;
 -float autocvar_hud_panel_strafehud_slickdetector_range = 0;
 -int autocvar_hud_panel_strafehud_slickdetector_granularity = 2;
 -vector autocvar_hud_panel_strafehud_slickdetector_color = '0 1 1';
 -float autocvar_hud_panel_strafehud_slickdetector_alpha = 0.5;
 -float autocvar_hud_panel_strafehud_slickdetector_height = 0.125;
 -float autocvar_hud_panel_strafehud_startspeed_fade = 0;
 -vector autocvar_hud_panel_strafehud_startspeed_color = '1 0.75 0';
 -float autocvar_hud_panel_strafehud_startspeed_size = 1.5;
 -float autocvar_hud_panel_strafehud_jumpheight_fade = 0;
 -float autocvar_hud_panel_strafehud_jumpheight_min = 50;
 -vector autocvar_hud_panel_strafehud_jumpheight_color = '0 1 0.75';
 -float autocvar_hud_panel_strafehud_jumpheight_size = 1.5;
 -float autocvar_hud_panel_strafehud_timeout_air = 0.1;
 -float autocvar_hud_panel_strafehud_timeout_ground = 0.03333333;
 -float autocvar_hud_panel_strafehud_timeout_turn = 0.1;
 -float autocvar_hud_panel_strafehud_timeout_direction = 0.5;
 -float autocvar_hud_panel_strafehud_antiflicker_angle = 0.01;
 -float autocvar_hud_panel_strafehud_antiflicker_speed = 0.0001;
 -
 -void HUD_Panel_DrawStrafeHUD(float, float, vector, float, int, int);
 +AUTOCVAR_SAVE(hud_panel_strafehud, int, 3, "enable this panel, 1 = show if not observing, 2 = show always, 3 = show only in race/cts if not observing");
 +AUTOCVAR_SAVE(_hud_panel_strafehud_demo, bool, false, "strafehud changes angle during configure");
 +AUTOCVAR_SAVE(hud_panel_strafehud_dynamichud, bool, true, "apply the dynamic hud effects to this panel");
 +AUTOCVAR_SAVE(hud_panel_strafehud_mode, int, 0, "strafehud mode which controls whether the strafehud is centered at \"0\" = view angle, \"1\" = velocity angle");
 +AUTOCVAR_SAVE(hud_panel_strafehud_range, float, 90, "the angle range up to 360 degrees displayed on the strafehud, \"-1\" = current fov, \"0\" = dynamic (chooses the minimum range required to still see the whole area needed for accelerating)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_style, int, 2, "\"0\" = no styling, \"1\" = progress bar style for the strafe bar, \"2\" = gradient for the strafe bar");
- AUTOCVAR_SAVE(hud_panel_strafehud_unit, int, 1, "speed unit (1 = qu/s, 2 = m/s, 3 = km/h, 4 = mph, 5 = knots), length unit (1 = qu, 2 = m, 3 = km, 4 = mi, 5 = nmi)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_unit_show, bool, true, "show units");
 +AUTOCVAR_SAVE(hud_panel_strafehud_uncapped, bool, false, "set to \"1\" to remove some safety restrictions, useful to set thinner indicator lines down to 1px or for trying out higher values for some performance degrading operations (warning: elements may turn invisible if too thin, other configurations may crash your game or look horribly ugly)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_bar_preaccel, bool, true, "set to \"1\" to extend the acceleration zone by the strafe meter zone before full acceleration can be achieved");
 +AUTOCVAR_SAVE(hud_panel_strafehud_bar_neutral_color, vector, '1 1 1', "color of the strafe meter neutral zone");
 +AUTOCVAR_SAVE(hud_panel_strafehud_bar_neutral_alpha, float, 0.1, "opacity of the strafe meter neutral zone");
 +AUTOCVAR_SAVE(hud_panel_strafehud_bar_accel_color, vector, '0 1 0', "color of the strafe meter acceleration zone");
 +AUTOCVAR_SAVE(hud_panel_strafehud_bar_accel_alpha, float, 0.5, "opacity of the strafe meter acceleration zone");
 +AUTOCVAR_SAVE(hud_panel_strafehud_bar_overturn_color, vector, '1 0 1', "color of the strafe meter overturn zone");
 +AUTOCVAR_SAVE(hud_panel_strafehud_bar_overturn_alpha, float, 0.5, "opacity of the strafe meter overturn zone");
 +AUTOCVAR_SAVE(hud_panel_strafehud_angle_style, int, 0, "set the angle indicator style: 0 = none, 1 = solid line, 2 = dashed line");
 +AUTOCVAR_SAVE(hud_panel_strafehud_angle_dashes, int, 4, "determines the amount of dashes if the angle indicator uses a dashed line");
 +AUTOCVAR_SAVE(hud_panel_strafehud_angle_alpha, float, 0.8, "opacity of the indicator showing the player's current angle");
 +AUTOCVAR_SAVE(hud_panel_strafehud_angle_height, float, 1, "height of the indicator showing the player's current angle (relative to the panel height)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_angle_width, float, 0.001, "width of the indicator showing the player's current angle (relative to the panel width)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_angle_neutral_color, vector, '1 1 0', "color of the indicator showing the player's current angle if it is within the neutral zone");
 +AUTOCVAR_SAVE(hud_panel_strafehud_angle_accel_color, vector, '0 1 1', "color of the indicator showing the player's current angle if it is within the acceleration zone");
 +AUTOCVAR_SAVE(hud_panel_strafehud_angle_overturn_color, vector, '1 0 1', "color of the indicator showing the player's current angle if it is within the overturn zone");
 +AUTOCVAR_SAVE(hud_panel_strafehud_angle_arrow, int, 1, "set the angle indicator's arrow style: 0 = none, 1 = top, 2 = bottom, 3 = both");
 +AUTOCVAR_SAVE(hud_panel_strafehud_angle_arrow_size, float, 0.5, "size of the arrow (relative to the panel height)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_bestangle, bool, true, "set to \"1\" to enable a ghost angle indicator showing the best angle to gain maximum acceleration");
 +AUTOCVAR_SAVE(hud_panel_strafehud_bestangle_color, vector, '1 1 1', "color of the indicator showing the best angle to gain maximum acceleration");
 +AUTOCVAR_SAVE(hud_panel_strafehud_bestangle_alpha, float, 0.5, "opacity of the indicator showing the best angle to gain maximum acceleration");
 +AUTOCVAR_SAVE(hud_panel_strafehud_switch, bool, true, "set to \"1\" to enable the switch indicator showing the angle to move to when switching sides");
 +AUTOCVAR_SAVE(hud_panel_strafehud_switch_minspeed, float, -1, "minimum speed in qu/s at which switch indicator(s) which are used to aid changing strafe direction will be shown (set to -1 for dynamic minspeed)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_switch_color, vector, '1 1 0', "color of the switch indicator");
 +AUTOCVAR_SAVE(hud_panel_strafehud_switch_alpha, float, 1, "opacity of the switch indicator");
 +AUTOCVAR_SAVE(hud_panel_strafehud_switch_width, float, 0.003, "width of the strafe angle indicator(s) (relative to the strafe bar width)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_direction, bool, false, "set to \"1\" to enable the direction caps to see in which direction you are currently strafing");
 +AUTOCVAR_SAVE(hud_panel_strafehud_direction_color, vector, '0 0.5 1', "color of the direction caps which indicate the direction the player is currently strafing towards");
 +AUTOCVAR_SAVE(hud_panel_strafehud_direction_alpha, float, 1, "opacity of the direction caps which indicate the direction the player is currently strafing towards");
 +AUTOCVAR_SAVE(hud_panel_strafehud_direction_width, float, 0.25, "stroke width of the direction caps which indicate the direction the player is currently strafing towards (relative to the panel height)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_direction_length, float, 0.02, "length of the horizontal component of the direction caps which indicate the direction the player is currently strafing towards (relative to the panel width)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector, bool, true, "set to \"1\" to enable the slick detector which notifies you if there is slick near you");
 +AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_range, float, 200, "range of the slick detector in qu");
 +AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_granularity, int, 1, "value from 0 to 4 which defines how exact the search for slick should be, higher values may yield better results but require more computation");
 +AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_color, vector, '0 1 1', "color of the slick detector indicator");
 +AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_alpha, float, 0.5, "opacity of the slick detector indicator");
 +AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_height, float, 0.125, "height of the slick detector indicator (relative to the panel height)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_startspeed, bool, true, "set to \"1\" to enable the start speed indicator which shows you the speed you had while passing the start trigger of a race map");
 +AUTOCVAR_SAVE(hud_panel_strafehud_startspeed_fade, float, 4, "fade time (in seconds) of the start speed text");
 +AUTOCVAR_SAVE(hud_panel_strafehud_startspeed_color, vector, '1 0.75 0', "color of the start speed text");
 +AUTOCVAR_SAVE(hud_panel_strafehud_startspeed_size, float, 1.5, "size of the start speed text (relative to the panel height)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight, bool, false, "set to \"1\" to enable the jump height indicator which tells you how high you jumped");
 +AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight_fade, float, 4, "fade time (in seconds) of the jump height text");
 +AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight_min, float, 50, "minimum jump height to display in the selected unit");
 +AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight_color, vector, '0 1 0.75', "color of the jump height text");
 +AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight_size, float, 1.5, "size of the jump height text (relative to the panel height)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_timeout_ground, float, 0.1, "time (in seconds) after take off before changing to air strafe physics when not jumping (visually more consistent hud while on slick downwards ramps)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_timeout_turn, float, 0.1, "time (in seconds) after releasing the strafe keys before changing mode (visually more consistent hud while switching between left/right strafe turning)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_antiflicker_angle, float, 0.01, "how many degrees from 0° to 180° the hud ignores if it could cause visual disturbances otherwise (and to counter rounding errors)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_fps_update, float, 0.5, "update interval (in seconds) of the frametime to calculate the optimal angle, smaller values may cause flickering");
 +AUTOCVAR_SAVE(hud_panel_strafehud_sonar, bool, false, "set to \"1\" to enable the strafe sonar");
 +AUTOCVAR_SAVE(hud_panel_strafehud_sonar_audio, string, "misc/talk", "audio to play for sonar");
 +AUTOCVAR_SAVE(hud_panel_strafehud_sonar_start, float, 0.5, "how optimal from 0 to 1 your strafing angle has to be for the strafe sonar to activate");
 +AUTOCVAR_SAVE(hud_panel_strafehud_sonar_interval_start, float, 0.333333, "strafe sonar sound interval in seconds");
 +AUTOCVAR_SAVE(hud_panel_strafehud_sonar_interval_range, float, -0.222222, "dynamic sound interval range in seconds of the strafe sonar as you approach the optimal angle");
 +AUTOCVAR_SAVE(hud_panel_strafehud_sonar_interval_exponent, float, 1, "exponent of the dynamic sound interval range of the strafe sonar");
 +AUTOCVAR_SAVE(hud_panel_strafehud_sonar_volume_start, float, 0.333333, "sound volume of the strafe sonar");
 +AUTOCVAR_SAVE(hud_panel_strafehud_sonar_volume_range, float, 0.666666, "dynamic volume range of the strafe sonar as you approach the optimal angle");
 +AUTOCVAR_SAVE(hud_panel_strafehud_sonar_volume_exponent, float, 1, "exponent of the dynamic volume range of the strafe sonar");
 +AUTOCVAR_SAVE(hud_panel_strafehud_sonar_pitch_start, float, 0.9, "playback speed of the strafe sonar");
 +AUTOCVAR_SAVE(hud_panel_strafehud_sonar_pitch_range, float, 0.1, "dynamic playback speed range of the strafe sonar as you approach the optimal angle");
 +AUTOCVAR_SAVE(hud_panel_strafehud_sonar_pitch_exponent, float, 1, "exponent of the dynamic playback speed range of the strafe sonar");
 +AUTOCVAR_SAVE(hud_panel_strafehud_vangle, bool, false, "set to \"1\" to enable the vertical angle indicator");
 +AUTOCVAR_SAVE(hud_panel_strafehud_vangle_color, vector, '0.75 0.75 0.75', "color of the vertical angle text");
 +AUTOCVAR_SAVE(hud_panel_strafehud_vangle_size, float, 1, "size of the vertical angle text (relative to the panel height)");
 +AUTOCVAR_SAVE(hud_panel_strafehud_projection, int, 0, "strafehud projection mode, \"0\" = linear, \"1\" = perspective, \"2\" = panoramic");
 +
 +void HUD_Panel_DrawStrafeHUD(float, float, float, vector, float, int, int, bool, float);
  vector StrafeHUD_mixColors(vector, vector, float);
 -void StrafeHUD_drawGradient(vector, vector, vector, float, float, float, float, int);
 +void StrafeHUD_drawGradient(vector, vector, vector, float, float, float, float, int, bool, float);
  float GetLengthUnitFactor(int);
  string GetLengthUnit(int);
 +void StrafeHUD_drawStrafeArrow(vector, float, vector, float, bool, float);
 +bool StrafeHUD_drawTextIndicator(string, float, vector, float, float, float, int);
 +float StrafeHUD_projectOffset(float, float);
 +float StrafeHUD_projectWidth(float, float, float);
 +
 +const int STRAFEHUD_MODE_VIEW_CENTERED = 0;
 +const int STRAFEHUD_MODE_VELOCITY_CENTERED = 1;
 +
 +const int STRAFEHUD_DIRECTION_NONE = 0;
 +const int STRAFEHUD_DIRECTION_LEFT = 1;
 +const int STRAFEHUD_DIRECTION_RIGHT = 2;
 +
 +const int STRAFEHUD_KEYS_NONE = 0;
 +const int STRAFEHUD_KEYS_FORWARD = 1;
 +const int STRAFEHUD_KEYS_BACKWARD = 2;
 +
 +const int STRAFEHUD_STYLE_DRAWFILL = 0;
 +const int STRAFEHUD_STYLE_PROGRESSBAR = 1;
 +const int STRAFEHUD_STYLE_GRADIENT = 2;
 +
 +const int STRAFEHUD_GRADIENT_NONE = 0;
 +const int STRAFEHUD_GRADIENT_LEFT = 1;
 +const int STRAFEHUD_GRADIENT_RIGHT = 2;
 +const int STRAFEHUD_GRADIENT_BOTH = 3;
 +
 +const int STRAFEHUD_INDICATOR_NONE = 0;
 +const int STRAFEHUD_INDICATOR_SOLID = 1;
 +const int STRAFEHUD_INDICATOR_DASHED = 2;
 +
 +const int STRAFEHUD_TEXT_TOP = 0;
 +const int STRAFEHUD_TEXT_BOTTOM = 1;
 +
 +const int STRAFEHUD_PROJECTION_LINEAR = 0;
 +const int STRAFEHUD_PROJECTION_PERSPECTIVE = 1;
 +const int STRAFEHUD_PROJECTION_PANORAMIC = 2;