]> git.rm.cloudns.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
StrafeHUD-Next, new StrafeHUD features + fixes and refactoring
authorJuhu <5894800-Juhu_@users.noreply.gitlab.com>
Wed, 22 Jan 2025 11:43:57 +0000 (11:43 +0000)
committerbones_was_here <bones_was_here@xonotic.au>
Wed, 22 Jan 2025 11:43:57 +0000 (11:43 +0000)
15 files changed:
_hud_common.cfg
qcsrc/client/hud/panel/_mod.inc
qcsrc/client/hud/panel/_mod.qh
qcsrc/client/hud/panel/strafehud.qc
qcsrc/client/hud/panel/strafehud.qh
qcsrc/client/hud/panel/strafehud/_mod.inc [new file with mode: 0644]
qcsrc/client/hud/panel/strafehud/_mod.qh [new file with mode: 0644]
qcsrc/client/hud/panel/strafehud/draw.qc [new file with mode: 0644]
qcsrc/client/hud/panel/strafehud/draw.qh [new file with mode: 0644]
qcsrc/client/hud/panel/strafehud/draw_core.qc [new file with mode: 0644]
qcsrc/client/hud/panel/strafehud/draw_core.qh [new file with mode: 0644]
qcsrc/client/hud/panel/strafehud/extra.qc [new file with mode: 0644]
qcsrc/client/hud/panel/strafehud/extra.qh [new file with mode: 0644]
qcsrc/client/hud/panel/strafehud/util.qc [new file with mode: 0644]
qcsrc/client/hud/panel/strafehud/util.qh [new file with mode: 0644]

index 4266918ce3b2960e35700bf1407256ca5c908254..05992fce3b280e142fbd099dfc93cda7cb32646b 100644 (file)
@@ -166,11 +166,10 @@ seta hud_panel_scoreboard_itemstats_showdelay_minpos 0.75 "delay displaying the
 
 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 "90" "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)"
-seta hud_panel_strafehud_range_sidestrafe "-1" "the angle range up to 360 degrees displayed on the StrafeHUD when side strafing, \"0\" = dynamic (chooses the minimum range required to still see the whole area needed for accelerating), \"-1\" = same as the normal range"
-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_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_range_sidestrafe "-2" "the angle range up to 360 degrees displayed on the StrafeHUD when side strafing, \"0\" = dynamic (chooses the minimum range required to still see the whole area needed for accelerating), \"-1\" = current fov, \"-2\" = same as the normal range"
+seta hud_panel_strafehud_style "2" "\"0\" = no styling, \"1\" = progress bar style for the strafe bar, \"2\" = accelerated gradient for the strafe bar (no nonlinear projection for gradient color/opacity), \"3\" = software gradient for the strafe bar (slow)"
 seta hud_panel_strafehud_unit_show "1" "show units"
-seta hud_panel_strafehud_uncapped "0" "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_onground_mode "2" "handling of landing at speeds where friction is higher than optimal acceleration; \"0\" = fill the whole HUD with overturn, \"1\" = show zones regardless, \"2\" = show the zones as if airborne (useful for quake2 and quake3 physics)"
 seta hud_panel_strafehud_onground_friction "1" "account for friction in calculations"
 seta hud_panel_strafehud_bar_preaccel "1" "extend the acceleration zone by the strafe meter zone before full acceleration can be achieved"
@@ -182,31 +181,43 @@ seta hud_panel_strafehud_bar_accel_color "0 1 0" "color of the strafe meter acce
 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.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" "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_preaccel_color "0 1 1" "color of the indicator showing the player's current angle if it is within the pre-acceleration zone"
 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_angle_line "0" "defines the number of dashes of the indicator line showing the player's current angle; \"0\" = no line, \"1\" = solid line"
+seta hud_panel_strafehud_angle_line_width "0.001" "width of the indicator line showing the player's current angle (relative to the panel width)"
+seta hud_panel_strafehud_angle_line_height "1" "height of the indicator line showing the player's current angle (relative to the panel height)"
+seta hud_panel_strafehud_angle_arrow "1" "arrow style of the angle indicator showing the player's current angle; \"0\" = none, \"1\" = top, \"2\" = bottom, \"3\" = both"
+seta hud_panel_strafehud_angle_arrow_size "0.5" "arrow size of the indicator showing the player's current angle (relative to the panel height)"
 seta hud_panel_strafehud_bestangle "1" "\"1\" = enable a ghost angle indicator showing the best angle to gain maximum acceleration, \"2\" = only when side strafing"
 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" "\"1\" = enable the switch indicator showing the angle to move to when switching sides, \"2\" = show the normal switch indicators when W-turning, \"3\" = also while side strafing"
-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; \"-1\" = dynamic"
-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_bestangle_line "0" "defines the number of dashes of the best angle indicator line; \"0\" = no line, \"1\" = solid line"
+seta hud_panel_strafehud_bestangle_line_width "0.001" "width of the best angle indicator line (relative to the panel width)"
+seta hud_panel_strafehud_bestangle_line_height "1" "height of the best angle indicator line (relative to the panel height)"
+seta hud_panel_strafehud_bestangle_arrow "1" "arrow style of the best angle indicator; \"0\" = none, \"1\" = top, \"2\" = bottom, \"3\" = both"
+seta hud_panel_strafehud_bestangle_arrow_size "0.5" "arrow size of the best angle indicator (relative to the panel height)"
+seta hud_panel_strafehud_switch "1" "\"1\" = enable the strafe angle indicator showing the angle to move to when changing side, \"2\" = show the normal switch indicators when W-turning, \"3\" = also while side strafing"
+seta hud_panel_strafehud_switch_minspeed "-1" "minimum speed in qu/s at which angle indicator(s) which are used to aid changing strafe direction will be shown; \"-1\" = dynamic"
+seta hud_panel_strafehud_switch_color "1 1 0" "color of the strafe angle indicators for changing strafe direction"
+seta hud_panel_strafehud_switch_alpha "0.5" "opacity of the strafe angle indicators for changing strafe direction"
+seta hud_panel_strafehud_switch_line "0" "defines the number of dashes of the change angle indicator line; \"0\" = no line, \"1\" = solid line"
+seta hud_panel_strafehud_switch_line_width "0.001" "width of the change angle indicator line (relative to the panel width)"
+seta hud_panel_strafehud_switch_line_height "1" "height of the change angle indicator line (relative to the panel height)"
+seta hud_panel_strafehud_switch_arrow "1" "arrow style of the change angle indicator; \"0\" = none, \"1\" = top, \"2\" = bottom, \"3\" = both"
+seta hud_panel_strafehud_switch_arrow_size "0.5" "arrow size of the change angle indicator (relative to the panel height)"
 seta hud_panel_strafehud_wturn "1" "enable W-turn indicators showing the angle to rotate your velocity as fast as possible; \"1\" = only if W-turning, \"2\" = also while strafing normally, \"3\" = also while side strafing"
-seta hud_panel_strafehud_wturn_color "0 1 1" "color of the W-turn indicators"
-seta hud_panel_strafehud_wturn_alpha "1" "opacity of the W-turn indicators"
-seta hud_panel_strafehud_wturn_width "0.003" "width of the W-turn indicators (relative to the strafe bar width)"
+seta hud_panel_strafehud_wturn_color "0 0 1" "color of the W-turn indicators"
+seta hud_panel_strafehud_wturn_alpha "0.5" "opacity of the W-turn indicators"
 seta hud_panel_strafehud_wturn_proper "0" "use the proper formula to calculate W-turn indicators (WARNING: loses accuracy at high speeds)"
 seta hud_panel_strafehud_wturn_unrestricted "0" "enable W-turn indicators even when W-turning gives acceleration (WARNING: not completely accurate)"
+seta hud_panel_strafehud_wturn_line "0" "defines the number of dashes of the W-turn angle indicator line; \"0\" = no line, \"1\" = solid line"
+seta hud_panel_strafehud_wturn_line_width "0.001" "width of the W-turn angle indicator line (relative to the panel width)"
+seta hud_panel_strafehud_wturn_line_height "1" "height of the W-turn angle indicator line (relative to the panel height)"
+seta hud_panel_strafehud_wturn_arrow "1" "arrow style of the W-turn angle indicator; \"0\" = none, \"1\" = top, \"2\" = bottom, \"3\" = both"
+seta hud_panel_strafehud_wturn_arrow_size "0.5" "arrow size of the W-turn angle indicator (relative to the panel height)"
 seta hud_panel_strafehud_direction "0" "enable 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"
@@ -221,16 +232,38 @@ seta hud_panel_strafehud_slickdetector_height "0.125" "height of the slick detec
 seta hud_panel_strafehud_startspeed "1" "enable the start speed indicator which shows you the speed you had while passing the start trigger of a race map"
 seta hud_panel_strafehud_startspeed_fade "4" "fade time (in seconds) of the start speed text"
 seta hud_panel_strafehud_startspeed_color "1 0.75 0" "color of the start speed text"
+seta hud_panel_strafehud_startspeed_pos "0 -1" "position of the start speed text (relative to the panel), the Y coordinate must be <= -1 (below) or >= 1 (above) the panel"
 seta hud_panel_strafehud_startspeed_size "1.5" "size of the start speed text (relative to the panel height)"
 seta hud_panel_strafehud_jumpheight "0" "enable the jump height indicator which tells you how high you jumped"
 seta hud_panel_strafehud_jumpheight_fade "4" "fade time (in seconds) of the jump height text"
 seta hud_panel_strafehud_jumpheight_min "50" "minimum jump height to display in the selected unit"
 seta hud_panel_strafehud_jumpheight_color "0 1 0.75" "color of the jump height text"
+seta hud_panel_strafehud_jumpheight_pos "0 -2" "position of the jump height text (relative to the panel), the Y coordinate must be <= -1 (below) or >= 1 (above) the panel"
 seta hud_panel_strafehud_jumpheight_size "1.5" "size of the jump height text (relative to the panel height)"
 seta hud_panel_strafehud_timeout_ground "0.1" "time (in seconds) after take off before changing to air strafe physics when not jumping (visually more consistent HUD while on downwards slick ramps)"
 seta hud_panel_strafehud_timeout_turn "0.1" "time (in seconds) after releasing the strafe keys before changing mode (visually more consistent HUD while switching between left/right strafe turning)"
 seta hud_panel_strafehud_antiflicker_angle "0.01" "how many degrees from 0° to 180° the HUD ignores if it could cause visual disturbances otherwise (and to counter rounding errors)"
 seta hud_panel_strafehud_fps_update "0.5" "update interval (in seconds) of the frametime to calculate the optimal angle, smaller values may cause flickering"
+seta hud_panel_strafehud_sonar "0" "\"1\" = enable the strafe sonar"
+seta hud_panel_strafehud_sonar_audio "misc/talk" "audio to play for sonar"
+seta hud_panel_strafehud_sonar_start "0.5" "how optimal from 0 to 1 your strafing angle has to be for the strafe sonar to activate"
+seta hud_panel_strafehud_sonar_interval_start "0.333333" "strafe sonar sound interval in seconds"
+seta hud_panel_strafehud_sonar_interval_range "-0.222222" "dynamic sound interval range in seconds of the strafe sonar as you approach the optimal angle"
+seta hud_panel_strafehud_sonar_interval_exponent "1" "exponent of the dynamic sound interval range of the strafe sonar"
+seta hud_panel_strafehud_sonar_volume_start "0.333333" "sound volume of the strafe sonar"
+seta hud_panel_strafehud_sonar_volume_range "0.666666" "dynamic volume range of the strafe sonar as you approach the optimal angle"
+seta hud_panel_strafehud_sonar_volume_exponent "1" "exponent of the dynamic volume range of the strafe sonar"
+seta hud_panel_strafehud_sonar_pitch_start "0.9" "playback speed of the strafe sonar"
+seta hud_panel_strafehud_sonar_pitch_range "0.1" "dynamic playback speed range of the strafe sonar as you approach the optimal angle"
+seta hud_panel_strafehud_sonar_pitch_exponent "1" "exponent of the dynamic playback speed range of the strafe sonar"
+seta hud_panel_strafehud_vangle "0" "\"1\" = enable the vertical angle indicator"
+seta hud_panel_strafehud_vangle_color "0.75 0.75 0.75" "color of the vertical angle text"
+seta hud_panel_strafehud_vangle_pos "-0.25 1" "position of the vertical angle text (relative to the panel), the Y coordinate must be <= -1 (below) or >= 1 (above) the panel"
+seta hud_panel_strafehud_vangle_size "1" "size of the vertical angle text (relative to the panel height)"
+seta hud_panel_strafehud_strafeefficiency "0" "\"1\" = enable the strafe efficiency indicator"
+seta hud_panel_strafehud_strafeefficiency_pos "0.25 1" "position of the strafe efficiency text (relative to the panel), the Y coordinate must be <= -1 (below) or >= 1 (above) the panel"
+seta hud_panel_strafehud_strafeefficiency_size "1" "size of the strafe efficiency text (relative to the panel height)"
+seta hud_panel_strafehud_projection "0" "StrafeHUD projection mode; \"0\" = linear, \"1\" = perspective, \"2\" = panoramic"
 
 // HUD panel aliases
 alias quickmenu "cl_cmd hud quickmenu ${* ?}"
index 78558773e4ce935d0960f7b7ac6df36f82a34569..73f61c3a71b76200ae50483f4d56fae14d4be620 100644 (file)
@@ -21,3 +21,5 @@
 #include <client/hud/panel/timer.qc>
 #include <client/hud/panel/vote.qc>
 #include <client/hud/panel/weapons.qc>
+
+#include <client/hud/panel/strafehud/_mod.inc>
index df7910b5454ad7ea970fb36b853ea873ef039f66..4a6ca59c84996337a5203b465342ffa5008b5517 100644 (file)
@@ -21,3 +21,5 @@
 #include <client/hud/panel/timer.qh>
 #include <client/hud/panel/vote.qh>
 #include <client/hud/panel/weapons.qh>
+
+#include <client/hud/panel/strafehud/_mod.qh>
index 15ccac0dc1c202bd5371648631ef3e3b6d4401ef..6601f38140cdc5f63ad3f49f97ee08efc1da8e4b 100644 (file)
@@ -6,6 +6,7 @@
 #include <lib/csqcmodel/cl_player.qh>
 #include <common/physics/player.qh>
 #include <common/physics/movetypes/movetypes.qh>
+#include "racetimer.qh"
 
 // non-essential
 #include <client/view.qh> // for v_flipped state
 #include <common/animdecide.qh> // anim_implicit_state
 #include <common/ent_cs.qh> // CSQCModel_server2csqc()
 
-// start speed
-#include <client/hud/panel/racetimer.qh> // checkpoint information (race_*)
-
-// jump height
-#include <lib/csqcmodel/common.qh> // for IS_PLAYER() macro
-#include <common/resources/cl_resources.qh> // IS_DEAD() macro
-
 // StrafeHUD (#25)
 
 void HUD_StrafeHUD_Export(int fh)
@@ -28,20 +22,22 @@ void HUD_StrafeHUD_Export(int fh)
        // allow saving cvars that aesthetically change the panel into hud skin files
 }
 
-float GeomLerp(float a, float _lerp, float b); // declare GeomLerp here since there's no header file for it
-
 void HUD_StrafeHUD()
 {
        static float hud_lasttime = 0;
        entity strafeplayer;
-       bool islocal;
+       bool is_local;
 
        // generic hud routines
        if(!autocvar__hud_configure)
        {
-               if(!autocvar_hud_panel_strafehud ||
-                  (spectatee_status == -1 && (autocvar_hud_panel_strafehud == 1 || autocvar_hud_panel_strafehud == 3)) ||
-                  (autocvar_hud_panel_strafehud == 3 && !MUTATOR_CALLHOOK(HUD_StrafeHUD_showoptional))) { hud_lasttime = time; return; }
+               if(!autocvar_hud_panel_strafehud
+               || (spectatee_status == -1 && (autocvar_hud_panel_strafehud == 1 || autocvar_hud_panel_strafehud == 3))
+               || (autocvar_hud_panel_strafehud == 3 && !MUTATOR_CALLHOOK(HUD_StrafeHUD_showoptional)))
+               {
+                       hud_lasttime = time;
+                       return;
+               }
        }
 
        HUD_Panel_LoadCvars();
@@ -62,1487 +58,735 @@ void HUD_StrafeHUD()
        // find out whether the local csqcmodel entity is valid
        if(spectatee_status > 0 || isdemo())
        {
-               islocal = false;
+               is_local = false;
                strafeplayer = CSQCModel_server2csqc(player_localentnum - 1);
        }
        else
        {
-               islocal = true;
+               is_local = true;
                strafeplayer = csqcplayer;
        }
 
-       // draw strafehud
-       if(csqcplayer && strafeplayer)
+       if(!csqcplayer || !strafeplayer)
        {
-               float strafe_waterlevel;
+               hud_lasttime = time;
+               return;
+       }
 
-               // 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;
-               }
+       // draw strafehud
 
-               int keys = STAT(PRESSED_KEYS);
-               // 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 jumpheld = false;
-               if(islocal)
-               {
-                       if((PHYS_INPUT_BUTTON_JUMP(strafeplayer) || PHYS_INPUT_BUTTON_JETPACK(strafeplayer)) && !PHYS_CL_TRACK_CANJUMP(strafeplayer))
-                               jumpheld = true;
-               }
-               else
-               {
-                       if((keys & KEY_JUMP) && !PHYS_TRACK_CANJUMP(strafeplayer))
-                               jumpheld = true;
-               }
+       int keys = STAT(PRESSED_KEYS);
+       bool jumpheld = StrafeHUD_DetermineJumpHeld(strafeplayer, keys, is_local);
 
-               // 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
-               // doesn't get changed by ground timeout and isn't affected by jump input
-               bool   real_onground                 = islocal ? IS_ONGROUND(strafeplayer) : !(strafeplayer.anim_implicit_state & ANIMIMPLICITSTATE_INAIR);
-               // doesn't get changed by ground timeout
-               bool   real_onslick                  = false;
-               // if jump is held assume we are in air, avoids flickering of the hud when hitting the ground
-               bool   onground                      = real_onground && !jumpheld;
-               bool   onslick                       = real_onslick;
-               bool   onground_expired;
-               bool   strafekeys;
-               // the hud will not work well while swimming
-               bool   swimming                      = strafe_waterlevel >= WATERLEVEL_SWIMMING;
-               // use local csqcmodel entity for this even when spectating, flickers too much otherwise
-               float  speed                         = !autocvar__hud_configure ? vlen(vec2(csqcplayer.velocity)) : 1337;
-               // only the local csqcplayer entity contains this information even when spectating
-               float  maxspeed_mod                  = IS_DUCKED(csqcplayer) ? .5 : 1;
-               float  maxspeed_phys                 = onground ? PHYS_MAXSPEED(strafeplayer) : PHYS_MAXAIRSPEED(strafeplayer);
-               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  airstopaccel                  = PHYS_AIRSTOPACCELERATE(strafeplayer);
-               float  aircontrol                    = PHYS_AIRCONTROL(strafeplayer);
-               bool   aircontrol_backwards          = PHYS_AIRCONTROL_BACKWARDS(strafeplayer) == 1;
-               bool   airaccel_qw                   = PHYS_AIRACCEL_QW(strafeplayer) == 1;
-               // change the range from 0° - 360° to -180° - 180° to match how view_angle represents angles
-               float  vel_angle                     = vectoangles(strafeplayer.velocity).y - (vectoangles(strafeplayer.velocity).y > 180 ? 360 : 0);
-               float  view_angle                    = PHYS_INPUT_ANGLES(strafeplayer).y;
-               float  angle;
-               vector movement                      = PHYS_INPUT_MOVEVALUES(strafeplayer);
-               bool   fwd; // left & right variables are flipped when !fwd
-               int    keys_fwd;
-               float  wishangle;
-               int    direction;
-               float  strafity                      = 0;
-
-               // HUD
-               int    mode;
-               float  speed_conversion_factor       = GetSpeedUnitFactor(autocvar_hud_speed_unit);
-               float  length_conversion_factor      = GetLengthUnitFactor(autocvar_hud_speed_unit);
-               // use more decimals when displaying km or miles
-               int    length_decimals               = autocvar_hud_speed_unit >= 3 && autocvar_hud_speed_unit <= 5 ? 6 : 2;
-               float  antiflicker_angle             = bound(0, autocvar_hud_panel_strafehud_antiflicker_angle, 180);
-               float  minspeed;
-               float  shift_offset                  = 0;
-               bool   straight_overturn             = false;
-               bool   immobile                      = speed <= 0;
-               float  hudangle;
-               float  hidden_width;
-               float  neutral_offset;
-               float  neutral_width;
-               vector currentangle_color            = autocvar_hud_panel_strafehud_angle_neutral_color;
-               float  currentangle_offset;
-               vector currentangle_size;
-               float  bestangle;
-               float  prebestangle;
-               float  overturn_angle;
-               float  odd_bestangle;
-               float  bestangle_offset              = 0;
-               float  switch_bestangle_offset       = 0;
-               bool   odd_angles                    = false;
-               float  odd_bestangle_offset          = 0;
-               float  switch_odd_bestangle_offset   = 0;
-               float  switch_bestangle_width        = 0;
-               float  wturn_bestangle               = 0;
-               float  wturn_left_bestangle_offset   = 0;
-               float  wturn_right_bestangle_offset  = 0;
-               float  wturn_bestangle_width         = 0;
-               float  accelzone_left_offset;
-               float  accelzone_right_offset;
-               float  accelzone_width;
-               float  preaccelzone_left_offset;
-               float  preaccelzone_right_offset;
-               float  preaccelzone_width;
-               float  overturn_offset;
-               float  overturn_width;
-               float  slickdetector_height;
-               vector direction_size_vertical       = '0 0 0';
-               vector direction_size_horizontal     = '0 0 0';
-               float  range_minangle;
-               float  text_offset_top               = 0;
-               float  text_offset_bottom            = 0;
-
-               // real_* variables which are always positive with no wishangle offset
-               float  real_bestangle;
-               float  real_prebestangle;
-               float  real_overturn_angle;
-               float  real_wturn_bestangle          = 0;
-
-               if(autocvar_hud_panel_strafehud_mode >= 0 && autocvar_hud_panel_strafehud_mode <= 1)
-                       mode = autocvar_hud_panel_strafehud_mode;
-               else
-                       mode = STRAFEHUD_MODE_VIEW_CENTERED;
+       // does not get changed by ground timeout and is not affected by jump input
+       bool real_onground = is_local ? IS_ONGROUND(strafeplayer) : !(strafeplayer.anim_implicit_state & ANIMIMPLICITSTATE_INAIR);
 
-               // there's only one size cvar for the arrows, they will always have a 45° angle to ensure proper rendering without antialiasing
-               float arrow_size = max(panel_size.y * min(autocvar_hud_panel_strafehud_angle_arrow_size, 10), 0);
+       // does not get changed by ground timeout
+       bool real_onslick = false;
 
-               if(onground)
-               {
-                       if(PHYS_FRICTION(strafeplayer) == 0)
-                       {
-                               onslick = true;
-                       }
-                       else // don't use IS_ONSLICK(), it only works for the local player and only if client prediction is enabled
-                       {
-                               trace_dphitq3surfaceflags = 0;
-                               tracebox(strafeplayer.origin, strafeplayer.mins, strafeplayer.maxs, strafeplayer.origin - '0 0 1', MOVE_NOMONSTERS, strafeplayer);
-                               onslick = trace_dphitq3surfaceflags & Q3SURFACEFLAG_SLICK;
-                       }
-                       real_onslick = onslick;
+       // if jump is held assume we are in air, avoids flickering of the hud when hitting the ground
+       bool onground = real_onground && !jumpheld;
+       bool onslick = real_onslick;
 
-                       onground_lasttime = time;
-                       onslick_last = onslick;
-               }
-               else if(jumpheld || swimming)
-               {
-                       onground_lasttime = 0;
-               }
+       // the hud will not work well while swimming
+       float strafe_waterlevel = StrafeHUD_DetermineWaterLevel(strafeplayer);
+       bool swimming = strafe_waterlevel >= WATERLEVEL_SWIMMING;
 
-               if(onground_lasttime == 0)
-                       onground_expired = true;
-               else
-                       onground_expired = (time - onground_lasttime) >= autocvar_hud_panel_strafehud_timeout_ground; // timeout for slick ramps
+       static float onground_lasttime = 0;
+       static bool onslick_last = false;
+       if(onground)
+       {
+               // do not use IS_ONSLICK(), it only works for the local player and only if client prediction is enabled
+               trace_dphitq3surfaceflags = 0;
+               tracebox(strafeplayer.origin, strafeplayer.mins, strafeplayer.maxs, strafeplayer.origin - '0 0 1', MOVE_NOMONSTERS, strafeplayer);
+               real_onslick = onslick = trace_dphitq3surfaceflags & Q3SURFACEFLAG_SLICK;
 
-               if(!onground && !onground_expired) // if ground timeout hasn't expired yet use ground physics
-               {
-                       onground = true;
-                       onslick = onslick_last;
+               onground_lasttime = time;
+               onslick_last = onslick;
+       }
+       else if(jumpheld || swimming)
+       {
+               onground_lasttime = 0;
+       }
 
-                       if(!autocvar__hud_configure)
-                       {
-                               maxspeed = PHYS_MAXSPEED(strafeplayer) * maxspeed_mod;
-                               maxaccel = PHYS_ACCELERATE(strafeplayer);
-                       }
-               }
+       bool onground_expired;
+       if(onground_lasttime == 0)
+               onground_expired = true;
+       else
+               onground_expired = (time - onground_lasttime) >= autocvar_hud_panel_strafehud_timeout_ground; // timeout for slick ramps
 
-               movespeed = vlen(vec2(movement));
-               if(movespeed == 0)
-                       movespeed = maxspeed;
-               else
-                       movespeed = min(movespeed, maxspeed);
+       // only the local csqcplayer entity contains this information even when spectating
+       float maxspeed_mod = IS_DUCKED(csqcplayer) ? .5 : 1;
+       float maxspeed_phys = onground ? PHYS_MAXSPEED(strafeplayer) : PHYS_MAXAIRSPEED(strafeplayer);
+       float maxspeed = !autocvar__hud_configure ? maxspeed_phys * maxspeed_mod : 320;
+       float maxaccel_phys = onground ? PHYS_ACCELERATE(strafeplayer) : PHYS_AIRACCELERATE(strafeplayer);
+       float maxaccel = !autocvar__hud_configure ? maxaccel_phys : 1;
 
-               if(!autocvar_hud_panel_strafehud_uncapped)
-                       arrow_size = max(arrow_size, 1);
+       if(!onground && !onground_expired) // if ground timeout has not expired yet use ground physics
+       {
+               onground = true;
+               onslick = onslick_last;
 
-               // determine frametime
-               if((csqcplayer_status == CSQCPLAYERSTATUS_PREDICTED) && (input_timelength > 0))
+               if(!autocvar__hud_configure)
                {
-                       float dt_client = input_timelength;
+                       maxspeed = PHYS_MAXSPEED(strafeplayer) * maxspeed_mod;
+                       maxaccel = PHYS_ACCELERATE(strafeplayer);
+               }
+       }
 
-                       if(dt_client > .05) // server splits frames longer than 50 ms into two moves
-                               dt_client /= 2; // doesn't ensure frames are smaller than 50 ms, just splits large frames in half, matches server behaviour
+       // move values are only valid for the local player
+       vector movement = PHYS_INPUT_MOVEVALUES(strafeplayer);
 
-                       // calculate average frametime
-                       dt_sum += dt_client * dt_client;
-                       dt_time += dt_client;
+       float movespeed;
+       if(is_local)
+       {
+               movespeed = min(vlen(vec2(movement)), maxspeed);
 
-                       if(((time - dt_update) > autocvar_hud_panel_strafehud_fps_update) || (dt_update == 0))
-                       {
-                               dt = dt_sum / dt_time;
-                               dt_update = time;
-                               dt_time = dt_sum = 0;
-                       }
-               }
-               else // when spectating other players server ticrate will be used, this may not be accurate but there is no way to find other player's frametime
-               {
-                       dt = ticrate;
-                       dt_update = dt_time = dt_sum = 0;
-               }
+               // assume maxspeed so that the hud remains useful even if no direction keys are pressed
+               if(movespeed == 0) movespeed = maxspeed;
+       }
+       else
+       {
+               // the only information available is whether a movement key is pressed or not
+               // which means the movespeed would be either maxspeed or zero
+               // since we set it to maxspeed if it is zero the movespeed will always equal maxspeed if the player is not local
+               movespeed = maxspeed;
+       }
 
-               // determine whether the player is pressing forwards or backwards keys
-               if(islocal) // if entity is local player
-               {
-                       if(movement.x > 0)
-                               keys_fwd = STRAFEHUD_KEYS_FORWARD;
-                       else if(movement.x < 0)
-                               keys_fwd = STRAFEHUD_KEYS_BACKWARD;
-                       else
-                               keys_fwd = STRAFEHUD_KEYS_NONE;
-               }
-               else // alternatively determine direction by querying pressed keys
-               {
-                       if((keys & KEY_FORWARD) && !(keys & KEY_BACKWARD))
-                               keys_fwd = STRAFEHUD_KEYS_FORWARD;
-                       else if(!(keys & KEY_FORWARD) && (keys & KEY_BACKWARD))
-                               keys_fwd = STRAFEHUD_KEYS_BACKWARD;
-                       else
-                               keys_fwd = STRAFEHUD_KEYS_NONE;
-               }
+       // the following functions have to check themselves whether the player is local and use the move values accordingly
+       int keys_fwd = StrafeHUD_DetermineForwardKeys(movement, keys, is_local);
+       float wishangle = StrafeHUD_DetermineWishAngle(movement, keys, is_local);
+       float absolute_wishangle = fabs(wishangle); // unmodified by side strafing code
+       bool strafekeys = fabs(wishangle) > 45;
 
-               // determine player wishdir
-               if(islocal) // if entity is local player
-               {
-                       if(movement.x == 0)
-                       {
-                               if(movement.y < 0)
-                                       wishangle = -90;
-                               else if(movement.y > 0)
-                                       wishangle = 90;
-                               else
-                                       wishangle = 0;
-                       }
-                       else
-                       {
-                               if(movement.y == 0)
-                               {
-                                       wishangle = 0;
-                               }
-                               else
-                               {
-                                       wishangle = RAD2DEG * atan2(movement.y, movement.x);
-                                       // wrap the wish angle if it exceeds ±90°
-                                       if(fabs(wishangle) > 90)
-                                       {
-                                               if(wishangle < 0)
-                                                       wishangle += 180;
-                                               else
-                                                       wishangle -= 180;
-
-                                               wishangle *= -1;
-                                       }
-                               }
-                       }
-               }
-               else // alternatively calculate wishdir by querying pressed keys
-               {
-                       if(keys & (KEY_FORWARD | KEY_BACKWARD))
-                               wishangle = 45;
-                       else
-                               wishangle = 90;
-                       if(keys & KEY_LEFT)
-                               wishangle *= -1;
-                       else if(!(keys & KEY_RIGHT))
-                               wishangle = 0; // wraps at 180°
-               }
-
-               float real_wishangle = fabs(wishangle); // unmodified by side strafing code
-               strafekeys = real_wishangle > 45;
+       // detect air strafe turning
+       static bool turn = false;
+       float strafity = 0;
+       if(!strafekeys || onground || autocvar__hud_configure)
+       {
+               turn = false;
+       }
+       else // air strafe only
+       {
+               static float turn_lasttime = 0;
+               static float turnangle;
+               bool turn_expired = (time - turn_lasttime) >= autocvar_hud_panel_strafehud_timeout_turn; // timeout for jumping with strafe keys only
 
-               // detect air strafe turning
-               if((!strafekeys && vlen(vec2(movement)) > 0) || onground || autocvar__hud_configure)
-               {
+               if(strafekeys)
+                       turn = true;
+               else if(turn_expired)
                        turn = false;
-               }
-               else // air strafe only
-               {
-                       bool turn_expired = (time - turn_lasttime) >= autocvar_hud_panel_strafehud_timeout_turn; // timeout for jumping with strafe keys only
 
+               if(turn) // side strafing (A/D)
+               {
                        if(strafekeys)
-                               turn = true;
-                       else if(turn_expired)
-                               turn = false;
-
-                       if(turn) // side strafing (A/D)
                        {
-                               if(strafekeys)
-                               {
-                                       turn_lasttime = time;
-                                       turnangle = wishangle;
-                               }
-                               else // retain last state until strafe turning times out
-                               {
-                                       wishangle = turnangle;
-                               }
-
-                               // calculate the maximum air strafe speed and acceleration
-                               strafity = 1 - (90 - fabs(wishangle)) / 45;
-
-                               if(PHYS_MAXAIRSTRAFESPEED(strafeplayer) != 0)
-                                       maxspeed = min(maxspeed, GeomLerp(PHYS_MAXAIRSPEED(strafeplayer), strafity, PHYS_MAXAIRSTRAFESPEED(strafeplayer)));
-
-                               movespeed = min(movespeed, maxspeed);
-
-                               if(PHYS_AIRSTRAFEACCELERATE(strafeplayer) != 0)
-                                       maxaccel = GeomLerp(PHYS_AIRACCELERATE(strafeplayer), strafity, PHYS_AIRSTRAFEACCELERATE(strafeplayer));
+                               turn_lasttime = time;
+                               turnangle = wishangle;
+                       }
+                       else // retain last state until strafe turning times out
+                       {
+                               wishangle = turnangle;
                        }
-               }
-
-               maxaccel *= dt * movespeed;
-               bestspeed = max(movespeed - maxaccel, 0); // target speed to gain maximum acceleration
-
-               float frictionspeed; // speed lost from friction
-               float strafespeed; // speed minus friction
 
-               if((speed > 0) && onground)
-               {
-                       float strafefriction = onslick ? PHYS_FRICTION_SLICK(strafeplayer) : PHYS_FRICTION(strafeplayer);
+                       // calculate the maximum air strafe speed and acceleration
+                       strafity = 1 - (90 - fabs(wishangle)) / 45;
 
-                       frictionspeed = speed * dt * strafefriction * max(PHYS_STOPSPEED(strafeplayer) / speed, 1);
-                       strafespeed = max(speed - frictionspeed, 0);
-               }
-               else
-               {
-                       frictionspeed = 0;
-                       strafespeed = speed;
-               }
+                       if(PHYS_MAXAIRSTRAFESPEED(strafeplayer) != 0)
+                               maxspeed = min(maxspeed, GeomLerp(PHYS_MAXAIRSPEED(strafeplayer), strafity, PHYS_MAXAIRSTRAFESPEED(strafeplayer)));
 
-               minspeed = autocvar_hud_panel_strafehud_switch_minspeed;
-               if(minspeed < 0)
-                       minspeed = bestspeed + frictionspeed;
+                       movespeed = min(movespeed, maxspeed);
 
-               // get current strafing angle ranging from -180° to +180°
-               if(!autocvar__hud_configure)
-               {
-                       if(speed > 0)
-                       {
-                               // calculate view angle relative to the players current velocity direction
-                               angle = vel_angle - view_angle;
-
-                               // if the angle goes above 180° or below -180° wrap it to the opposite side since we want the interior angle
-                               if(angle > 180)
-                                       angle -= 360;
-                               else if(angle < -180)
-                                       angle += 360;
-
-                               // determine whether the player is strafing forwards or backwards
-                               // if the player isn't strafe turning use forwards/backwards keys to determine direction
-                               if(fabs(wishangle) != 90)
-                               {
-                                       if(keys_fwd == STRAFEHUD_KEYS_FORWARD)
-                                               fwd = true;
-                                       else if(keys_fwd == STRAFEHUD_KEYS_BACKWARD)
-                                               fwd = false;
-                                       else
-                                               fwd = fabs(angle) <= 90;
-                               }
-                               // otherwise determine by examining the strafe angle
-                               else
-                               {
-                                       if(wishangle < 0) // detect direction using wishangle since the direction is not yet set
-                                               fwd = angle <= -wishangle;
-                                       else
-                                               fwd = angle >= -wishangle;
-                               }
-
-                               // shift the strafe angle by 180° when strafing backwards
-                               if(!fwd)
-                               {
-                                       if(angle < 0)
-                                               angle += 180;
-                                       else
-                                               angle -= 180;
-                               }
-
-                               // don't make the angle indicator switch side too much at ±180° if anti flicker is turned on
-                               if(angle > (180 - antiflicker_angle) || angle < (-180 + antiflicker_angle))
-                                       straight_overturn = true;
-                       }
-                       else
-                       {
-                               angle = 0;
-                               fwd = true;
-                       }
+                       if(PHYS_AIRSTRAFEACCELERATE(strafeplayer) != 0)
+                               maxaccel = GeomLerp(PHYS_AIRACCELERATE(strafeplayer), strafity, PHYS_AIRSTRAFEACCELERATE(strafeplayer));
                }
-               else // simulate turning for HUD setup
-               {
-                       const float demo_maxangle = 55; // maximum angle before changing direction
-                       const float demo_turnspeed = 40; // turning speed in degrees per second
+       }
 
-                       static float demo_position = -37 / demo_maxangle; // current positioning value between -1 and +1
+       float dt = StrafeHUD_DetermineFrameTime();
 
-                       if(autocvar__hud_panel_strafehud_demo)
-                       {
-                               float demo_dt = time - hud_lasttime;
-                               float demo_step = (demo_turnspeed / demo_maxangle) * demo_dt;
-                               demo_position = ((demo_position + demo_step) % 4 + 4) % 4;
-                       }
+       maxaccel *= dt * movespeed;
+       float bestspeed = max(movespeed - maxaccel, 0); // target speed to gain maximum acceleration
 
-                       // triangle wave function
-                       if(demo_position > 3)
-                               angle = -1 + (demo_position - 3);
-                       else if(demo_position > 1)
-                               angle = +1 - (demo_position - 1);
-                       else
-                               angle = demo_position;
-                       angle *= demo_maxangle;
+       // use local csqcmodel entity for this even when spectating, flickers too much otherwise
+       vector strafevelocity = csqcplayer.velocity;
 
-                       fwd = true;
-                       wishangle = 45;
-                       if(angle < 0)
-                               wishangle *= -1;
-               }
+       float speed = !autocvar__hud_configure ? vlen(vec2(strafevelocity)) : 1337;
+       bool moving = speed > 0;
 
-               // invert the wish angle when strafing backwards
-               if(!fwd)
-                       wishangle *= -1;
+       float frictionspeed; // speed lost from friction
+       float strafespeed; // speed minus friction
 
-               // flip angles if v_flipped is enabled
-               if(autocvar_v_flipped)
-               {
-                       angle *= -1;
-                       wishangle *= -1;
-               }
+       if(moving && onground)
+       {
+               float strafefriction = onslick ? PHYS_FRICTION_SLICK(strafeplayer) : PHYS_FRICTION(strafeplayer);
 
-               // determine whether the player is strafing left or right
-               if(wishangle > 0)
-               {
-                       direction = STRAFEHUD_DIRECTION_RIGHT;
-               }
-               else if(wishangle < 0)
-               {
-                       direction = STRAFEHUD_DIRECTION_LEFT;
-               }
-               else
-               {
-                       if(angle > antiflicker_angle && angle < (180 - antiflicker_angle))
-                               direction = STRAFEHUD_DIRECTION_RIGHT;
-                       else if(angle < -antiflicker_angle && angle > (-180 + antiflicker_angle))
-                               direction = STRAFEHUD_DIRECTION_LEFT;
-                       else
-                               direction = STRAFEHUD_DIRECTION_NONE;
-               }
+               frictionspeed = speed * dt * strafefriction * max(PHYS_STOPSPEED(strafeplayer) / speed, 1);
+               strafespeed = max(speed - frictionspeed, 0);
+       }
+       else
+       {
+               frictionspeed = 0;
+               strafespeed = speed;
+       }
 
-               if(airstopaccel == 0)
-                       airstopaccel = 1; // values of 0 are equivalent to 1
+       // get current strafing angle ranging from -180° to +180°
+       float angle;
+       bool fwd; // left & right variables are flipped when !fwd
 
-               // best angle to strafe at
-               if(immobile)
-               {
-                       // these are unused (neutral fills whole strafe bar)
-                       prebestangle = bestangle = 0;
-                       overturn_angle = 180;
-               }
-               else if(onground && autocvar_hud_panel_strafehud_onground_friction)
-               {
-                       // draw ground angles
-                       {
-                               // delta_opt = acos((s - a) / v_f), same in air
-                               bestangle = strafespeed > bestspeed
-                                       ? acos(bestspeed / strafespeed) * RAD2DEG // case 1
-                                       : 0; // case 2
-                               // case 1: normal. case 2: low speed, best angle is forwards
-                       }
-                       {
-                               // needed later if autocvar_hud_panel_strafehud_wturn != STRAFEHUD_WTURN_NONE,
-                               // ... so calculate even if autocvar_hud_panel_strafehud_bar_preaccel == 0
-                               float prebestangle_sqrt = movespeed * movespeed + strafespeed * strafespeed - speed * speed;
-                               // delta_min = acos(sqrt(s^2 - v_f^2 + v^2) / v_f), or just acos(s / v) in air
-                               prebestangle = (prebestangle_sqrt > 0 && strafespeed > sqrt(prebestangle_sqrt))
-                                       ? acos(sqrt(prebestangle_sqrt) / strafespeed) * RAD2DEG // case 1
-                                       : (prebestangle_sqrt > 0 ? 0 : 90); // case 2 : case 3
-                               // case 1: normal. case 2: low speed, best angle is forwards. case 3: landed at high speed, neutral zone is very large (see explanation below)
+       if(!autocvar__hud_configure)
+       {
+               if(moving)
+               {
+                       // change the range from 0° - 360° to -180° - 180° to match how view_angle represents angles
+                       float vel_angle = vectoangles(strafevelocity).y;
+                       if(vel_angle > 180) vel_angle -= 360;
+                       float view_angle = PHYS_INPUT_ANGLES(strafeplayer).y;
+
+                       // calculate view angle relative to the players current velocity direction
+                       angle = vel_angle - view_angle;
+
+                       // if the angle goes above 180° or below -180° wrap it to the opposite side since we want the interior angle
+                       if(angle > 180)
+                               angle -= 360;
+                       else if(angle < -180)
+                               angle += 360;
+
+                       // determine whether the player is strafing forwards or backwards
+                       // if the player is not strafe turning use forwards/backwards keys to determine direction
+                       if(fabs(wishangle) != 90)
+                       {
+                               if(keys_fwd == STRAFEHUD_KEYS_FORWARD)
+                                       fwd = true;
+                               else if(keys_fwd == STRAFEHUD_KEYS_BACKWARD)
+                                       fwd = false;
+                               else
+                                       fwd = fabs(angle) <= 90;
                        }
+                       // otherwise determine by examining the strafe angle
+                       else
                        {
-                               float overturn_numer = speed * speed - strafespeed * strafespeed - maxaccel * maxaccel;
-                               float overturn_denom = 2 * maxaccel * strafespeed;
-                               // delta_max = acos((v^2 - v_f^2 - a^2) / (2av_f)), or just acos(-a / 2v) if in air
-                               overturn_angle = overturn_denom > fabs(overturn_numer)
-                                       ? acos(overturn_numer / overturn_denom) * RAD2DEG // case 1
-                                       : (overturn_numer < 0 ? 180 : 0); // case 2 : case 3
-                               // case 1: normal. case 2: low speed, turning anywhere will gain speed. case 3: landed at high speed, turning anywhere will lose speed (due to friction)
+                               if(wishangle < 0) // detect direction using wishangle since the direction is not yet set
+                                       fwd = angle <= -wishangle;
+                               else
+                                       fwd = angle >= -wishangle;
                        }
-                       if(overturn_angle < bestangle || bestangle < prebestangle)
+
+                       // shift the strafe angle by 180° when strafing backwards
+                       if(!fwd)
                        {
-                               // these conditions occur when you land at high speed (above max onground speed), such that every wishangle will result in a speed loss due to friction
-                               if(autocvar_hud_panel_strafehud_onground_mode == STRAFEHUD_ONGROUND_OVERTURN)
-                               {
-                                       // make overturn fill the whole strafe bar
-                                       // most correct option by the true definition of accel, since every angle results in deceleration
-                                       prebestangle = bestangle = 0;
-                                       overturn_angle = 0;
-                               }
-                               else if(autocvar_hud_panel_strafehud_onground_mode == STRAFEHUD_ONGROUND_GROUND)
-                               {
-                                       /* k9er: these aren't the true angles -- the real ones are very convoluted and difficult to understand
-                                        * essentially the prior definitions of the zones now overlap,
-                                        * ... with the overturn zone extending below bestangle, and eventually covering the whole hud
-                                        * ... and somehow the neutral zone extends above bestangle, and eventually covers the whole hud (i think)
-                                        * overall showing it accurately is just confusing and unnecessary to add
-                                        * thankfully the bestangle formula is unchanged, so the least confusing option is likely as follows:
-                                        */
-                                       overturn_angle = bestangle;
-                                       prebestangle = bestangle;
-                               }
+                               if(angle < 0)
+                                       angle += 180;
                                else
-                               {
-                                       // use angles as if in air
-                                       // no need to check if numerator < denominator, since all numerators < max onground speed < speed = all denominators
-                                       bestangle = acos(bestspeed / speed) * RAD2DEG;
-                                       prebestangle = acos(movespeed / speed) * RAD2DEG;
-                                       overturn_angle = acos(-(airstopaccel * maxaccel / 2) / speed) * RAD2DEG;
-                               }
+                                       angle -= 180;
                        }
                }
                else
                {
-                       // draw airborne angles. see above for documentation
-                       bestangle = speed > bestspeed
-                               ? acos(bestspeed / speed) * RAD2DEG
-                               : 0;
-                       prebestangle = speed > movespeed
-                               ? acos(movespeed / speed) * RAD2DEG
-                               : 0;
-                       // with airstopaccel, delta_max = acos(airstopaccel * -a / 2v), only in air
-                       overturn_angle = speed > airstopaccel * maxaccel / 2
-                               ? acos(-(airstopaccel * maxaccel / 2) / speed) * RAD2DEG
-                               : 180;
-               }
-               real_bestangle = bestangle;
-               real_prebestangle = prebestangle;
-               real_overturn_angle = overturn_angle;
-
-               /*
-                * k9er: proper W-turn angle assuming sv_aircontrol_power == 2 is acos(-speed/a * (cos((acos(V) + M_PI * 2) / 3) * 2 + 1)) rad,
-                * ... where a=dt*32*aircontrol, and V=1-(a*a)/(speed*speed),
-                * ... but this very quickly loses accuracy -- should be a strictly decreasing function, yet it increases at only speed=722 with 125 fps
-                * also note this is only valid when such angle is not in the accelzone, formula taking acceleration into account is unfathomably complicated
-                * afaik there's no simplified version of this formula that doesn't involve complex numbers, other than one valid for only speed<27.1 roughly
-                * furthermore, this function quite rapidly approaches its asymptote of ~35.26, e.g. being ~0.68 away when at only speed=600
-                * this asymptote is independent of whether the player is crouching or has haste, although they must be airborne
-                * thus, the best option is to just draw the asymptote (acos(sqrt(2/3))),
-                * ... but the proper angle can be drawn too if the player wants (hud_panel_strafehud_wturn_proper 1)
-                * this is only enabled if sv_airaccel_qw == 1 since otherwise W-turning gives acceleration, unless hud_panel_strafehud_wturn_unrestricted 1
-                * when sv_aircontrol_power != 2 (abbr. "p"), the asymptote is instead acos(sqrt(p/(p+1))). full formula is too difficult to calculate,
-                * ... so the angle will only be shown with hud_panel_strafehud_wturn_proper 0
-                * this doesn't have support for sv_aircontrol_sideways == 1
-                */
-               bool wturning    = !onground && wishangle == 0 && (keys_fwd == STRAFEHUD_KEYS_FORWARD || (aircontrol_backwards && keys_fwd == STRAFEHUD_KEYS_BACKWARD));
-               bool wturn_valid = aircontrol && PHYS_AIRCONTROL_PENALTY(strafeplayer) == 0 && (airaccel_qw || autocvar_hud_panel_strafehud_wturn_unrestricted == 1);
-               bool wturn_check = autocvar_hud_panel_strafehud_wturn && !immobile && wturn_valid;
-               if(wturn_check)
-               {
-                       float wturn_power = PHYS_AIRCONTROL_POWER(strafeplayer);
-                       if(wturn_power == 2)
-                       {
-                               float wturn_a = 32 * aircontrol * dt;
-                               float wturn_V = 1 - (wturn_a * wturn_a) / (speed * speed);
-                               if(autocvar_hud_panel_strafehud_wturn_proper && wturn_a > 1 && wturn_V < 1 && wturn_V > -1)
-                                       wturn_bestangle = acos(-speed / wturn_a * (cos((acos(wturn_V) + M_PI * 2) / 3) * 2 + 1)) * RAD2DEG;
-                               else
-                                       wturn_bestangle = ACOS_SQRT2_3_DEG;
-                               real_wturn_bestangle = wturn_bestangle;
-                       }
-                       else if(!autocvar_hud_panel_strafehud_wturn_proper && wturn_power >= 0)
-                       {
-                               wturn_bestangle = acos(sqrt(wturn_power / (wturn_power + 1))) * RAD2DEG;
-                               real_wturn_bestangle = wturn_bestangle;
-                       }
-                       else
-                       {
-                               wturn_valid = false;
-                               wturn_check = false;
-                       }
+                       angle = 0;
+                       fwd = true;
                }
+       }
+       else // simulate turning for HUD setup
+       {
+               const float demo_maxangle = 55; // maximum angle before changing direction
+               const float demo_turnspeed = 40; // turning speed in degrees per second
+               static float demo_position = -37 / demo_maxangle; // current positioning value between -1 and +1
 
-               // draw the switch indicators as if strafing normally (W+A style), while W-turning or side strafing
-               float n_bestangle                   = 0;
-               float n_odd_bestangle;
-               float n_bestangle_offset            = 0;
-               float n_switch_bestangle_offset     = 0;
-               float n_odd_bestangle_offset        = 0;
-               float n_switch_odd_bestangle_offset = 0;
-               bool  draw_normal = ((autocvar_hud_panel_strafehud_switch >= STRAFEHUD_SWITCH_NORMAL && wturning)
-                       || (autocvar_hud_panel_strafehud_switch == STRAFEHUD_SWITCH_SIDESTRAFE && turn));
-               if(draw_normal)
+               if(autocvar__hud_panel_strafehud_demo)
                {
-                       // recalculate bestangle as if strafing normally
-                       float n_maxspeed  = PHYS_MAXAIRSPEED(strafeplayer) * maxspeed_mod;
-                       float n_movespeed = n_maxspeed;
-                       float n_maxaccel  = PHYS_AIRACCELERATE(strafeplayer) * dt * n_movespeed;
-                       float n_bestspeed = max(n_movespeed - n_maxaccel, 0);
-                       n_bestangle = speed > n_bestspeed
-                               ? acos(n_bestspeed / speed) * RAD2DEG - 45
-                               : -45;
+                       float demo_dt = time - hud_lasttime;
+                       float demo_step = (demo_turnspeed / demo_maxangle) * demo_dt;
+                       demo_position = ((demo_position + demo_step) % 4 + 4) % 4;
                }
 
-               // determine the minimal required HUD angle to contain the full strafing angle range
-               // this is useful for the velocity centered mode where the zones do not follow the strafing angle
-               // how it works:
-               //   the angle where the most acceleration occurs moves relative to the player velocity
-               //   from 0 - wishangle to real_overturn_angle - wishangle
-               //   the angle farther away from the center is the maximum the optimal strafing angle can
-               //   diverge from the direction of velocity
-               //   this angle has to be multiplied by two since the HUD extends in both directions which
-               //   halves the amount it extends in a single direction
-               range_minangle = max(real_wishangle, real_overturn_angle - real_wishangle) * 2;
-
-               float range_normal = autocvar_hud_panel_strafehud_range;
-               float range_side   = autocvar_hud_panel_strafehud_range_sidestrafe;
-               float range_used;
-               if(range_normal == 0)
-                       range_normal = autocvar__hud_configure ? 90 : range_minangle; // use minimum angle required if dynamically setting hud angle
-               if(range_side == -1) // use the normal range
-                       range_used = range_normal;
+               // triangle wave function
+               if(demo_position > 3)
+                       angle = -1 + (demo_position - 3);
+               else if(demo_position > 1)
+                       angle = +1 - (demo_position - 1);
                else
-               {
-                       if(range_side == 0)
-                               range_side = autocvar__hud_configure ? 90 : range_minangle;
-                       range_used = GeomLerp(range_normal, strafity, range_side);
-               }
-               hudangle = bound(0, fabs(range_used), 360); // limit HUD range to 360 degrees, higher values don't make sense
+                       angle = demo_position;
+               angle *= demo_maxangle;
 
-               if(direction == STRAFEHUD_DIRECTION_LEFT) // the angle becomes negative in case we strafe left
-               {
-                       n_bestangle *= -1;
-                       bestangle *= -1;
-                       prebestangle *= -1;
-                       overturn_angle *= -1;
-               }
-               odd_bestangle = -bestangle - wishangle;
-               n_odd_bestangle = -n_bestangle - wishangle;
-               bestangle -= wishangle;
-               prebestangle -= wishangle;
-               overturn_angle -= wishangle;
-
-               // various offsets and size calculations of hud indicator elements
-               // how much is hidden by the current hud angle
-               hidden_width = (360 - hudangle) / hudangle * panel_size.x;
-
-               // current angle
-               currentangle_size.x = autocvar_hud_panel_strafehud_angle_width;
-               currentangle_size.y = autocvar_hud_panel_strafehud_angle_height;
-               currentangle_size.z = 0;
-               if(!autocvar_hud_panel_strafehud_uncapped)
-               {
-                       currentangle_size.x = min(currentangle_size.x, 10);
-                       currentangle_size.y = min(currentangle_size.y, 10);
-               }
-               currentangle_size.x *= panel_size.x;
-               currentangle_size.y *= panel_size.y;
-               if(!autocvar_hud_panel_strafehud_uncapped)
-               {
-                       currentangle_size.x = max(currentangle_size.x, 1);
-                       currentangle_size.y = max(currentangle_size.y, 1);
-               }
-               else
-               {
-                       currentangle_size.y = max(currentangle_size.y, 0);
-               }
-               if(mode == STRAFEHUD_MODE_VIEW_CENTERED)
-                       currentangle_offset = angle / hudangle * panel_size.x;
-               else
-                       currentangle_offset = bound(-hudangle / 2, angle, hudangle / 2) / hudangle * panel_size.x + panel_size.x / 2;
+               fwd = true;
+               wishangle = 45;
+               if(angle < 0)
+                       wishangle *= -1;
+       }
 
-               // best strafe acceleration angle
-               if((autocvar_hud_panel_strafehud_switch || autocvar_hud_panel_strafehud_bestangle) && speed >= minspeed)
-               {
-                       bestangle_offset        =  bestangle / hudangle * panel_size.x + panel_size.x / 2;
-                       switch_bestangle_offset = -bestangle / hudangle * panel_size.x + panel_size.x / 2;
-                       switch_bestangle_width  = panel_size.x * autocvar_hud_panel_strafehud_switch_width;
-                       if(!autocvar_hud_panel_strafehud_uncapped)
-                               switch_bestangle_width = max(switch_bestangle_width, 1);
+       // invert the wish angle when strafing backwards
+       if(!fwd)
+               wishangle *= -1;
 
-                       if((angle > -wishangle && direction == STRAFEHUD_DIRECTION_LEFT) || (angle < -wishangle && direction == STRAFEHUD_DIRECTION_RIGHT))
-                       {
-                               odd_angles = true;
-                               odd_bestangle_offset        = odd_bestangle / hudangle * panel_size.x + panel_size.x / 2;
-                               switch_odd_bestangle_offset = (odd_bestangle + bestangle * 2) / hudangle * panel_size.x + panel_size.x / 2;
+       // flip angles if v_flipped is enabled
+       if(autocvar_v_flipped)
+       {
+               angle *= -1;
+               wishangle *= -1;
+       }
+
+       float airstopaccel = PHYS_AIRSTOPACCELERATE(strafeplayer);
+       if(airstopaccel == 0)
+               airstopaccel = 1; // values of 0 are equivalent to 1
+
+       // best angle to strafe at
+       float bestangle;
+       float prebestangle;
+       float overturnangle;
+       if(!moving)
+       {
+               // these are unused (neutral fills whole strafe bar)
+               prebestangle = bestangle = 0;
+               overturnangle = 180;
+       }
+       else if(onground && autocvar_hud_panel_strafehud_onground_friction)
+       {
+               // draw ground angles
+               {
+                       // delta_opt = acos((s - a) / v_f), same in air
+                       bestangle = strafespeed > bestspeed
+                               ? acos(bestspeed / strafespeed) * RAD2DEG // case 1
+                               : 0; // case 2
+                       // case 1: normal. case 2: low speed, best angle is forwards
+               }
+               {
+                       // needed later if autocvar_hud_panel_strafehud_wturn != STRAFEHUD_WTURN_NONE,
+                       // ... so calculate even if autocvar_hud_panel_strafehud_bar_preaccel == 0
+                       float prebestangle_sqrt = movespeed * movespeed + strafespeed * strafespeed - speed * speed;
+                       // delta_min = acos(sqrt(s^2 - v_f^2 + v^2) / v_f), or just acos(s / v) in air
+                       prebestangle = (prebestangle_sqrt > 0 && strafespeed > sqrt(prebestangle_sqrt))
+                               ? acos(sqrt(prebestangle_sqrt) / strafespeed) * RAD2DEG // case 1
+                               : (prebestangle_sqrt > 0 ? 0 : 90); // case 2 : case 3
+                       // case 1: normal. case 2: low speed, best angle is forwards. case 3: landed at high speed, neutral zone is very large (see explanation below)
+               }
+               {
+                       float overturn_numer = speed * speed - strafespeed * strafespeed - maxaccel * maxaccel;
+                       float overturn_denom = 2 * maxaccel * strafespeed;
+                       // delta_max = acos((v^2 - v_f^2 - a^2) / (2av_f)), or just acos(-a / 2v) if in air
+                       overturnangle = overturn_denom > fabs(overturn_numer)
+                               ? acos(overturn_numer / overturn_denom) * RAD2DEG // case 1
+                               : (overturn_numer < 0 ? 180 : 0); // case 2 : case 3
+                       // case 1: normal. case 2: low speed, turning anywhere will gain speed. case 3: landed at high speed, turning anywhere will lose speed (due to friction)
+               }
+               if(overturnangle < bestangle || bestangle < prebestangle)
+               {
+                       // these conditions occur when you land at high speed (above max onground speed), such that every wishangle will result in a speed loss due to friction
+                       if(autocvar_hud_panel_strafehud_onground_mode == STRAFEHUD_ONGROUND_OVERTURN)
+                       {
+                               // make overturn fill the whole strafe bar
+                               // most correct option by the true definition of accel, since every angle results in deceleration
+                               prebestangle = bestangle = 0;
+                               overturnangle = 0;
+                       }
+                       else if(autocvar_hud_panel_strafehud_onground_mode == STRAFEHUD_ONGROUND_GROUND)
+                       {
+                               /* k9er: these aren't the true angles -- the real ones are very convoluted and difficult to understand
+                                * essentially the prior definitions of the zones now overlap,
+                                * ... with the overturn zone extending below bestangle, and eventually covering the whole hud
+                                * ... and somehow the neutral zone extends above bestangle, and eventually covers the whole hud (i think)
+                                * overall showing it accurately is just confusing and unnecessary to add
+                                * thankfully the bestangle formula is unchanged, so the least confusing option is likely as follows:
+                                */
+                               overturnangle = bestangle;
+                               prebestangle = bestangle;
                        }
-                       if(draw_normal)
+                       else
                        {
-                               n_bestangle_offset        =  n_bestangle / hudangle * panel_size.x + panel_size.x / 2;
-                               n_switch_bestangle_offset = -n_bestangle / hudangle * panel_size.x + panel_size.x / 2;
-                               if(odd_angles)
-                               {
-                                       n_odd_bestangle_offset        = n_odd_bestangle / hudangle * panel_size.x + panel_size.x / 2;
-                                       n_switch_odd_bestangle_offset = (n_odd_bestangle + n_bestangle * 2) / hudangle * panel_size.x + panel_size.x / 2;
-                               }
+                               // use angles as if in air
+                               // no need to check if numerator < denominator, since all numerators < max onground speed < speed = all denominators
+                               bestangle = acos(bestspeed / speed) * RAD2DEG;
+                               prebestangle = acos(movespeed / speed) * RAD2DEG;
+                               overturnangle = acos(-(airstopaccel * maxaccel / 2) / speed) * RAD2DEG;
                        }
                }
-
-               // best angle to aim at when W-turning to maximally rotate velocity vector
-               if(wturn_check)
+       }
+       else
+       {
+               // draw airborne angles. see above for documentation
+               bestangle = speed > bestspeed
+                       ? acos(bestspeed / speed) * RAD2DEG
+                       : 0;
+               prebestangle = speed > movespeed
+                       ? acos(movespeed / speed) * RAD2DEG
+                       : 0;
+               // with airstopaccel, delta_max = acos(airstopaccel * -a / 2v), only in air
+               overturnangle = speed > airstopaccel * maxaccel / 2
+                       ? acos(-(airstopaccel * maxaccel / 2) / speed) * RAD2DEG
+                       : 180;
+       }
+       // absolute_* variables which are always positive with no wishangle offset
+       float absolute_bestangle = bestangle;
+       float absolute_prebestangle = prebestangle;
+       float absolute_overturnangle = overturnangle;
+
+       float aircontrol = PHYS_AIRCONTROL(strafeplayer);
+       bool aircontrol_backwards = PHYS_AIRCONTROL_BACKWARDS(strafeplayer);
+       bool is_aircontrol_keys = keys_fwd == STRAFEHUD_KEYS_FORWARD || (aircontrol_backwards && keys_fwd == STRAFEHUD_KEYS_BACKWARD);
+       bool is_aircontrol_direction = fwd || aircontrol_backwards;
+       bool airaccel_qw = PHYS_AIRACCEL_QW(strafeplayer) == 1;
+
+       /*
+        * k9er: proper W-turn angle assuming sv_aircontrol_power == 2 is acos(-speed/a * (cos((acos(V) + M_PI * 2) / 3) * 2 + 1)) rad,
+        * ... where a=dt*32*aircontrol, and V=1-(a*a)/(speed*speed),
+        * ... but this very quickly loses accuracy -- should be a strictly decreasing function, yet it increases at only speed=722 with 125 fps
+        * also note this is only valid when such angle is not in the accelzone, formula taking acceleration into account is unfathomably complicated
+        * afaik there's no simplified version of this formula that doesn't involve complex numbers, other than one valid for only speed<27.1 roughly
+        * furthermore, this function quite rapidly approaches its asymptote of ~35.26, e.g. being ~0.68 away when at only speed=600
+        * this asymptote is independent of whether the player is crouching or has haste, although they must be airborne
+        * thus, the best option is to just draw the asymptote (acos(sqrt(2/3))),
+        * ... but the proper angle can be drawn too if the player wants (hud_panel_strafehud_wturn_proper 1)
+        * this is only enabled if sv_airaccel_qw == 1 since otherwise W-turning gives acceleration, unless hud_panel_strafehud_wturn_unrestricted 1
+        * when sv_aircontrol_power != 2 (abbr. "p"), the asymptote is instead acos(sqrt(p/(p+1))). full formula is too difficult to calculate,
+        * ... so the angle will only be shown with hud_panel_strafehud_wturn_proper 0
+        * this doesn't have support for sv_aircontrol_sideways == 1
+        */
+       bool wturning = (wishangle == 0) && !onground && is_aircontrol_keys;
+       bool wturn_valid = false;
+       float wturn_bestangle = 0;
+       if(autocvar_hud_panel_strafehud_wturn && moving &&
+               aircontrol && PHYS_AIRCONTROL_PENALTY(strafeplayer) == 0 &&
+               (airaccel_qw || autocvar_hud_panel_strafehud_wturn_unrestricted == 1))
+       {
+               float wturn_power = PHYS_AIRCONTROL_POWER(strafeplayer);
+               if(wturn_power == 2)
                {
-                       bool wturn_show = autocvar_hud_panel_strafehud_wturn == STRAFEHUD_WTURN_SIDESTRAFE ? (fwd || aircontrol_backwards)
-                               : autocvar_hud_panel_strafehud_wturn == STRAFEHUD_WTURN_NORMAL ? ((fwd || aircontrol_backwards) && !turn)
-                               : autocvar_hud_panel_strafehud_wturn == STRAFEHUD_WTURN_NONE ? false
-                               : wturning;
-                       if(wturn_show && real_wturn_bestangle < real_prebestangle && !onground)
-                       {
-                               wturn_left_bestangle_offset  =  wturn_bestangle / hudangle * panel_size.x + panel_size.x / 2;
-                               wturn_right_bestangle_offset = -wturn_bestangle / hudangle * panel_size.x + panel_size.x / 2;
-                               wturn_bestangle_width        = panel_size.x * autocvar_hud_panel_strafehud_wturn_width;
-                               if(!autocvar_hud_panel_strafehud_uncapped)
-                                       wturn_bestangle_width = max(wturn_bestangle_width, 1);
-                       }
+                       float wturn_a = 32 * aircontrol * dt;
+                       float wturn_V = 1 - (wturn_a * wturn_a) / (speed * speed);
+                       if(autocvar_hud_panel_strafehud_wturn_proper && wturn_a > 1 && wturn_V < 1 && wturn_V > -1)
+                               wturn_bestangle = acos(-speed / wturn_a * (cos((acos(wturn_V) + M_PI * 2) / 3) * 2 + 1)) * RAD2DEG;
+                       else
+                               wturn_bestangle = ACOS_SQRT2_3_DEG;
+                       wturn_valid = true;
                }
-
-               // the neutral zone fills the whole strafe bar
-               if(immobile)
+               else if(!autocvar_hud_panel_strafehud_wturn_proper && wturn_power >= 0)
                {
-                       // draw neutral zone
-                       if(panel_size.x > 0 && panel_size.y > 0 && autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha > 0)
-                       {
-                               switch(autocvar_hud_panel_strafehud_style)
-                               {
-                                       default:
-                                       case STRAFEHUD_STYLE_DRAWFILL:
-                                               drawfill(
-                                                       panel_pos, panel_size,
-                                                       autocvar_hud_panel_strafehud_bar_neutral_color,
-                                                       autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha,
-                                                       DRAWFLAG_NORMAL);
-                                               break;
-
-                                       case STRAFEHUD_STYLE_PROGRESSBAR:
-                                               HUD_Panel_DrawProgressBar(
-                                                       panel_pos, panel_size, "progressbar", 1, 0, 0,
-                                                       autocvar_hud_panel_strafehud_bar_neutral_color,
-                                                       autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha,
-                                                       DRAWFLAG_NORMAL);
-                               }
-                       }
+                       wturn_bestangle = acos(sqrt(wturn_power / (wturn_power + 1))) * RAD2DEG;
+                       wturn_valid = true;
                }
-               else
-               {
-                       // calculate various zones of the strafe-o-meter
-                       if(autocvar_hud_panel_strafehud_bar_preaccel)
-                               preaccelzone_width = fabs(real_bestangle - real_prebestangle) / hudangle * panel_size.x;
-                       else
-                               preaccelzone_width = 0;
-                       accelzone_width = (real_overturn_angle - real_bestangle) / hudangle * panel_size.x;
-                       overturn_width = (360 - real_overturn_angle * 2) / hudangle * panel_size.x;
-                       neutral_width = 360 / hudangle * panel_size.x - accelzone_width * 2 - preaccelzone_width * 2 - overturn_width;
-
-                       {
-                               float current_offset = 0;
-                               preaccelzone_right_offset = current_offset;
-                               current_offset += preaccelzone_width;
-
-                               accelzone_right_offset = current_offset;
-                               current_offset += accelzone_width;
-
-                               overturn_offset = current_offset;
-                               current_offset += overturn_width;
-
-                               accelzone_left_offset = current_offset;
-                               current_offset += accelzone_width;
+       }
+       float absolute_wturn_bestangle = wturn_bestangle;
+
+       // draw the switch indicators as if strafing normally (W+A style), while W-turning or side strafing
+       float n_bestangle = 0;
+       float absolute_n_prebestangle = 0; // also needed for W-turn angles
+       bool draw_normal = ((autocvar_hud_panel_strafehud_switch >= STRAFEHUD_SWITCH_NORMAL && wturning)
+               || (autocvar_hud_panel_strafehud_switch == STRAFEHUD_SWITCH_SIDESTRAFE && turn));
+       if(draw_normal || wturn_valid)
+       {
+               // recalculate bestangle as if strafing normally
+               float n_maxspeed  = PHYS_MAXAIRSPEED(strafeplayer) * maxspeed_mod;
+               float n_movespeed = n_maxspeed;
+               float n_maxaccel  = PHYS_AIRACCELERATE(strafeplayer) * dt * n_movespeed;
+               float n_bestspeed = max(n_movespeed - n_maxaccel, 0);
+               n_bestangle = speed > n_bestspeed
+                       ? acos(n_bestspeed / speed) * RAD2DEG - 45
+                       : -45;
+               absolute_n_prebestangle = speed > n_movespeed
+                       ? acos(n_movespeed / speed) * RAD2DEG
+                       : 0;
+       }
 
-                               preaccelzone_left_offset = current_offset;
-                               current_offset += preaccelzone_width;
+       float hudangle = StrafeHUD_DetermineHudAngle(absolute_wishangle, absolute_overturnangle, strafity);
 
-                               // the wrapping code may struggle if we always append it on the right side
-                               neutral_offset = direction == STRAFEHUD_DIRECTION_LEFT ? current_offset : -neutral_width;
-                       }
+       float antiflicker_angle = bound(0, autocvar_hud_panel_strafehud_antiflicker_angle, 180);
+       float direction = StrafeHUD_DetermineDirection(angle, wishangle, antiflicker_angle);
 
-                       // shift hud if operating in view angle centered mode
-                       if(mode == STRAFEHUD_MODE_VIEW_CENTERED)
-                       {
-                               shift_offset = -currentangle_offset;
-                               bestangle_offset += shift_offset;
-                               switch_bestangle_offset += shift_offset;
-                               odd_bestangle_offset += shift_offset;
-                               switch_odd_bestangle_offset += shift_offset;
-                               n_bestangle_offset += shift_offset;
-                               n_switch_bestangle_offset += shift_offset;
-                               n_odd_bestangle_offset += shift_offset;
-                               n_switch_odd_bestangle_offset += shift_offset;
-                               wturn_left_bestangle_offset += shift_offset;
-                               wturn_right_bestangle_offset += shift_offset;
-                       }
-                       if(direction == STRAFEHUD_DIRECTION_LEFT)
-                               shift_offset += -360 / hudangle * panel_size.x;
-
-                       // calculate how far off-center the strafe zones currently are
-                       shift_offset += (panel_size.x + neutral_width) / 2 - wishangle / hudangle * panel_size.x;
-
-                       // shift strafe zones into correct place
-                       neutral_offset += shift_offset;
-                       accelzone_left_offset += shift_offset;
-                       accelzone_right_offset += shift_offset;
-                       preaccelzone_left_offset += shift_offset;
-                       preaccelzone_right_offset += shift_offset;
-                       overturn_offset += shift_offset;
-
-                       // draw left acceleration zone
-                       if(accelzone_width > 0)
-                               HUD_Panel_DrawStrafeHUD(
-                                       accelzone_left_offset, accelzone_width, hidden_width,
-                                       autocvar_hud_panel_strafehud_bar_accel_color,
-                                       autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha,
-                                       autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_LEFT, false);
-
-                       if(autocvar_hud_panel_strafehud_bar_preaccel && preaccelzone_width > 0)
-                               HUD_Panel_DrawStrafeHUD(
-                                       preaccelzone_left_offset, preaccelzone_width, hidden_width,
-                                       autocvar_hud_panel_strafehud_bar_preaccel_color,
-                                       autocvar_hud_panel_strafehud_bar_preaccel_alpha * panel_fg_alpha,
-                                       autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_RIGHT, false);
-
-                       // draw right acceleration zone
-                       if(accelzone_width > 0)
-                               HUD_Panel_DrawStrafeHUD(
-                                       accelzone_right_offset, accelzone_width, hidden_width,
-                                       autocvar_hud_panel_strafehud_bar_accel_color,
-                                       autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha,
-                                       autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_RIGHT, false);
-
-                       if(autocvar_hud_panel_strafehud_bar_preaccel && preaccelzone_width > 0)
-                               HUD_Panel_DrawStrafeHUD(
-                                       preaccelzone_right_offset, preaccelzone_width, hidden_width,
-                                       autocvar_hud_panel_strafehud_bar_preaccel_color,
-                                       autocvar_hud_panel_strafehud_bar_preaccel_alpha * panel_fg_alpha,
-                                       autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_LEFT, false);
-
-                       // draw overturn zone
-                       if(overturn_width > 0)
-                               HUD_Panel_DrawStrafeHUD(
-                                       overturn_offset, overturn_width, hidden_width,
-                                       autocvar_hud_panel_strafehud_bar_overturn_color,
-                                       autocvar_hud_panel_strafehud_bar_overturn_alpha * panel_fg_alpha,
-                                       autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_BOTH, false);
-
-                       // draw neutral zone
-                       if(neutral_width > 0)
-                               HUD_Panel_DrawStrafeHUD(
-                                       neutral_offset, neutral_width, hidden_width,
-                                       autocvar_hud_panel_strafehud_bar_neutral_color,
-                                       autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha,
-                                       autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_NONE, false);
-
-                       // only draw switch indicators if minspeed is reached (switch_bestangle_width init to 0)
-                       if(autocvar_hud_panel_strafehud_switch && switch_bestangle_width > 0 && autocvar_hud_panel_strafehud_switch_alpha > 0)
-                       {
-                               // draw the switch indicator(s)
-                               float offset = draw_normal
-                                       ? (odd_angles ? n_odd_bestangle_offset : n_bestangle_offset)
-                                       : (odd_angles ? odd_bestangle_offset : bestangle_offset);
-                               float switch_offset = draw_normal
-                                       ? (odd_angles ? n_switch_odd_bestangle_offset : n_switch_bestangle_offset)
-                                       : (odd_angles ? switch_odd_bestangle_offset : switch_bestangle_offset);
-
-                               HUD_Panel_DrawStrafeHUD(
-                                       switch_offset, switch_bestangle_width, hidden_width,
-                                       autocvar_hud_panel_strafehud_switch_color,
-                                       autocvar_hud_panel_strafehud_switch_alpha * panel_fg_alpha,
-                                       STRAFEHUD_STYLE_DRAWFILL, STRAFEHUD_GRADIENT_NONE, true);
-
-                               if(direction == STRAFEHUD_DIRECTION_NONE || draw_normal)
-                                       HUD_Panel_DrawStrafeHUD(
-                                               offset, switch_bestangle_width, hidden_width,
-                                               autocvar_hud_panel_strafehud_switch_color,
-                                               autocvar_hud_panel_strafehud_switch_alpha * panel_fg_alpha,
-                                               STRAFEHUD_STYLE_DRAWFILL, STRAFEHUD_GRADIENT_NONE, true);
-                       }
+       if(direction == STRAFEHUD_DIRECTION_LEFT) // the angle becomes negative in case we strafe left
+       {
+               n_bestangle *= -1;
+               bestangle *= -1;
+               prebestangle *= -1;
+               overturnangle *= -1;
+       }
+       float opposite_bestangle = -bestangle;
+       float n_opposite_bestangle = -n_bestangle;
+
+       bestangle -= wishangle;
+       opposite_bestangle -= wishangle;
+       n_opposite_bestangle -= wishangle;
+       prebestangle -= wishangle;
+       overturnangle -= wishangle;
+
+       int mode;
+       if(autocvar_hud_panel_strafehud_mode >= 0 && autocvar_hud_panel_strafehud_mode <= 1)
+               mode = autocvar_hud_panel_strafehud_mode;
+       else
+               mode = STRAFEHUD_MODE_VIEW_CENTERED;
 
-                       // only draw wturn indicators if conditions were met (wturn_bestangle_width init to 0)
-                       if(autocvar_hud_panel_strafehud_wturn && wturn_bestangle_width > 0 && autocvar_hud_panel_strafehud_wturn_alpha > 0)
-                       {
-                               // draw the wturn indicators
-                               HUD_Panel_DrawStrafeHUD(
-                                       wturn_left_bestangle_offset, wturn_bestangle_width, hidden_width,
-                                       autocvar_hud_panel_strafehud_wturn_color,
-                                       autocvar_hud_panel_strafehud_wturn_alpha * panel_fg_alpha,
-                                       STRAFEHUD_STYLE_DRAWFILL, STRAFEHUD_GRADIENT_NONE, true);
-                               HUD_Panel_DrawStrafeHUD(
-                                       wturn_right_bestangle_offset, wturn_bestangle_width, hidden_width,
-                                       autocvar_hud_panel_strafehud_wturn_color,
-                                       autocvar_hud_panel_strafehud_wturn_alpha * panel_fg_alpha,
-                                       STRAFEHUD_STYLE_DRAWFILL, STRAFEHUD_GRADIENT_NONE, true);
-                       }
-               }
+       // best strafe acceleration angle
+       float changeangle = -bestangle;
+       float n_changeangle = -n_bestangle;
+       float n_opposite_changeangle = n_opposite_bestangle + n_bestangle * 2;
 
-               // slick detector
-               slickdetector_height = max(autocvar_hud_panel_strafehud_slickdetector_height, 0);
-               if(!autocvar_hud_panel_strafehud_uncapped)
-                       slickdetector_height = min(slickdetector_height, 1);
-               slickdetector_height *= panel_size.y;
-               if(autocvar_hud_panel_strafehud_slickdetector &&
-                  autocvar_hud_panel_strafehud_slickdetector_range > 0 &&
-                  autocvar_hud_panel_strafehud_slickdetector_alpha > 0 &&
-                  slickdetector_height > 0 &&
-                  panel_size.x > 0)
-               {
-                       float slicksteps = max(autocvar_hud_panel_strafehud_slickdetector_granularity, 0);
-                       bool slickdetected = false;
+       // minimum speed for change indicators
+       float minspeed = autocvar_hud_panel_strafehud_switch_minspeed;
+       if(minspeed < 0)
+               minspeed = bestspeed + frictionspeed;
 
-                       if(!autocvar_hud_panel_strafehud_uncapped)
-                               slicksteps = min(slicksteps, 4);
-                       slicksteps = 90 * DEG2RAD / 2 ** slicksteps;
+       bool opposite_direction = false;
+       float opposite_changeangle = 0;
+       if((angle > -wishangle && direction == STRAFEHUD_DIRECTION_LEFT) || (angle < -wishangle && direction == STRAFEHUD_DIRECTION_RIGHT))
+       {
+               opposite_direction = true;
+               opposite_changeangle = opposite_bestangle + bestangle * 2;
+       }
 
-                       slickdetected = real_onslick; // don't need to traceline if already touching slick
+       // best angle to aim at when W-turning to maximally rotate velocity vector
+       float wturn_left_bestangle = wturn_bestangle;
+       float wturn_right_bestangle = -wturn_bestangle;
 
-                       // traceline into every direction
-                       trace_dphitq3surfaceflags = 0;
-                       vector traceorigin = strafeplayer.origin + eZ * strafeplayer.mins.z;
-                       for(float i = 0; i < 90 * DEG2RAD - 0.00001 && !slickdetected; i += slicksteps)
-                       {
-                               vector slickoffset;
-                               float slickrotate;
-                               slickoffset.z = -cos(i) * autocvar_hud_panel_strafehud_slickdetector_range;
-                               slickrotate = sin(i) * autocvar_hud_panel_strafehud_slickdetector_range;
-
-                               for(float j = 0; j < 360 * DEG2RAD - 0.00001 && !slickdetected; j += slicksteps)
-                               {
-                                       slickoffset.x = sin(j) * slickrotate;
-                                       slickoffset.y = cos(j) * slickrotate;
-
-                                       traceline(traceorigin, traceorigin + slickoffset, MOVE_NOMONSTERS, strafeplayer);
-                                       if((PHYS_FRICTION(strafeplayer) == 0 && trace_fraction < 1)
-                                       || (trace_dphitq3surfaceflags & Q3SURFACEFLAG_SLICK))
-                                               slickdetected = true;
-                                       if(i == 0)
-                                               break;
-                               }
-                       }
+       // shift hud if operating in view angle centered mode
+       float shiftangle = 0;
+       if(mode == STRAFEHUD_MODE_VIEW_CENTERED)
+       {
+               shiftangle = -angle;
+               bestangle += shiftangle;
+               changeangle += shiftangle;
+               opposite_bestangle += shiftangle;
+               opposite_changeangle += shiftangle;
+               n_bestangle += shiftangle;
+               n_changeangle += shiftangle;
+               n_opposite_bestangle += shiftangle;
+               n_opposite_changeangle += shiftangle;
+               wturn_left_bestangle += shiftangle;
+               wturn_right_bestangle += shiftangle;
+       }
 
-                       // if a traceline hit a slick surface
-                       if(slickdetected)
-                       {
-                               vector slickdetector_size = panel_size;
-                               slickdetector_size.y = slickdetector_height;
-
-                               // top horizontal line
-                               drawfill(
-                                       panel_pos - eY * slickdetector_size.y, slickdetector_size,
-                                       autocvar_hud_panel_strafehud_slickdetector_color,
-                                       autocvar_hud_panel_strafehud_slickdetector_alpha * panel_fg_alpha,
-                                       DRAWFLAG_NORMAL);
-
-                               // 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);
-                       }
+       StrafeHUD_DrawStrafeMeter(shiftangle, wishangle, absolute_bestangle, absolute_prebestangle, absolute_overturnangle, moving, hudangle);
 
-                       text_offset_top = text_offset_bottom = slickdetector_height;
-               }
+       float text_offset_top;
+       float text_offset_bottom;
+       bool all_slick = PHYS_FRICTION(strafeplayer) == 0;
+       text_offset_top = text_offset_bottom = StrafeHUD_DrawSlickDetector(strafeplayer, all_slick && real_onground ? true : real_onslick);
 
-               // direction indicator
-               if(autocvar_hud_panel_strafehud_direction)
-               {
-                       direction_size_vertical.x = autocvar_hud_panel_strafehud_direction_width;
-                       if(!autocvar_hud_panel_strafehud_uncapped)
-                               direction_size_vertical.x = min(direction_size_vertical.x, 1);
-                       direction_size_vertical.x *= panel_size.y;
-                       if(!autocvar_hud_panel_strafehud_uncapped)
-                               direction_size_vertical.x = max(direction_size_vertical.x, 1);
-                       direction_size_vertical.y = panel_size.y + direction_size_vertical.x * 2;
-                       direction_size_vertical.z = 0;
-                       direction_size_horizontal.x = panel_size.x * min(autocvar_hud_panel_strafehud_direction_length, .5);
-                       direction_size_horizontal.y = direction_size_vertical.x;
-                       direction_size_horizontal.z = 0;
-               }
+       if(autocvar_hud_panel_strafehud_direction)
+               StrafeHUD_DrawDirectionIndicator(direction, opposite_direction, fwd);
 
-               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)
-               {
-                       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)
-                               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);
-               }
+       // determine the strafing ratio and the angle indicator color
+       vector currentangle_color = autocvar_hud_panel_strafehud_angle_neutral_color;
+       float strafe_ratio = 0;
+       if(moving)
+       {
+               float moveangle = fabs(angle + wishangle);
+               if(moveangle > 180) moveangle = 360 - moveangle; // restricted to between 0 and 180
 
-               // draw the actual strafe angle
-               if(!immobile)
+               // player is overturning
+               if(moveangle >= absolute_overturnangle)
                {
-                       float moveangle = fabs(angle + wishangle);
-                       if(moveangle > 180) moveangle = 360 - moveangle; // restricted to between 0 and 180
-                       float strafe_ratio = 0;
-
-                       // player is overturning
-                       if(moveangle >= real_overturn_angle)
-                       {
-                               if(moveangle == real_overturn_angle && real_overturn_angle == 180)
+                       if(moveangle == absolute_overturnangle && absolute_overturnangle == 180)
                                        ; // everywhere gives acceleration, keep strafe_ratio as 0
-                               else
-                               {
+                       else
+                       {
                                        currentangle_color = autocvar_hud_panel_strafehud_angle_overturn_color;
-                                       strafe_ratio = (moveangle - real_overturn_angle) / (180 - real_overturn_angle);
-                                       // moveangle is always <= 180, so this code won't run if real_overturn_angle == 180
+                                       strafe_ratio = (moveangle - absolute_overturnangle) / (180 - absolute_overturnangle);
+                                       // moveangle is always <= 180, so this code won't run if absolute_overturnangle == 180
                                        strafe_ratio *= -1;
-                               }
                        }
-                       // player gains speed by strafing
-                       else if(moveangle >= real_bestangle)
-                       {
-                               currentangle_color = autocvar_hud_panel_strafehud_angle_accel_color;
-                               strafe_ratio = (real_overturn_angle - moveangle) / (real_overturn_angle - real_bestangle);
-                               // if real_overturn_angle == real_bestangle, this code won't run, no need to check if their difference is 0
-                       }
-                       else if(moveangle >= real_prebestangle)
-                       {
-                               if(autocvar_hud_panel_strafehud_bar_preaccel)
-                                       currentangle_color = autocvar_hud_panel_strafehud_angle_preaccel_color;
-                               strafe_ratio = (moveangle - real_prebestangle) / (real_bestangle - real_prebestangle);
-                       }
-
-                       if(autocvar_hud_panel_strafehud_style == STRAFEHUD_STYLE_GRADIENT)
-                               currentangle_color = StrafeHUD_mixColors(autocvar_hud_panel_strafehud_angle_neutral_color, currentangle_color, fabs(strafe_ratio));
-               }
-
-               if(mode == STRAFEHUD_MODE_VIEW_CENTERED || straight_overturn)
-                       currentangle_offset = panel_size.x / 2;
-
-               float angleheight_offset = currentangle_size.y;
-               float ghost_offset = 0;
-               bool draw_bestangle = autocvar_hud_panel_strafehud_bestangle && (autocvar_hud_panel_strafehud_bestangle == 1 || turn) && direction != STRAFEHUD_DIRECTION_NONE;
-               if(draw_bestangle)
-                       ghost_offset = bound(0, (odd_angles ? odd_bestangle_offset : bestangle_offset), panel_size.x);
-
-               switch(autocvar_hud_panel_strafehud_angle_style)
-               {
-                       case STRAFEHUD_INDICATOR_SOLID:
-                               if(currentangle_size.x > 0 && currentangle_size.y > 0)
-                               {
-                                       if(draw_bestangle)
-                                               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(draw_bestangle)
-                                                       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';
                }
-
-               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))
+               // player gains speed by strafing
+               else if(moveangle >= absolute_bestangle)
                {
-                       // offset text by amount the angle indicator extrudes from the strafehud bar
-                       angle_offset_top = angle_offset_bottom = (angleheight_offset - panel_size.y) / 2;
+                       currentangle_color = autocvar_hud_panel_strafehud_angle_accel_color;
+                       strafe_ratio = (absolute_overturnangle - moveangle) / (absolute_overturnangle - absolute_bestangle);
+                       // if absolute_overturnangle == absolute_bestangle, this code won't run, no need to check if their difference is 0
                }
-
-               if(autocvar_hud_panel_strafehud_angle_arrow > 0)
+               else if(moveangle >= absolute_prebestangle)
                {
-                       if(arrow_size > 0)
-                       {
-                               if(autocvar_hud_panel_strafehud_angle_arrow == 1 || autocvar_hud_panel_strafehud_angle_arrow >= 3)
-                               {
-                                       if(draw_bestangle)
-                                               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
-                               }
-                               if(autocvar_hud_panel_strafehud_angle_arrow >= 2)
-                               {
-                                       if(draw_bestangle)
-                                               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(autocvar_hud_panel_strafehud_bar_preaccel)
+                               currentangle_color = autocvar_hud_panel_strafehud_angle_preaccel_color;
+                       strafe_ratio = (moveangle - absolute_prebestangle) / (absolute_bestangle - absolute_prebestangle);
                }
 
-               // 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);
-
-               draw_beginBoldFont();
-
-               // show speed when crossing the start trigger
-               {
-                       static float startspeed = 0, starttime = 0; // displayed value and timestamp for fade out
+               if(StrafeHUD_IsGradient(autocvar_hud_panel_strafehud_style))
+                       currentangle_color = StrafeHUD_MixColors(
+                               autocvar_hud_panel_strafehud_angle_neutral_color,
+                               currentangle_color, fabs(strafe_ratio));
+       }
 
-                       // check if the start trigger was hit (will also trigger if the finish trigger was hit if those have the same ID)
-                       if((race_nextcheckpoint == 1) || (race_checkpoint == 254 && race_nextcheckpoint == 255))
-                       {
-                               if((race_checkpointtime > 0) && (starttime != race_checkpointtime))
-                               {
-                                       starttime = race_checkpointtime;
-                                       startspeed = race_timespeed;
-                               }
-                       }
+       float currentangle = 0;
+       if(mode == STRAFEHUD_MODE_VELOCITY_CENTERED)
+       {
+               // avoid switching side too much at ±180° if anti flicker is triggered
+               if(fabs(angle) <= 180 - antiflicker_angle)
+                       currentangle = angle;
+       }
 
-                       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_speed_unit));
-
-                               bool was_drawn = 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);
-
-                               if(was_drawn)
-                                       text_offset_bottom += startspeed_height;
-                       }
-               }
+       float max_line_height = 0;
+       float max_top_arrow_size = 0;
+       float max_bottom_arrow_size = 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)
-               {
-                       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
+       // only draw switch indicators if minspeed is reached
+       if(autocvar_hud_panel_strafehud_switch && autocvar_hud_panel_strafehud_switch_alpha > 0 && speed >= minspeed)
+       {
+               // change angle indicator style
+               vector indicator_size;
+               indicator_size.x = max(panel_size.x * min(autocvar_hud_panel_strafehud_switch_line_width, 10), 1);
+               indicator_size.y = max(panel_size.y * min(autocvar_hud_panel_strafehud_switch_line_height, 10), 1);
+               indicator_size.z = 0;
 
-                       // 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;
-                               }
-                       }
+               float num_dashes = rint(autocvar_hud_panel_strafehud_switch_line);
+               bool has_top_arrow = autocvar_hud_panel_strafehud_switch_arrow == 1 || autocvar_hud_panel_strafehud_switch_arrow >= 3;
+               bool has_bottom_arrow = autocvar_hud_panel_strafehud_switch_arrow >= 2;
 
-                       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_speed_unit));
-
-                               bool was_drawn = 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);
-
-                               if(was_drawn)
-                                       text_offset_top += jumpheight_height;
-                       }
+               // there's only one size cvar for the arrows, they will always have a 45° angle to ensure proper rendering without antialiasing
+               float arrow_size = max(panel_size.y * min(autocvar_hud_panel_strafehud_switch_arrow_size, 10), 1);
+
+               if(num_dashes > 0)
+                       max_line_height = max(max_line_height, indicator_size.y);
+               if(has_top_arrow)
+                       max_top_arrow_size = max(max_top_arrow_size, arrow_size);
+               if(has_bottom_arrow)
+                       max_bottom_arrow_size = max(max_bottom_arrow_size, arrow_size);
+
+               // draw the change indicator(s)
+               float current_changeangle = draw_normal
+                       ? (opposite_direction ? n_opposite_changeangle : n_changeangle)
+                       : (opposite_direction ? opposite_changeangle : changeangle);
+               float opposite_changeangle = draw_normal
+                       ? (opposite_direction ? n_opposite_bestangle : n_bestangle)
+                       : (opposite_direction ? opposite_bestangle : bestangle);
+
+               StrafeHUD_DrawAngleIndicator(
+                       current_changeangle, indicator_size, arrow_size, num_dashes,
+                       has_top_arrow, has_bottom_arrow, autocvar_hud_panel_strafehud_switch_color,
+                       autocvar_hud_panel_strafehud_switch_alpha, hudangle);
+
+               if(direction == STRAFEHUD_DIRECTION_NONE || draw_normal)
+               {
+                       StrafeHUD_DrawAngleIndicator(
+                               opposite_changeangle, indicator_size, arrow_size, num_dashes,
+                               has_top_arrow, has_bottom_arrow, autocvar_hud_panel_strafehud_switch_color,
+                               autocvar_hud_panel_strafehud_switch_alpha, hudangle);
                }
-
-               draw_endBoldFont();
        }
-       hud_lasttime = time;
-}
 
-// functions to make hud elements align perfectly in the hud area
-void HUD_Panel_DrawStrafeHUD(float offset, float width, float hidden_width, vector color, float alpha, int type, int gradientType, bool offset_centered)
-{
-       float mirror_offset, mirror_width;
-       vector size = panel_size;
-       vector mirror_size = panel_size;
-       float overflow_width = 0, overflow_mirror_width = 0;
-       float original_width = width; // required for gradient
+       if(autocvar_hud_panel_strafehud_bestangle && autocvar_hud_panel_strafehud_bestangle_alpha > 0 &&
+               (autocvar_hud_panel_strafehud_bestangle == 1 || turn) && direction != STRAFEHUD_DIRECTION_NONE)
+       {
+               // best angle indicator style
+               vector indicator_size;
+               indicator_size.x = max(panel_size.x * min(autocvar_hud_panel_strafehud_bestangle_line_width, 10), 1);
+               indicator_size.y = max(panel_size.y * min(autocvar_hud_panel_strafehud_bestangle_line_height, 10), 1);
+               indicator_size.z = 0;
 
-       if(offset_centered) // offset gives the center of the bar, not left edge
-               offset -= original_width / 2;
+               float num_dashes = rint(autocvar_hud_panel_strafehud_bestangle_line);
+               bool has_top_arrow = autocvar_hud_panel_strafehud_bestangle_arrow == 1 || autocvar_hud_panel_strafehud_bestangle_arrow >= 3;
+               bool has_bottom_arrow = autocvar_hud_panel_strafehud_bestangle_arrow >= 2;
 
-       if(type == STRAFEHUD_STYLE_GRADIENT && gradientType == STRAFEHUD_GRADIENT_NONE)
-               type = STRAFEHUD_STYLE_DRAWFILL;
+               // there's only one size cvar for the arrows, they will always have a 45° angle to ensure proper rendering without antialiasing
+               float arrow_size = max(panel_size.y * min(autocvar_hud_panel_strafehud_bestangle_arrow_size, 10), 1);
 
-       if(alpha <= 0 && type != STRAFEHUD_STYLE_GRADIENT || width <= 0)
-               return;
+               if(num_dashes > 0)
+                       max_line_height = max(max_line_height, indicator_size.y);
+               if(has_top_arrow)
+                       max_top_arrow_size = max(max_top_arrow_size, arrow_size);
+               if(has_bottom_arrow)
+                       max_bottom_arrow_size = max(max_bottom_arrow_size, arrow_size);
 
-       if(offset < 0)
-       {
-               mirror_width = min(fabs(offset), width);
-               mirror_offset = panel_size.x + hidden_width - fabs(offset);
-               width += offset;
-               offset = 0;
-       }
-       else
-       {
-               mirror_width = min(offset + width - panel_size.x - hidden_width, width);
-               mirror_offset = max(offset - panel_size.x - hidden_width, 0);
-       }
+               float ghostangle = opposite_direction ? opposite_bestangle : bestangle;
 
-       width = max(width, 0);
-       if((offset + width) > panel_size.x)
-       {
-               overflow_width = (offset + width) - panel_size.x;
-               width = panel_size.x - offset;
+               StrafeHUD_DrawAngleIndicator(
+                       ghostangle, indicator_size, arrow_size, num_dashes,
+                       has_top_arrow, has_bottom_arrow, autocvar_hud_panel_strafehud_bestangle_color,
+                       autocvar_hud_panel_strafehud_bestangle_alpha, hudangle);
        }
-       size.x = width;
 
-       if(mirror_offset < 0)
+       // only draw wturn indicators if conditions were met
+       if(wturn_valid && !onground && is_aircontrol_direction &&
+               autocvar_hud_panel_strafehud_wturn_alpha > 0 &&
+               absolute_wturn_bestangle < absolute_n_prebestangle &&
+               ((autocvar_hud_panel_strafehud_wturn && wturning) ||
+               (autocvar_hud_panel_strafehud_wturn == STRAFEHUD_WTURN_NORMAL && !turn) ||
+               (autocvar_hud_panel_strafehud_wturn == STRAFEHUD_WTURN_SIDESTRAFE)))
        {
-               mirror_width += mirror_offset;
-               mirror_offset = 0;
-       }
+               // wturn angle indicator style
+               vector indicator_size;
+               indicator_size.x = max(panel_size.x * min(autocvar_hud_panel_strafehud_wturn_line_width, 10), 1);
+               indicator_size.y = max(panel_size.y * min(autocvar_hud_panel_strafehud_wturn_line_height, 10), 1);
+               indicator_size.z = 0;
 
-       mirror_width = max(mirror_width, 0);
-       if((mirror_offset + mirror_width) > panel_size.x)
-       {
-               overflow_mirror_width = (mirror_offset + mirror_width) - panel_size.x;
-               mirror_width = panel_size.x - mirror_offset;
+               float num_dashes = rint(autocvar_hud_panel_strafehud_wturn_line);
+               bool has_top_arrow = autocvar_hud_panel_strafehud_wturn_arrow == 1 || autocvar_hud_panel_strafehud_wturn_arrow >= 3;
+               bool has_bottom_arrow = autocvar_hud_panel_strafehud_wturn_arrow >= 2;
+
+               // there's only one size cvar for the arrows, they will always have a 45° angle to ensure proper rendering without antialiasing
+               float arrow_size = max(panel_size.y * min(autocvar_hud_panel_strafehud_wturn_arrow_size, 10), 1);
+
+               if(num_dashes > 0)
+                       max_line_height = max(max_line_height, indicator_size.y);
+               if(has_top_arrow)
+                       max_top_arrow_size = max(max_top_arrow_size, arrow_size);
+               if(has_bottom_arrow)
+                       max_bottom_arrow_size = max(max_bottom_arrow_size, arrow_size);
+
+               // draw the wturn indicators
+               StrafeHUD_DrawAngleIndicator(
+                       wturn_left_bestangle, indicator_size, arrow_size, num_dashes,
+                       has_top_arrow, has_bottom_arrow, autocvar_hud_panel_strafehud_wturn_color,
+                       autocvar_hud_panel_strafehud_wturn_alpha, hudangle);
+               StrafeHUD_DrawAngleIndicator(
+                       wturn_right_bestangle, indicator_size, arrow_size, num_dashes,
+                       has_top_arrow, has_bottom_arrow, autocvar_hud_panel_strafehud_wturn_color,
+                       autocvar_hud_panel_strafehud_wturn_alpha, hudangle);
        }
-       mirror_size.x = mirror_width;
 
-       switch(type)
+       if(autocvar_hud_panel_strafehud_angle_alpha > 0)
        {
-               default:
-               case STRAFEHUD_STYLE_DRAWFILL: // no styling (drawfill)
-                       if(mirror_size.x > 0 && mirror_size.y > 0)
-                               drawfill(panel_pos + eX * mirror_offset, mirror_size, color, alpha, DRAWFLAG_NORMAL);
-                       if(size.x > 0 && size.y > 0)
-                               drawfill(panel_pos + eX * offset, size, color, alpha, DRAWFLAG_NORMAL);
-                       break;
-
-               case STRAFEHUD_STYLE_PROGRESSBAR: // progress bar style
-                       if(mirror_size.x > 0 && mirror_size.y > 0)
-                               HUD_Panel_DrawProgressBar(
-                                       panel_pos + eX * mirror_offset,
-                                       mirror_size, "progressbar",
-                                       1, 0, 0, color, alpha, DRAWFLAG_NORMAL);
-                       if(size.x > 0 && size.y > 0)
-                               HUD_Panel_DrawProgressBar(
-                                       panel_pos + eX * offset,
-                                       size, "progressbar",
-                                       1, 0, 0, color, alpha, DRAWFLAG_NORMAL);
-                       break;
-
-               case STRAFEHUD_STYLE_GRADIENT: // gradient style (types: 1 = left, 2 = right, 3 = both)
-                       // determine whether the gradient starts in the mirrored or the non-mirrored area
-                       int gradient_start;
-                       float gradient_offset, gradient_mirror_offset;
-
-                       if(offset == 0 && mirror_offset == 0)
-                               gradient_start = width > mirror_width ? 2 : 1;
-                       else if(offset == 0)
-                               gradient_start = 2;
-                       else if(mirror_offset == 0)
-                               gradient_start = 1;
-                       else
-                               gradient_start = 0;
-
-                       switch(gradient_start)
-                       {
-                               default:
-                               case 0: // no offset required
-                                       gradient_offset = gradient_mirror_offset = 0;
-                                       break;
-                               case 1: // offset starts in non-mirrored area, mirrored area requires offset
-                                       gradient_offset = 0;
-                                       gradient_mirror_offset = original_width - (mirror_width + overflow_mirror_width);
-                                       break;
-                               case 2: // offset starts in mirrored area, non-mirrored area requires offset
-                                       gradient_offset = original_width - (width + overflow_width);
-                                       gradient_mirror_offset = 0;
-                       }
+               // current angle indicator style
+               vector indicator_size;
+               indicator_size.x = max(panel_size.x * min(autocvar_hud_panel_strafehud_angle_line_width, 10), 1);
+               indicator_size.y = max(panel_size.y * min(autocvar_hud_panel_strafehud_angle_line_height, 10), 1);
+               indicator_size.z = 0;
 
-                       StrafeHUD_drawGradient(
-                               color, autocvar_hud_panel_strafehud_bar_neutral_color,
-                               mirror_size, original_width, mirror_offset,
-                               alpha, gradient_mirror_offset, gradientType);
+               float num_dashes = rint(autocvar_hud_panel_strafehud_angle_line);
+               bool has_top_arrow = autocvar_hud_panel_strafehud_angle_arrow == 1 || autocvar_hud_panel_strafehud_angle_arrow >= 3;
+               bool has_bottom_arrow = autocvar_hud_panel_strafehud_angle_arrow >= 2;
 
-                       StrafeHUD_drawGradient(
-                               color, autocvar_hud_panel_strafehud_bar_neutral_color,
-                               size, original_width, offset,
-                               alpha, gradient_offset, gradientType);
+               // there's only one size cvar for the arrows, they will always have a 45° angle to ensure proper rendering without antialiasing
+               float arrow_size = max(panel_size.y * min(autocvar_hud_panel_strafehud_angle_arrow_size, 10), 1);
+
+               if(num_dashes > 0)
+                       max_line_height = max(max_line_height, indicator_size.y);
+               if(has_top_arrow)
+                       max_top_arrow_size = max(max_top_arrow_size, arrow_size);
+               if(has_bottom_arrow)
+                       max_bottom_arrow_size = max(max_bottom_arrow_size, arrow_size);
+
+               StrafeHUD_DrawAngleIndicator(
+                       currentangle, indicator_size, arrow_size, num_dashes,
+                       has_top_arrow, has_bottom_arrow, currentangle_color,
+                       autocvar_hud_panel_strafehud_angle_alpha, hudangle);
        }
-}
-
-vector StrafeHUD_mixColors(vector color1, vector color2, float ratio)
-{
-       vector mixedColor;
-       if(ratio <= 0) return color1;
-       if(ratio >= 1) return color2;
-       mixedColor.x = color1.x + (color2.x - color1.x) * ratio;
-       mixedColor.y = color1.y + (color2.y - color1.y) * ratio;
-       mixedColor.z = color1.z + (color2.z - color1.z) * ratio;
-       return mixedColor;
-}
 
-void StrafeHUD_drawGradient(vector color1, vector color2, vector size, float original_width, float offset, float alpha, float gradientOffset, int gradientType)
-{
-       float color_ratio, alpha1, alpha2;
-       vector segment_size = size;
-       alpha1 = bound(0, alpha, 1);
-       alpha2 = bound(0, autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha, 1);
-       if((alpha1 + alpha2) == 0) return;
-       color_ratio = alpha1 / (alpha1 + alpha2);
-       for(int i = 0; i < size.x; ++i)
+       // offset text by amount the angle indicator extrudes from the strafehud bar
        {
-               float ratio, alpha_ratio, combine_ratio1, combine_ratio2;
-               segment_size.x = min(size.x - i, 1); // each gradient segment is 1 unit wide except if there is less than 1 unit of gradient remaining
-               ratio = (i + segment_size.x / 2 + gradientOffset) / original_width * (gradientType == STRAFEHUD_GRADIENT_BOTH ? 2 : 1);
-               if(ratio > 1) ratio = 2 - ratio;
-               if(gradientType != STRAFEHUD_GRADIENT_RIGHT) ratio = 1 - ratio;
-               alpha_ratio = alpha1 - (alpha1 - alpha2) * ratio;
-               combine_ratio1 = ratio * (1 - color_ratio);
-               combine_ratio2 = (1 - ratio) * color_ratio;
-               ratio = (combine_ratio1 + combine_ratio2) == 0 ? 1 : combine_ratio1 / (combine_ratio1 + combine_ratio2);
-
-               if(alpha_ratio > 0)
-                       drawfill(
-                               panel_pos + eX * (offset + i),
-                               segment_size,
-                               StrafeHUD_mixColors(color1, color2, ratio),
-                               alpha_ratio,
-                               DRAWFLAG_NORMAL);
-       }
-}
+               float line_height_offset = max_line_height;
 
-// draw the strafe arrows (inspired by drawspritearrow() in common/mutators/mutator/waypoints/waypointsprites.qc)
-void StrafeHUD_drawStrafeArrow(vector origin, float size, vector color, float alpha, bool flipped, float connection_width)
-{
-       origin = HUD_Shift(origin);
-       float width = HUD_ScaleX(size * 2 + connection_width);
-       float height = HUD_ScaleY(size);
-       if(flipped) origin -= size * eY;
-       R_BeginPolygon("", DRAWFLAG_NORMAL, true);
-       if(connection_width > 0)
-       {
-               R_PolygonVertex(origin + (connection_width / 2 * eX) + (flipped ? height * eY : '0 0 0'), '0 0 0', color, alpha);
-               R_PolygonVertex(origin - (connection_width / 2 * eX) + (flipped ? height * eY : '0 0 0'), '0 0 0', color, alpha);
-       }
-       else
-       {
-               R_PolygonVertex(origin + (flipped ? height * eY : '0 0 0'), '0 0 0', color, alpha);
-       }
-       R_PolygonVertex(origin + (flipped ? '0 0 0' : height * eY) - (width / 2) * eX, '0 0 0', color, alpha);
-       R_PolygonVertex(origin + (flipped ? '0 0 0' : height * eY) + (width / 2) * eX, '0 0 0', color, alpha);
-       R_EndPolygon();
-}
+               // amount line extrudes from the strafehud bar
+               line_height_offset = (line_height_offset - panel_size.y) / 2;
 
-// draw a fading text indicator above or below the strafe meter, return true if something was displayed
-bool StrafeHUD_drawTextIndicator(string text, float height, vector color, float fadetime, float lasttime, float offset, int position)
-{
-       if((height <= 0) || (lasttime <= 0) || (fadetime <= 0) || ((time - lasttime) >= fadetime))
-               return false;
+               // further offset the top text offset if the top arrow is drawn
+               float angle_offset_top;
+               angle_offset_top = line_height_offset + max_top_arrow_size;
 
-       float alpha = cos(((time - lasttime) / fadetime) * M_PI_2); // fade non-linear like the physics panel does
-       vector size = panel_size;
-       size.y = height;
+               // further offset the bottom text offset if the bottom arrow is drawn
+               float angle_offset_bottom;
+               angle_offset_bottom = line_height_offset + max_bottom_arrow_size;
 
-       switch(position)
-       {
-               case STRAFEHUD_TEXT_TOP:
-                       offset += size.y;
-                       offset *= -1;
-                       break;
-               case STRAFEHUD_TEXT_BOTTOM:
-                       offset += panel_size.y;
-                       break;
+               // make sure text does not 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);
        }
 
-       drawstring_aspect(panel_pos + eY * offset, text, size, color, alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
-       return true;
-}
+       StrafeHUD_DrawVerticalAngle(strafeplayer, text_offset_top, text_offset_bottom);
 
-// length unit conversion (km and miles are only included to match the GetSpeedUnit* functions)
-float GetLengthUnitFactor(int length_unit)
-{
-       switch(length_unit)
-       {
-               default:
-               case 1: return 1.0;
-               case 2: return 0.0254;
-               case 3: return 0.0254 * 0.001;
-               case 4: return 0.0254 * 0.001 * 0.6213711922;
-               case 5: return 0.0254 * 0.001 * 0.5399568035;
-       }
-}
+       draw_beginBoldFont();
+       StrafeHUD_DrawStartSpeed(race_timespeed, text_offset_top, text_offset_bottom);
+       StrafeHUD_DrawStrafeEfficiency(strafe_ratio, text_offset_top, text_offset_bottom);
+       StrafeHUD_DrawJumpHeight(strafeplayer, real_onground, swimming, text_offset_top, text_offset_bottom);
+       draw_endBoldFont();
 
-string GetLengthUnit(int length_unit)
-{
-       switch(length_unit)
-       {
-               // translator-friendly strings without the initial space
-               default:
-               case 1: return strcat(" ", _("qu"));
-               case 2: return strcat(" ", _("m"));
-               case 3: return strcat(" ", _("km"));
-               case 4: return strcat(" ", _("mi"));
-               case 5: return strcat(" ", _("nmi"));
-       }
+       StrafeHUD_Sonar(strafe_ratio, StrafeHUD_UpdateSonarSound());
+
+       hud_lasttime = time;
 }
index 398ab5597c3d8e50d2d03a64b053f853bf0e49eb..a84d8abf5f9eb5a9341f4e65739c5710e84bfaeb 100644 (file)
@@ -1,17 +1,23 @@
 #pragma once
 #include "../panel.qh"
+#include <client/hud/panel/strafehud/_mod.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 = 90;
-float autocvar_hud_panel_strafehud_range_sidestrafe = -1;
+float autocvar_hud_panel_strafehud_range_sidestrafe = -2;
 int autocvar_hud_panel_strafehud_style = 2;
 bool autocvar_hud_panel_strafehud_unit_show = true;
-bool autocvar_hud_panel_strafehud_uncapped = false;
 int autocvar_hud_panel_strafehud_onground_mode = 2;
 bool autocvar_hud_panel_strafehud_onground_friction = true;
+
+float autocvar_hud_panel_strafehud_timeout_ground = 0.1;
+float autocvar_hud_panel_strafehud_timeout_turn = 0.1;
+float autocvar_hud_panel_strafehud_antiflicker_angle = 0.01;
+float autocvar_hud_panel_strafehud_fps_update = 0.5;
+
 bool autocvar_hud_panel_strafehud_bar_preaccel = true;
 vector autocvar_hud_panel_strafehud_bar_preaccel_color = '0 1 0';
 float autocvar_hud_panel_strafehud_bar_preaccel_alpha = 0.5;
@@ -21,63 +27,90 @@ vector autocvar_hud_panel_strafehud_bar_accel_color = '0 1 0';
 float autocvar_hud_panel_strafehud_bar_accel_alpha = 0.5;
 vector autocvar_hud_panel_strafehud_bar_overturn_color = '1 0 1';
 float autocvar_hud_panel_strafehud_bar_overturn_alpha = 0.5;
-int autocvar_hud_panel_strafehud_angle_style = 0;
-int autocvar_hud_panel_strafehud_angle_dashes = 4;
+
 float autocvar_hud_panel_strafehud_angle_alpha = 0.8;
-float autocvar_hud_panel_strafehud_angle_height = 1;
-float autocvar_hud_panel_strafehud_angle_width = 0.001;
 vector autocvar_hud_panel_strafehud_angle_preaccel_color = '0 1 1';
 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';
+int autocvar_hud_panel_strafehud_angle_line = 0;
+float autocvar_hud_panel_strafehud_angle_line_width = 0.001;
+float autocvar_hud_panel_strafehud_angle_line_height = 1;
 int autocvar_hud_panel_strafehud_angle_arrow = 1;
 float autocvar_hud_panel_strafehud_angle_arrow_size = 0.5;
 int autocvar_hud_panel_strafehud_bestangle = 1;
 vector autocvar_hud_panel_strafehud_bestangle_color = '1 1 1';
 float autocvar_hud_panel_strafehud_bestangle_alpha = 0.5;
+int autocvar_hud_panel_strafehud_bestangle_line = 0;
+float autocvar_hud_panel_strafehud_bestangle_line_width = 0.001;
+float autocvar_hud_panel_strafehud_bestangle_line_height = 1;
+int autocvar_hud_panel_strafehud_bestangle_arrow = 1;
+float autocvar_hud_panel_strafehud_bestangle_arrow_size = 0.5;
 int autocvar_hud_panel_strafehud_switch = 1;
 float autocvar_hud_panel_strafehud_switch_minspeed = -1;
 vector autocvar_hud_panel_strafehud_switch_color = '1 1 0';
-float autocvar_hud_panel_strafehud_switch_alpha = 1;
-float autocvar_hud_panel_strafehud_switch_width = 0.003;
+float autocvar_hud_panel_strafehud_switch_alpha = 0.5;
+int autocvar_hud_panel_strafehud_switch_line = 0;
+float autocvar_hud_panel_strafehud_switch_line_width = 0.001;
+float autocvar_hud_panel_strafehud_switch_line_height = 1;
+int autocvar_hud_panel_strafehud_switch_arrow = 1;
+float autocvar_hud_panel_strafehud_switch_arrow_size = 0.5;
 int autocvar_hud_panel_strafehud_wturn = 1;
-vector autocvar_hud_panel_strafehud_wturn_color = '0 1 1';
-float autocvar_hud_panel_strafehud_wturn_alpha = 1;
-float autocvar_hud_panel_strafehud_wturn_width = 0.003;
+vector autocvar_hud_panel_strafehud_wturn_color = '0 0 1';
+float autocvar_hud_panel_strafehud_wturn_alpha = 0.5;
 bool autocvar_hud_panel_strafehud_wturn_proper = false;
 bool autocvar_hud_panel_strafehud_wturn_unrestricted = false;
+int autocvar_hud_panel_strafehud_wturn_line = 0;
+float autocvar_hud_panel_strafehud_wturn_line_width = 0.001;
+float autocvar_hud_panel_strafehud_wturn_line_height = 1;
+int autocvar_hud_panel_strafehud_wturn_arrow = 1;
+float autocvar_hud_panel_strafehud_wturn_arrow_size = 0.5;
+
 bool autocvar_hud_panel_strafehud_direction = false;
 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;
+
 bool autocvar_hud_panel_strafehud_slickdetector = true;
 float autocvar_hud_panel_strafehud_slickdetector_range = 200;
 int autocvar_hud_panel_strafehud_slickdetector_granularity = 1;
 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;
+
 bool autocvar_hud_panel_strafehud_startspeed = true;
 float autocvar_hud_panel_strafehud_startspeed_fade = 4;
 vector autocvar_hud_panel_strafehud_startspeed_color = '1 0.75 0';
+vector autocvar_hud_panel_strafehud_startspeed_pos = '0 -1 0';
 float autocvar_hud_panel_strafehud_startspeed_size = 1.5;
 bool autocvar_hud_panel_strafehud_jumpheight = false;
 float autocvar_hud_panel_strafehud_jumpheight_fade = 4;
 float autocvar_hud_panel_strafehud_jumpheight_min = 50;
 vector autocvar_hud_panel_strafehud_jumpheight_color = '0 1 0.75';
+vector autocvar_hud_panel_strafehud_jumpheight_pos = '0 -2 0';
 float autocvar_hud_panel_strafehud_jumpheight_size = 1.5;
-float autocvar_hud_panel_strafehud_timeout_ground = 0.1;
-float autocvar_hud_panel_strafehud_timeout_turn = 0.1;
-float autocvar_hud_panel_strafehud_antiflicker_angle = 0.01;
-float autocvar_hud_panel_strafehud_fps_update = 0.5;
+bool autocvar_hud_panel_strafehud_vangle = false;
+vector autocvar_hud_panel_strafehud_vangle_color = '0.75 0.75 0.75';
+vector autocvar_hud_panel_strafehud_vangle_pos = '-0.25 1 0';
+float autocvar_hud_panel_strafehud_vangle_size = 1;
+bool autocvar_hud_panel_strafehud_strafeefficiency = false;
+vector autocvar_hud_panel_strafehud_strafeefficiency_pos = '0.25 1 0';
+float autocvar_hud_panel_strafehud_strafeefficiency_size = 1;
+int autocvar_hud_panel_strafehud_projection = 0;
 
-void HUD_Panel_DrawStrafeHUD(float, float, float, vector, float, int, int, bool);
-vector StrafeHUD_mixColors(vector, vector, float);
-void StrafeHUD_drawGradient(vector, vector, vector, float, float, float, float, int);
-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);
+bool autocvar_hud_panel_strafehud_sonar = false;
+string autocvar_hud_panel_strafehud_sonar_audio = "misc/talk";
+float autocvar_hud_panel_strafehud_sonar_start = 0.5;
+float autocvar_hud_panel_strafehud_sonar_interval_start = 0.333333;
+float autocvar_hud_panel_strafehud_sonar_interval_range = -0.222222;
+float autocvar_hud_panel_strafehud_sonar_interval_exponent = 1;
+float autocvar_hud_panel_strafehud_sonar_volume_start = 0.333333;
+float autocvar_hud_panel_strafehud_sonar_volume_range = 0.666666;
+float autocvar_hud_panel_strafehud_sonar_volume_exponent = 1;
+float autocvar_hud_panel_strafehud_sonar_pitch_start = 0.9;
+float autocvar_hud_panel_strafehud_sonar_pitch_range = 0.1;
+float autocvar_hud_panel_strafehud_sonar_pitch_exponent = 1;
 
 const int STRAFEHUD_MODE_VIEW_CENTERED = 0;
 const int STRAFEHUD_MODE_VELOCITY_CENTERED = 1;
@@ -107,17 +140,17 @@ 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_STYLE_SOFT_GRADIENT = 3;
 
 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;
 
 const float ACOS_SQRT2_3_DEG = 35.2643896827546543153;  /* acos(sqrt(2/3)) * RAD2DEG */
+
+float GeomLerp(float a, float _lerp, float b); // declare GeomLerp here since there's no header file for it
diff --git a/qcsrc/client/hud/panel/strafehud/_mod.inc b/qcsrc/client/hud/panel/strafehud/_mod.inc
new file mode 100644 (file)
index 0000000..56ddc7b
--- /dev/null
@@ -0,0 +1,5 @@
+// genmod.sh autogenerated file; do not modify
+#include <client/hud/panel/strafehud/draw.qc>
+#include <client/hud/panel/strafehud/draw_core.qc>
+#include <client/hud/panel/strafehud/extra.qc>
+#include <client/hud/panel/strafehud/util.qc>
diff --git a/qcsrc/client/hud/panel/strafehud/_mod.qh b/qcsrc/client/hud/panel/strafehud/_mod.qh
new file mode 100644 (file)
index 0000000..0a656e0
--- /dev/null
@@ -0,0 +1,5 @@
+// genmod.sh autogenerated file; do not modify
+#include <client/hud/panel/strafehud/draw.qh>
+#include <client/hud/panel/strafehud/draw_core.qh>
+#include <client/hud/panel/strafehud/extra.qh>
+#include <client/hud/panel/strafehud/util.qh>
diff --git a/qcsrc/client/hud/panel/strafehud/draw.qc b/qcsrc/client/hud/panel/strafehud/draw.qc
new file mode 100644 (file)
index 0000000..51f92f8
--- /dev/null
@@ -0,0 +1,336 @@
+#include "draw.qh"
+
+#include <client/draw.qh>
+
+// draw the strafe-o-meter bar
+// aligns HUD elements perfectly in the hud area
+// also deals with wrapping around on edges, different HUD styles, etc.
+void StrafeHUD_DrawStrafeHUD(float startangle, float offsetangle, vector color, float alpha, int type, int gradient_type, float range)
+{
+       float offset = StrafeHUD_AngleToOffset(startangle % 360, range);
+       float width = StrafeHUD_AngleToWidth(offsetangle, range);
+       float mirror_offset;
+       float mirror_width;
+
+       if(width <= 0) return;
+
+       if(StrafeHUD_IsGradient(type))
+       {
+               if(gradient_type == STRAFEHUD_GRADIENT_NONE)
+               {
+                       type = STRAFEHUD_STYLE_DRAWFILL;
+                       if(alpha <= 0) return;
+               }
+       }
+       else if(alpha <= 0) return;
+
+       // how much is hidden by the current hud angle
+       float hidden_width = (360 - range) / range * panel_size.x;
+       float total_width = panel_size.x + hidden_width;
+       float original_width = width; // required for gradient
+
+       if(offset < 0)
+       {
+               mirror_width = min(fabs(offset), width);
+               mirror_offset = offset + total_width;
+               width += offset;
+               offset = 0;
+       }
+       else
+       {
+               mirror_offset = offset - total_width;
+               mirror_width = min(mirror_offset + width, width);
+               if(mirror_offset < 0) mirror_offset = 0;
+       }
+
+       float overflow_width = offset + width - panel_size.x;
+       width = max(width, 0);
+       if(overflow_width > 0)
+               width = panel_size.x - offset;
+       else
+               overflow_width = 0;
+
+       vector size = panel_size;
+       size.x = width;
+
+       float original_offset = offset;
+
+       // the accelerated gradient does the projection later
+       if(type != STRAFEHUD_STYLE_GRADIENT)
+       {
+               if(size.x > 0)
+                       size.x = StrafeHUD_ProjectWidth(offset, size.x, range);
+
+               offset = StrafeHUD_ProjectOffset(offset, range, false);
+       }
+
+       if(mirror_offset < 0)
+       {
+               mirror_width += mirror_offset;
+               mirror_offset = 0;
+       }
+
+       float overflow_mirror_width = mirror_offset + mirror_width - panel_size.x;
+       mirror_width = max(mirror_width, 0);
+       if(overflow_mirror_width > 0)
+               mirror_width = panel_size.x - mirror_offset;
+       else
+               overflow_mirror_width = 0;
+
+       vector mirror_size = panel_size;
+       mirror_size.x = mirror_width;
+
+       float original_mirror_offset = mirror_offset;
+
+       // the accelerated gradient does the projection later
+       if(type != STRAFEHUD_STYLE_GRADIENT)
+       {
+               if(mirror_size.x > 0)
+                       mirror_size.x = StrafeHUD_ProjectWidth(mirror_offset, mirror_size.x, range);
+
+               mirror_offset = StrafeHUD_ProjectOffset(mirror_offset, range, false);
+       }
+
+       switch(type)
+       {
+               default:
+               case STRAFEHUD_STYLE_DRAWFILL: // no styling (drawfill)
+                       if(mirror_size.x > 0 && mirror_size.y > 0)
+                               drawfill(panel_pos + eX * mirror_offset, mirror_size, color, alpha, DRAWFLAG_NORMAL);
+                       if(size.x > 0 && size.y > 0)
+                               drawfill(panel_pos + eX * offset, size, color, alpha, DRAWFLAG_NORMAL);
+                       break;
+
+               case STRAFEHUD_STYLE_PROGRESSBAR: // progress bar style
+                       if(mirror_size.x > 0 && mirror_size.y > 0)
+                               HUD_Panel_DrawProgressBar(
+                                       panel_pos + eX * mirror_offset,
+                                       mirror_size, "progressbar",
+                                       1, 0, 0, color, alpha, DRAWFLAG_NORMAL);
+                       if(size.x > 0 && size.y > 0)
+                               HUD_Panel_DrawProgressBar(
+                                       panel_pos + eX * offset,
+                                       size, "progressbar",
+                                       1, 0, 0, color, alpha, DRAWFLAG_NORMAL);
+                       break;
+
+               case STRAFEHUD_STYLE_GRADIENT: // gradient style (types: 1 = left, 2 = right, 3 = both)
+               case STRAFEHUD_STYLE_SOFT_GRADIENT:
+                       // determine whether the gradient starts in the mirrored or the non-mirrored area
+                       int gradient_start;
+                       float gradient_offset, gradient_mirror_offset;
+
+                       if(offset == 0 && mirror_offset == 0)
+                               gradient_start = width > mirror_width ? 2 : 1;
+                       else if(offset == 0)
+                               gradient_start = 2;
+                       else if(mirror_offset == 0)
+                               gradient_start = 1;
+                       else
+                               gradient_start = 0;
+
+                       switch(gradient_start)
+                       {
+                               default:
+                               case 0: // no offset required
+                                       gradient_offset = gradient_mirror_offset = 0;
+                                       break;
+                               case 1: // offset starts in non-mirrored area, mirrored area requires offset
+                                       gradient_offset = 0;
+                                       gradient_mirror_offset = original_width - (mirror_width + overflow_mirror_width);
+                                       break;
+                               case 2: // offset starts in mirrored area, non-mirrored area requires offset
+                                       gradient_offset = original_width - (width + overflow_width);
+                                       gradient_mirror_offset = 0;
+                       }
+
+                       if(type == STRAFEHUD_STYLE_GRADIENT)
+                       {
+                               if(mirror_size.x > 0)
+                                       StrafeHUD_DrawGradient(
+                                               color, autocvar_hud_panel_strafehud_bar_neutral_color,
+                                               mirror_size, original_width, mirror_offset, alpha,
+                                               gradient_mirror_offset, gradient_type, range);
+
+                               if(size.x > 0)
+                                       StrafeHUD_DrawGradient(
+                                               color, autocvar_hud_panel_strafehud_bar_neutral_color,
+                                               size, original_width, offset, alpha,
+                                               gradient_offset, gradient_type, range);
+                       }
+                       else
+                       {
+                               if(mirror_size.x > 0)
+                                       StrafeHUD_DrawSoftGradient(
+                                               color, autocvar_hud_panel_strafehud_bar_neutral_color,
+                                               mirror_size, original_width, mirror_offset, original_mirror_offset,
+                                               alpha, gradient_mirror_offset, gradient_type, range);
+
+                               if(size.x > 0)
+                                       StrafeHUD_DrawSoftGradient(
+                                               color, autocvar_hud_panel_strafehud_bar_neutral_color,
+                                               size, original_width, offset, original_offset,
+                                               alpha, gradient_offset, gradient_type, range);
+                       }
+       }
+}
+
+// accelerated gradient, does not support non-linear projection of the color and opacity within individual segments
+void StrafeHUD_DrawGradient(
+       vector color1, vector color2, vector size, float original_width,
+       float offset, float alpha, float gradient_offset, int gradient_type, float range)
+{
+       if(gradient_type == STRAFEHUD_GRADIENT_BOTH)
+       {
+               original_width /= 2;
+
+               vector size1 = size;
+               size1.x = bound(0, original_width - gradient_offset, size.x);
+
+               vector size2 = size;
+               size2.x = size.x - size1.x;
+
+               if(size1.x > 0)
+                       StrafeHUD_DrawGradient(color1, color2, size1, original_width, offset, alpha, gradient_offset, STRAFEHUD_GRADIENT_LEFT, range);
+
+               if(size2.x > 0)
+                       StrafeHUD_DrawGradient(color1, color2, size2, original_width, offset + size1.x, alpha, max(0, gradient_offset - original_width), STRAFEHUD_GRADIENT_RIGHT, range);
+
+               return;
+       }
+
+       vector gradient_start = eX * offset;
+       float gradient_width = StrafeHUD_ProjectWidth(gradient_start.x, size.x, range);
+       gradient_start.x = StrafeHUD_ProjectOffset(gradient_start.x, range, false);
+       vector gradient_end = gradient_start + eX * gradient_width;
+       vector gradient_height = eY * size.y;
+
+       float alpha1 = bound(0, alpha, 1);
+       float alpha2 = bound(0, autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha, 1);
+       if(alpha1 + alpha2 == 0) return;
+
+       float ratio1 = gradient_offset / original_width;
+       float ratio2 = (gradient_offset + size.x) / original_width;
+       if(gradient_type == STRAFEHUD_GRADIENT_LEFT)
+       {
+               ratio1 = 1 - ratio1;
+               ratio2 = 1 - ratio2;
+       }
+
+       vector origin = HUD_Shift(panel_pos);
+       gradient_start.x = HUD_ScaleX(gradient_start.x);
+       gradient_end.x = HUD_ScaleX(gradient_end.x);
+       gradient_height.y = HUD_ScaleY(gradient_height.y);
+
+       R_BeginPolygon("", DRAWFLAG_NORMAL, true);
+       R_PolygonVertex(origin + gradient_start,                   '0 0 0', color1, alpha1 * (1 - ratio1));
+       R_PolygonVertex(origin + gradient_start + gradient_height, '0 0 0', color1, alpha1 * (1 - ratio1));
+       R_PolygonVertex(origin + gradient_end   + gradient_height, '0 0 0', color1, alpha1 * (1 - ratio2));
+       R_PolygonVertex(origin + gradient_end,                     '0 0 0', color1, alpha1 * (1 - ratio2));
+       R_EndPolygon();
+
+       R_BeginPolygon("", DRAWFLAG_NORMAL, true);
+       R_PolygonVertex(origin + gradient_start,                   '0 0 0', color2, alpha2 * ratio1);
+       R_PolygonVertex(origin + gradient_start + gradient_height, '0 0 0', color2, alpha2 * ratio1);
+       R_PolygonVertex(origin + gradient_end   + gradient_height, '0 0 0', color2, alpha2 * ratio2);
+       R_PolygonVertex(origin + gradient_end,                     '0 0 0', color2, alpha2 * ratio2);
+       R_EndPolygon();
+}
+
+// more expensive gradient rendering which does not rely on vertex gradients (required to properly render the color/opacity of individual segments in non-linear projection modes)
+void StrafeHUD_DrawSoftGradient(
+       vector color1, vector color2, vector size, float original_width, float offset, float original_offset,
+       float alpha, float gradient_offset, int gradient_type, float range)
+{
+       float alpha1 = bound(0, alpha, 1);
+       float alpha2 = bound(0, autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha, 1);
+       if(alpha1 + alpha2 == 0) return;
+
+       float color_ratio = alpha1 / (alpha1 + alpha2);
+       vector segment_size = size;
+       for(int i = 0; i < size.x; ++i)
+       {
+               segment_size.x = min(size.x - i, 1); // each gradient segment is 1 unit wide except if there is less than 1 unit of gradient remaining
+               float segment_offset = offset + i;
+               float ratio_offset = segment_offset + segment_size.x / 2;
+               ratio_offset = StrafeHUD_ProjectOffset(ratio_offset, range, true);
+               ratio_offset += gradient_offset;
+               float ratio = (ratio_offset - original_offset) / original_width * (gradient_type == STRAFEHUD_GRADIENT_BOTH ? 2 : 1);
+               if(ratio > 1) ratio = 2 - ratio;
+               if(gradient_type != STRAFEHUD_GRADIENT_RIGHT) ratio = 1 - ratio;
+               float alpha_ratio = alpha1 - (alpha1 - alpha2) * ratio;
+               float combine_ratio1 = ratio * (1 - color_ratio);
+               float combine_ratio2 = (1 - ratio) * color_ratio;
+               ratio = (combine_ratio1 + combine_ratio2) == 0 ? 1 : combine_ratio1 / (combine_ratio1 + combine_ratio2);
+
+               if(alpha_ratio > 0)
+                       drawfill(
+                               panel_pos + eX * segment_offset,
+                               segment_size,
+                               StrafeHUD_MixColors(color1, color2, ratio),
+                               alpha_ratio,
+                               DRAWFLAG_NORMAL);
+       }
+}
+
+// draw the strafe arrows (inspired by drawspritearrow() in common/mutators/mutator/waypoints/waypointsprites.qc)
+void StrafeHUD_DrawStrafeArrow(vector origin, float size, vector color, float alpha, bool flipped, float connection_width)
+{
+       // alpha and size already checked
+
+       origin = HUD_Shift(origin);
+       float width  = HUD_ScaleX(size * 2 + connection_width);
+       float height = HUD_ScaleY(size);
+       if(flipped) origin.y -= size;
+       R_BeginPolygon("", DRAWFLAG_NORMAL, true);
+       if(connection_width > 0)
+       {
+               R_PolygonVertex(origin + (connection_width / 2 * eX) + (flipped ? height * eY : '0 0 0'), '0 0 0', color, alpha);
+               R_PolygonVertex(origin - (connection_width / 2 * eX) + (flipped ? height * eY : '0 0 0'), '0 0 0', color, alpha);
+       }
+       else
+       {
+               R_PolygonVertex(origin + (flipped ? height * eY : '0 0 0'), '0 0 0', color, alpha);
+       }
+       R_PolygonVertex(origin + (flipped ? '0 0 0' : height * eY) - (width / 2) * eX, '0 0 0', color, alpha);
+       R_PolygonVertex(origin + (flipped ? '0 0 0' : height * eY) + (width / 2) * eX, '0 0 0', color, alpha);
+       R_EndPolygon();
+}
+
+// draw a fading text indicator above or below the strafe meter
+void StrafeHUD_DrawTextIndicator(
+       string text, float height, vector color, float fadetime, float lasttime,
+       vector pos, float offset_top, float offset_bottom)
+{
+       float time_frac = (time - lasttime) / fadetime;
+       if(height <= 0 || lasttime <= 0 || fadetime <= 0 || time_frac > 1)
+               return;
+
+       float alpha = cos(time_frac * M_PI_2); // fade non-linear like the physics panel does
+       vector size = panel_size;
+       size.y = height;
+
+       if(pos.y >= 1)
+       {
+               --pos.y; // for calculations the position should not start at +1
+               pos = StrafeHUD_CalculateTextIndicatorPosition(pos);
+               pos.y += size.y + offset_top;
+               pos.y *= -1; // it's more intuitive for up to be positive
+       }
+       else if(pos.y <= -1)
+       {
+               ++pos.y; // for calculations the position should not start at -1
+               pos = StrafeHUD_CalculateTextIndicatorPosition(pos);
+               pos.y *= -1; // it's more intuitive for down to be negative
+               pos.y += panel_size.y + offset_bottom;
+       }
+       else return;
+
+       drawstring_aspect(panel_pos + pos, text, size, color, alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+}
+
+// checks whether the current style is a gradient style
+bool StrafeHUD_IsGradient(int style)
+{
+       return style == STRAFEHUD_STYLE_GRADIENT || style == STRAFEHUD_STYLE_SOFT_GRADIENT;
+}
diff --git a/qcsrc/client/hud/panel/strafehud/draw.qh b/qcsrc/client/hud/panel/strafehud/draw.qh
new file mode 100644 (file)
index 0000000..1bab2ea
--- /dev/null
@@ -0,0 +1,9 @@
+#pragma once
+#include "../strafehud.qh"
+
+void StrafeHUD_DrawStrafeHUD(float, float, vector, float, int, int, float);
+void StrafeHUD_DrawGradient(vector, vector, vector, float, float, float, float, int, float);
+void StrafeHUD_DrawSoftGradient(vector, vector, vector, float, float, float, float, float, int, float);
+void StrafeHUD_DrawStrafeArrow(vector, float, vector, float, bool, float);
+void StrafeHUD_DrawTextIndicator(string, float, vector, float, float, vector, float, float);
+bool StrafeHUD_IsGradient(int);
diff --git a/qcsrc/client/hud/panel/strafehud/draw_core.qc b/qcsrc/client/hud/panel/strafehud/draw_core.qc
new file mode 100644 (file)
index 0000000..707f211
--- /dev/null
@@ -0,0 +1,248 @@
+#include "draw_core.qh"
+
+#include <client/draw.qh>
+
+void StrafeHUD_DrawStrafeMeter(
+       float shiftangle, float wishangle, float absolute_bestangle,
+       float absolute_prebestangle, float absolute_overturnangle,
+       bool moving, float hudangle)
+{
+       // the neutral zone fills the whole strafe bar
+       if(!moving)
+       {
+               // draw neutral zone
+               if(panel_size.x > 0 && panel_size.y > 0 && autocvar_hud_panel_strafehud_bar_neutral_alpha > 0)
+               {
+                       switch(autocvar_hud_panel_strafehud_style)
+                       {
+                               default:
+                               case STRAFEHUD_STYLE_DRAWFILL:
+                                       drawfill(
+                                               panel_pos, panel_size,
+                                               autocvar_hud_panel_strafehud_bar_neutral_color,
+                                               autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha,
+                                               DRAWFLAG_NORMAL);
+                                       break;
+
+                               case STRAFEHUD_STYLE_PROGRESSBAR:
+                                       HUD_Panel_DrawProgressBar(
+                                               panel_pos, panel_size, "progressbar", 1, 0, 0,
+                                               autocvar_hud_panel_strafehud_bar_neutral_color,
+                                               autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha,
+                                               DRAWFLAG_NORMAL);
+                       }
+               }
+       }
+       else
+       {
+               // calculate various zones of the strafe-o-meter
+               float accelzone_left_startangle;
+               float accelzone_right_startangle;
+               float preaccelzone_left_startangle;
+               float preaccelzone_right_startangle;
+               float neutral_startangle;
+               float overturn_startangle;
+
+               float accelzone_offsetangle = absolute_overturnangle - absolute_bestangle;
+               float preaccelzone_offsetangle = fabs(absolute_bestangle - absolute_prebestangle);
+               float neutral_offsetangle = 360;
+               float overturn_offsetangle = 360 - absolute_overturnangle * 2;
+
+               if(!autocvar_hud_panel_strafehud_bar_preaccel)
+                       preaccelzone_offsetangle = 0;
+
+               // assign starting angles and shift the current offset for every element
+               float current_startangle = 0;
+
+               preaccelzone_right_startangle = current_startangle;
+               current_startangle += preaccelzone_offsetangle;
+
+               accelzone_right_startangle = current_startangle;
+               current_startangle += accelzone_offsetangle;
+
+               overturn_startangle = current_startangle;
+               current_startangle += overturn_offsetangle;
+
+               accelzone_left_startangle = current_startangle;
+               current_startangle += accelzone_offsetangle;
+
+               preaccelzone_left_startangle = current_startangle;
+               current_startangle += preaccelzone_offsetangle;
+
+               neutral_startangle = current_startangle;
+               neutral_offsetangle = 360 - current_startangle;
+
+               // calculate how far off-center the strafe zones currently are
+               shiftangle += neutral_offsetangle / 2 - wishangle;
+
+               // shift strafe zones into correct place
+               neutral_startangle += shiftangle;
+               accelzone_left_startangle += shiftangle;
+               accelzone_right_startangle += shiftangle;
+               preaccelzone_left_startangle += shiftangle;
+               preaccelzone_right_startangle += shiftangle;
+               overturn_startangle += shiftangle;
+
+               // draw left acceleration zone
+               if(accelzone_offsetangle > 0)
+                       StrafeHUD_DrawStrafeHUD(
+                               accelzone_left_startangle, accelzone_offsetangle,
+                               autocvar_hud_panel_strafehud_bar_accel_color,
+                               autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha,
+                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_LEFT,
+                               hudangle);
+
+               if(autocvar_hud_panel_strafehud_bar_preaccel && preaccelzone_offsetangle > 0)
+                       StrafeHUD_DrawStrafeHUD(
+                               preaccelzone_left_startangle, preaccelzone_offsetangle,
+                               autocvar_hud_panel_strafehud_bar_preaccel_color,
+                               autocvar_hud_panel_strafehud_bar_preaccel_alpha * panel_fg_alpha,
+                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_RIGHT,
+                               hudangle);
+
+               // draw right acceleration zone
+               if(accelzone_offsetangle > 0)
+                       StrafeHUD_DrawStrafeHUD(
+                               accelzone_right_startangle, accelzone_offsetangle,
+                               autocvar_hud_panel_strafehud_bar_accel_color,
+                               autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha,
+                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_RIGHT,
+                               hudangle);
+
+               if(autocvar_hud_panel_strafehud_bar_preaccel && preaccelzone_offsetangle > 0)
+                       StrafeHUD_DrawStrafeHUD(
+                               preaccelzone_right_startangle, preaccelzone_offsetangle,
+                               autocvar_hud_panel_strafehud_bar_preaccel_color,
+                               autocvar_hud_panel_strafehud_bar_preaccel_alpha * panel_fg_alpha,
+                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_LEFT,
+                               hudangle);
+
+               // draw overturn zone
+               if(overturn_offsetangle > 0)
+                       StrafeHUD_DrawStrafeHUD(
+                               overturn_startangle, overturn_offsetangle,
+                               autocvar_hud_panel_strafehud_bar_overturn_color,
+                               autocvar_hud_panel_strafehud_bar_overturn_alpha * panel_fg_alpha,
+                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_BOTH,
+                               hudangle);
+
+               // draw neutral zone
+               if(neutral_offsetangle > 0)
+                       StrafeHUD_DrawStrafeHUD(
+                               neutral_startangle, neutral_offsetangle,
+                               autocvar_hud_panel_strafehud_bar_neutral_color,
+                               autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha,
+                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_NONE,
+                               hudangle);
+       }
+}
+
+// draw the actual strafe angle indicator
+void StrafeHUD_DrawAngleIndicator(
+       float angle, vector line_size, float arrow_size, int num_dashes,
+       bool has_top_arrow, bool has_bottom_arrow, vector color, float alpha, float hudangle)
+{
+       if(alpha <= 0) return;
+
+       // bound to HUD area
+       angle = bound(-hudangle / 2, angle, hudangle / 2);
+
+       float offset = StrafeHUD_AngleToOffset(angle, hudangle);
+       offset = StrafeHUD_ProjectOffset(offset, hudangle, false);
+
+       StrafeHUD_DrawAngleIndicatorLine(line_size, offset, num_dashes, color, alpha);
+
+       if(has_top_arrow)
+               StrafeHUD_DrawAngleIndicatorArrow(arrow_size, offset, line_size, color, alpha, true);
+
+       if(has_bottom_arrow)
+               StrafeHUD_DrawAngleIndicatorArrow(arrow_size, offset, line_size, color, alpha, false);
+}
+
+// draw the line of the angle indicator
+void StrafeHUD_DrawAngleIndicatorLine(vector size, float offset, int num_dashes, vector color, float alpha)
+{
+       if(num_dashes <= 0 || size.x <= 0 || size.y <= 0) return;
+
+       vector segment_size = size;
+       segment_size.y = size.y / (bound(1, num_dashes, size.y) * 2 - 1);
+
+       for(float i = 0; i < size.y; i += segment_size.y * 2)
+       {
+               // check if last iteration
+               if(i + segment_size.y * 2 >= size.y)
+                       segment_size.y = size.y - i;
+
+               drawfill(
+                       panel_pos - eY * ((size.y - panel_size.y) / 2 - i) + eX * (offset - segment_size.x / 2),
+                       segment_size, color, alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+       }
+}
+
+// draw the arrows on the angle indicator
+void StrafeHUD_DrawAngleIndicatorArrow(float size, float offset, vector line_size, vector color, float alpha, bool top)
+{
+       if(size <= 0) return;
+
+       if(top)
+       {
+               StrafeHUD_DrawStrafeArrow(
+                       panel_pos + eY * ((panel_size.y - line_size.y) / 2) + eX * offset,
+                       size, color, alpha * panel_fg_alpha, true, line_size.x);
+       }
+       else
+       {
+               StrafeHUD_DrawStrafeArrow(
+                       panel_pos + eY * ((panel_size.y - line_size.y) / 2 + line_size.y) + eX * offset,
+                       size, color, alpha * panel_fg_alpha, false, line_size.x);
+       }
+}
+
+// direction indicator
+void StrafeHUD_DrawDirectionIndicator(int direction, bool opposite_direction, bool fwd)
+{
+       vector direction_size_vertical;
+       direction_size_vertical.x = max(panel_size.y * min(autocvar_hud_panel_strafehud_direction_width, 1), 1);
+       direction_size_vertical.y = panel_size.y + direction_size_vertical.x * 2;
+       direction_size_vertical.z = 0;
+
+       vector direction_size_horizontal;
+       direction_size_horizontal.x = panel_size.x * min(autocvar_hud_panel_strafehud_direction_length, .5);
+       direction_size_horizontal.y = direction_size_vertical.x;
+       direction_size_horizontal.z = 0;
+
+       if(direction != STRAFEHUD_DIRECTION_NONE &&
+          direction_size_vertical.x > 0 &&
+          autocvar_hud_panel_strafehud_direction_alpha > 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 != opposite_direction)
+                       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);
+       }
+}
diff --git a/qcsrc/client/hud/panel/strafehud/draw_core.qh b/qcsrc/client/hud/panel/strafehud/draw_core.qh
new file mode 100644 (file)
index 0000000..8ed5d53
--- /dev/null
@@ -0,0 +1,8 @@
+#pragma once
+#include "../strafehud.qh"
+
+void StrafeHUD_DrawStrafeMeter(float, float, float, float, float, bool, float);
+void StrafeHUD_DrawAngleIndicator(float, vector, float, int, bool, bool, vector, float, float);
+void StrafeHUD_DrawAngleIndicatorLine(vector, float, int, vector, float);
+void StrafeHUD_DrawAngleIndicatorArrow(float, float, vector, vector, float, bool);
+void StrafeHUD_DrawDirectionIndicator(int, bool, bool);
diff --git a/qcsrc/client/hud/panel/strafehud/extra.qc b/qcsrc/client/hud/panel/strafehud/extra.qc
new file mode 100644 (file)
index 0000000..e580395
--- /dev/null
@@ -0,0 +1,270 @@
+#include "extra.qh"
+
+#include <client/csqcmodel_hooks.qh>
+#include <client/draw.qh>
+#include <lib/csqcmodel/cl_player.qh>
+#include <common/physics/player.qh>
+
+// start speed
+#include <client/hud/panel/racetimer.qh> // checkpoint information (race_*)
+
+// jump height
+#include <lib/csqcmodel/common.qh> // for IS_PLAYER() macro
+#include <common/resources/cl_resources.qh> // IS_DEAD() macro
+
+// epsilon value for the slick detector steps to avoid
+// an infinite loop due to floating point rounding errors
+// (works with current limits)
+#define SLICKDETECT_STEPS_EPSILON 0.00001
+
+// slick detector
+// scans for slick in every direction downwards from the player's feet
+// may cause performance issues on slower machines
+float StrafeHUD_DrawSlickDetector(entity e, bool already_detected)
+{
+       float slickdetector_height = bound(0, autocvar_hud_panel_strafehud_slickdetector_height, 1);
+       slickdetector_height *= panel_size.y;
+       if(autocvar_hud_panel_strafehud_slickdetector &&
+          autocvar_hud_panel_strafehud_slickdetector_range > 0 &&
+          autocvar_hud_panel_strafehud_slickdetector_alpha > 0 &&
+          slickdetector_height > 0 &&
+          panel_size.x > 0)
+       {
+               float slicksteps = bound(0, autocvar_hud_panel_strafehud_slickdetector_granularity, 4);
+               bool allslick = PHYS_FRICTION(e) == 0;
+               bool slickdetected = false;
+
+               slicksteps = 90 * DEG2RAD / 2 ** slicksteps;
+
+               // don't need to traceline if already touching slick
+               slickdetected = already_detected;
+
+               // coordinates at the bottom center of the player bbox
+               vector traceorigin = e.origin + eZ * e.mins.z;
+
+               // traceline downwards into every direction
+               trace_dphitq3surfaceflags = 0;
+               for(float i = 0; i < 90 * DEG2RAD - SLICKDETECT_STEPS_EPSILON && !slickdetected; i += slicksteps)
+               {
+                       vector slickoffset;
+                       float slickrotate;
+
+                       // creates a vector angled 'i' degrees relative to the Z vector
+                       // negative cosine value to face downwards
+                       slickoffset.z = -cos(i) * autocvar_hud_panel_strafehud_slickdetector_range;
+                       slickrotate = sin(i) * autocvar_hud_panel_strafehud_slickdetector_range;
+
+                       for(float j = 0; j < 360 * DEG2RAD - SLICKDETECT_STEPS_EPSILON && !slickdetected; j += slicksteps)
+                       {
+                               // adjusts the vector so that it rotates 'j' degrees around the Z vector
+                               slickoffset.x = sin(j) * slickrotate;
+                               slickoffset.y = cos(j) * slickrotate;
+
+                               // trace a line, we hit slick if:
+                               //  - it hits something and surface friction is disabled
+                               //  - the slick surface flag got set
+                               // note: it is not guaranteed that the detected surface is actually
+                               //       a zero friction surface if PHYS_FRICTION_SLICK() does not equal zero
+                               traceline(traceorigin, traceorigin + slickoffset, MOVE_NOMONSTERS, e);
+                               if((allslick && trace_fraction < 1)
+                               || (trace_dphitq3surfaceflags & Q3SURFACEFLAG_SLICK))
+                                       slickdetected = true;
+
+                               // rotation does nothing when we are perpendicular to the ground, hence only one iteration
+                               if(i == 0) break;
+                       }
+               }
+
+               // if a traceline hit a slick surface
+               if(slickdetected)
+               {
+                       vector slickdetector_size = panel_size;
+                       slickdetector_size.y = slickdetector_height;
+
+                       // horizontal lines
+                       for(int i = 0; i <= 1; ++i)
+                       {
+                               float y_offset = (i == 0)
+                                       ? -slickdetector_size.y // top
+                                       : panel_size.y;         // bottom
+                               drawfill(
+                                       panel_pos + eY * y_offset,
+                                       slickdetector_size, autocvar_hud_panel_strafehud_slickdetector_color,
+                                       autocvar_hud_panel_strafehud_slickdetector_alpha * panel_fg_alpha,
+                                       DRAWFLAG_NORMAL);
+                       }
+               }
+
+               return slickdetector_height;
+       }
+
+       return 0;
+}
+
+// vertical angle for weapon jumps
+void StrafeHUD_DrawVerticalAngle(entity e, float text_offset_top, float text_offset_bottom)
+{
+       if(!autocvar_hud_panel_strafehud_vangle) return;
+
+       float vangle = -PHYS_INPUT_ANGLES(e).x;
+       float vangle_height = autocvar_hud_panel_strafehud_vangle_size * panel_size.y;
+       string vangle_text = strcat(ftos_decimals(vangle, 2), "°");
+
+       StrafeHUD_DrawTextIndicator(
+               vangle_text, vangle_height,
+               autocvar_hud_panel_strafehud_vangle_color, 1,
+               time, autocvar_hud_panel_strafehud_vangle_pos,
+               text_offset_top, text_offset_bottom);
+}
+
+// show height achieved by a single jump
+// FIXME: checking z position differences is unreliable (warpzones, teleporter, kill, etc), use velocity to calculate jump height instead
+// FIXME: move capturing the jump height value out of the HUD
+void StrafeHUD_DrawJumpHeight(entity e, bool onground, bool swimming, float text_offset_top, float text_offset_bottom)
+{
+       float length_conversion_factor = StrafeHUD_GetLengthUnitFactor(autocvar_hud_speed_unit);
+       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((e.velocity.z <= 0) || onground || swimming || IS_DEAD(e) || !IS_PLAYER(e))
+       {
+               height_min = height_max = e.origin.z;
+       }
+       else if(e.origin.z > height_max)
+       {
+               height_max = e.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) return;
+
+       // use more decimals when displaying km or miles
+       int length_decimals = autocvar_hud_speed_unit >= 3 && autocvar_hud_speed_unit <= 5 ? 6 : 2;
+
+       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, StrafeHUD_GetLengthUnit(autocvar_hud_speed_unit));
+
+       StrafeHUD_DrawTextIndicator(
+               jumpheight_text, jumpheight_height,
+               autocvar_hud_panel_strafehud_jumpheight_color,
+               autocvar_hud_panel_strafehud_jumpheight_fade,
+               jumptime, autocvar_hud_panel_strafehud_jumpheight_pos,
+               text_offset_top, text_offset_bottom);
+}
+
+// strafe efficiency, percentage of how far away the current angle is from the optimal angle
+// the percentage changes linearly with angular distance
+void StrafeHUD_DrawStrafeEfficiency(float strafe_ratio, float text_offset_top, float text_offset_bottom)
+{
+       if(!autocvar_hud_panel_strafehud_strafeefficiency) return;
+
+       float strafeeff_height = autocvar_hud_panel_strafehud_strafeefficiency_size * panel_size.y;
+       string strafeeff_text = strcat(ftos_decimals(strafe_ratio * 100, 2), "%");
+       vector strafeeff_color = StrafeHUD_MixColors('1 1 1', (strafe_ratio > 0 ? '0 1 0' : '1 0 0'), fabs(strafe_ratio));
+
+       StrafeHUD_DrawTextIndicator(
+               strafeeff_text, strafeeff_height,
+               strafeeff_color, 1,
+               time, autocvar_hud_panel_strafehud_strafeefficiency_pos,
+               text_offset_top, text_offset_bottom);
+}
+
+// show speed when crossing the start trigger
+// FIXME: move capturing the race start speed value out of the HUD
+void StrafeHUD_DrawStartSpeed(float speed, float text_offset_top, float text_offset_bottom)
+{
+       static float startspeed = 0, starttime = 0; // displayed value and timestamp for fade out
+
+       // check if the start trigger was hit (will also trigger if the finish trigger was hit if those have the same ID)
+       if((race_nextcheckpoint == 1) || (race_checkpoint == 254 && race_nextcheckpoint == 255))
+       {
+               if((race_checkpointtime > 0) && (starttime != race_checkpointtime))
+               {
+                       starttime = race_checkpointtime;
+                       startspeed = speed;
+               }
+       }
+
+       if(!autocvar_hud_panel_strafehud_startspeed) return;
+
+       float speed_conversion_factor = GetSpeedUnitFactor(autocvar_hud_speed_unit);
+       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_speed_unit));
+
+       StrafeHUD_DrawTextIndicator(
+               startspeed_text, startspeed_height,
+               autocvar_hud_panel_strafehud_startspeed_color,
+               autocvar_hud_panel_strafehud_startspeed_fade,
+               starttime, autocvar_hud_panel_strafehud_startspeed_pos,
+               text_offset_top, text_offset_bottom);
+}
+
+// strafe sonar for audible feedback when strafing
+void StrafeHUD_Sonar(float strafe_ratio, string sonarsound)
+{
+       static float sonar_time = 0;
+
+       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);
+       }
+}
+
+// update and precache the sonar sound and store the proper sound path
+string StrafeHUD_UpdateSonarSound()
+{
+       string newsound = autocvar_hud_panel_strafehud_sonar_audio;
+       static string cursound = string_null;
+       static string sonarsound = string_null;
+       if(newsound == "")
+       {
+               strfree(cursound);
+               strfree(sonarsound);
+               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);
+               }
+       }
+
+       return sonarsound;
+}
diff --git a/qcsrc/client/hud/panel/strafehud/extra.qh b/qcsrc/client/hud/panel/strafehud/extra.qh
new file mode 100644 (file)
index 0000000..836d43d
--- /dev/null
@@ -0,0 +1,12 @@
+#pragma once
+#include "../strafehud.qh"
+
+float StrafeHUD_DrawSlickDetector(entity, bool);
+
+void StrafeHUD_DrawVerticalAngle(entity, float, float);
+void StrafeHUD_DrawJumpHeight(entity, bool, bool, float, float);
+void StrafeHUD_DrawStrafeEfficiency(float, float, float);
+void StrafeHUD_DrawStartSpeed(float, float, float);
+
+void StrafeHUD_Sonar(float, string);
+string StrafeHUD_UpdateSonarSound();
diff --git a/qcsrc/client/hud/panel/strafehud/util.qc b/qcsrc/client/hud/panel/strafehud/util.qc
new file mode 100644 (file)
index 0000000..c3786e1
--- /dev/null
@@ -0,0 +1,336 @@
+#include "util.qh"
+
+#include <lib/csqcmodel/cl_player.qh>
+#include <common/physics/player.qh>
+#include <common/physics/movetypes/movetypes.qh>
+
+// convert a strafe angle into a HUD width value
+float StrafeHUD_AngleToWidth(float angle, float range)
+{
+       return angle / range * panel_size.x;
+}
+
+// convert a strafe angle into a centered HUD offset value
+float StrafeHUD_AngleToOffset(float angle, float range)
+{
+       return StrafeHUD_AngleToWidth(angle, range) + panel_size.x / 2;
+}
+
+// turn a ratio into a projected ratio based on the total angular distance
+float StrafeHUD_Project(float ratio, float range, bool reverse)
+{
+       range *= DEG2RAD / 2;
+       switch(autocvar_hud_panel_strafehud_projection)
+       {
+               default:
+               case STRAFEHUD_PROJECTION_LINEAR:
+                       return ratio;
+               case STRAFEHUD_PROJECTION_PERSPECTIVE:
+                       if(!reverse)
+                       {
+                               ratio *= range;
+                               ratio = tan(ratio) / tan(range);
+                       }
+                       else
+                       {
+                               ratio = atan(ratio * tan(range));
+                               ratio /= range;
+                       }
+                       break;
+               case STRAFEHUD_PROJECTION_PANORAMIC:
+                       if(!reverse)
+                       {
+                               ratio *= range;
+                               ratio = tan(ratio / 2) / tan(range / 2);
+                       }
+                       else
+                       {
+                               ratio = atan(ratio * tan(range / 2)) * 2;
+                               ratio /= range;
+                       }
+                       break;
+       }
+       return ratio;
+}
+
+// project a centered HUD offset value
+float StrafeHUD_ProjectOffset(float offset, float range, bool reverse)
+{
+       if(autocvar_hud_panel_strafehud_projection == STRAFEHUD_PROJECTION_LINEAR)
+               return offset;
+
+       float ratio = (offset - (panel_size.x / 2)) / (panel_size.x / 2);
+       ratio = StrafeHUD_Project(ratio, range, reverse);
+       offset = ratio * (panel_size.x / 2) + (panel_size.x / 2);
+       return offset;
+}
+
+// project a HUD width value
+float StrafeHUD_ProjectWidth(float offset, float width, float range)
+{
+       if(autocvar_hud_panel_strafehud_projection == STRAFEHUD_PROJECTION_LINEAR)
+               return width;
+
+       return StrafeHUD_ProjectOffset(offset + width, range, false) - StrafeHUD_ProjectOffset(offset, range, false);
+}
+
+// length unit conversion (km and miles are only included to match the GetSpeedUnit* functions)
+float StrafeHUD_GetLengthUnitFactor(int length_unit)
+{
+       switch(length_unit)
+       {
+               default:
+               case 1: return 1.0;
+               case 2: return 0.0254;
+               case 3: return 0.0254 * 0.001;
+               case 4: return 0.0254 * 0.001 * 0.6213711922;
+               case 5: return 0.0254 * 0.001 * 0.5399568035;
+       }
+}
+
+string StrafeHUD_GetLengthUnit(int length_unit)
+{
+       switch(length_unit)
+       {
+               // translator-friendly strings without the initial space
+               default:
+               case 1: return strcat(" ", _("qu"));
+               case 2: return strcat(" ", _("m"));
+               case 3: return strcat(" ", _("km"));
+               case 4: return strcat(" ", _("mi"));
+               case 5: return strcat(" ", _("nmi"));
+       }
+}
+
+// check the player waterlevel without affecting the player entity, this way we can fetch waterlevel even if client prediction is disabled
+float StrafeHUD_DetermineWaterLevel(entity e)
+{
+       // store old values
+       void old_contentstransition(int, int) = e.contentstransition;
+       float old_watertype = e.watertype;
+       float old_waterlevel = e.waterlevel;
+
+       e.contentstransition = func_null; // unset the contentstransition function if present
+       _Movetype_CheckWater(e);
+       float new_waterlevel = e.waterlevel; // store the player waterlevel
+
+       // restore old values
+       e.contentstransition = old_contentstransition;
+       e.watertype = old_watertype;
+       e.waterlevel = old_waterlevel;
+
+       return new_waterlevel;
+}
+
+// determine frametime, to avoid jitter, average the frametime in case client prediction is used
+float StrafeHUD_DetermineFrameTime()
+{
+       static float dt_update = 0;
+       static int dt_time = 0;
+       static float dt_sum = 0;
+       static float dt = 0;
+       if((csqcplayer_status == CSQCPLAYERSTATUS_PREDICTED) && (input_timelength > 0))
+       {
+               float dt_client = input_timelength;
+
+               if(dt_client > .05) // server splits frames longer than 50 ms into two moves (DarkPlaces behaviour)
+                       dt_client /= 2; // does not ensure frames are smaller than 50 ms, just splits large frames in half, matches server behaviour
+
+               /* calculate average frametime
+                * calculated using a weighted arithmetic mean, where the weighting is equal to the frametime itself
+                * for example, given a 1 ms frame and a 9 ms frame we have:
+                *   a total time of 10 ms
+                *   a weighted sum of 1 ms * 1 ms + 9 ms * 9 ms = 82 ms^2
+                *   the final result is 82 ms^2 / 10 ms = 8.2 ms
+                */
+               dt_sum += dt_client * dt_client; // weighted sum of all frametimes (mean numerator)
+               dt_time += dt_client; // time spent averaging (mean denominator)
+
+               if(((time - dt_update) > autocvar_hud_panel_strafehud_fps_update) || (dt_update == 0))
+               {
+                       dt = dt_sum / dt_time;
+                       dt_update = time;
+                       dt_time = dt_sum = 0;
+               }
+       }
+       else // when spectating other players server ticrate will be used, this may not be accurate but there is no way to find other player's frametime
+       {
+               dt = ticrate;
+               dt_update = dt_time = dt_sum = 0;
+       }
+
+       return dt;
+}
+
+// determine player wishdir, non-local player movement is limited to 45 degree steps
+float StrafeHUD_DetermineWishAngle(vector movement, int keys, bool is_local)
+{
+       float wishangle;
+       if(is_local) // if entity is local player
+       {
+               if(movement.y == 0)
+                       wishangle = 0;
+               else
+               {
+                       wishangle = RAD2DEG * atan2(movement.y, movement.x);
+                       // wrap the wish angle if it exceeds ±90°
+                       if(fabs(wishangle) > 90)
+                       {
+                               if(wishangle < 0)
+                                       wishangle += 180;
+                               else
+                                       wishangle -= 180;
+
+                               wishangle *= -1;
+                       }
+               }
+       }
+       else // alternatively calculate wishdir by querying pressed keys
+       {
+               if(keys & (KEY_FORWARD | KEY_BACKWARD))
+                       wishangle = 45;
+               else
+                       wishangle = 90;
+               if(keys & KEY_LEFT)
+                       wishangle *= -1;
+               else if(!(keys & KEY_RIGHT))
+                       wishangle = 0; // wraps at 180°
+       }
+
+       return wishangle;
+}
+
+// determine whether the player is pressing forwards or backwards keys
+int StrafeHUD_DetermineForwardKeys(vector movement, int keys, bool is_local)
+{
+       if(is_local) // if entity is local player
+       {
+               if(movement.x > 0)
+                       return STRAFEHUD_KEYS_FORWARD;
+               else if(movement.x < 0)
+                       return STRAFEHUD_KEYS_BACKWARD;
+               else
+                       return STRAFEHUD_KEYS_NONE;
+       }
+       else // alternatively determine direction by querying pressed keys
+       {
+               if((keys & KEY_FORWARD) && !(keys & KEY_BACKWARD))
+                       return STRAFEHUD_KEYS_FORWARD;
+               else if(!(keys & KEY_FORWARD) && (keys & KEY_BACKWARD))
+                       return STRAFEHUD_KEYS_BACKWARD;
+               else
+                       return STRAFEHUD_KEYS_NONE;
+       }
+}
+
+float StrafeHUD_DetermineHudAngle(float absolute_wishangle, float absolute_overturnangle, float strafity)
+{
+
+       // determine the minimal required HUD angle to contain the full strafing angle range
+       // this is useful for the velocity centered mode where the zones do not follow the strafing angle
+       // how it works:
+       //   the angle where the most acceleration occurs moves relative to the player velocity
+       //   from 0 - wishangle to absolute_overturnangle - wishangle
+       //   the angle farther away from the center is the maximum the optimal strafing angle can
+       //   diverge from the direction of velocity
+       //   this angle has to be multiplied by two since the HUD extends in both directions which
+       //   halves the amount it extends in a single direction
+       float range_minangle = max(absolute_wishangle, absolute_overturnangle - absolute_wishangle) * 2;
+
+       float range_normal = autocvar_hud_panel_strafehud_range;
+       float range_side   = autocvar_hud_panel_strafehud_range_sidestrafe;
+       float range_used;
+
+       float hfov = getproperty(VF_FOVX);
+       if(isnan(range_normal) || isnan(range_side) || isnan(hfov)) return 360;
+
+       // negative values enable different behaviour
+       // no exact matching so that all negative values are caught
+       if(range_normal == 0) // range = 0, use minimum angle required if dynamically setting hud angle
+               range_normal = autocvar__hud_configure ? 90 : range_minangle;
+       else if(range_normal < 0) // range = -1, use the current field of view
+               range_normal = hfov;
+
+       if(range_side < -1) // range = -2, use the normal range
+               range_used = range_normal;
+       else
+       {
+               if(range_side == 0)  // range = 0, use minimum angle required if dynamically setting hud angle
+                       range_side = autocvar__hud_configure ? 90 : range_minangle;
+               else if(range_side < 0) // range = -1, use the current field of view
+                       range_side = hfov;
+
+               range_used = GeomLerp(range_normal, strafity, range_side);
+       }
+       float hudangle = bound(0, fabs(range_used), 360); // limit HUD range to 360 degrees, higher values don't make sense
+
+       // limit strafe-meter angle to values suitable for the current projection mode
+       switch(autocvar_hud_panel_strafehud_projection)
+       {
+               // those limits are a little less than the maximal FOV the game allows
+               // however, they suffice for all realistic use cases
+               case STRAFEHUD_PROJECTION_PERSPECTIVE:
+                       hudangle = min(hudangle, 170);
+                       break;
+               case STRAFEHUD_PROJECTION_PANORAMIC:
+                       hudangle = min(hudangle, 350);
+                       break;
+       }
+
+       return hudangle;
+}
+
+// determine whether the player is strafing left or right
+float StrafeHUD_DetermineDirection(float angle, float wishangle, float antiflicker_angle)
+{
+       if(wishangle > 0)
+               return STRAFEHUD_DIRECTION_RIGHT;
+       else if(wishangle < 0)
+               return STRAFEHUD_DIRECTION_LEFT;
+       else
+       {
+               if(angle > antiflicker_angle && angle < (180 - antiflicker_angle))
+                       return STRAFEHUD_DIRECTION_RIGHT;
+               else if(angle < -antiflicker_angle && angle > (-180 + antiflicker_angle))
+                       return STRAFEHUD_DIRECTION_LEFT;
+               else
+                       return STRAFEHUD_DIRECTION_NONE;
+       }
+}
+
+// determine whether the player holds the jump key
+// try to ignore if track_canjump is enabled
+// does not work in spectator mode if the spectated player uses +jetpack or cl_movement_track_canjump
+bool StrafeHUD_DetermineJumpHeld(entity e, int keys, bool is_local)
+{
+       if(is_local)
+       {
+               if((PHYS_INPUT_BUTTON_JUMP(e) || PHYS_INPUT_BUTTON_JETPACK(e)) && !PHYS_CL_TRACK_CANJUMP(e))
+                       return true;
+       }
+       else
+       {
+               if((keys & KEY_JUMP) && !PHYS_TRACK_CANJUMP(e))
+                       return true;
+       }
+
+       return false;
+}
+
+// mix two colors based on a ratio
+// TODO: move mixing colors out of the HUD, this could be useful for other code
+vector StrafeHUD_MixColors(vector color1, vector color2, float ratio)
+{
+       if(ratio <= 0) return color1;
+       if(ratio >= 1) return color2;
+       return color1 + (color2 - color1) * ratio;
+}
+
+vector StrafeHUD_CalculateTextIndicatorPosition(vector pos)
+{
+       pos.x *= panel_size.x / 2; // more intuitive since we start in the middle, this turns the range from -0.5 to +0.5 into -1 to +1
+       pos.y *= panel_size.y;
+       pos.z = 0;
+
+       return pos;
+}
diff --git a/qcsrc/client/hud/panel/strafehud/util.qh b/qcsrc/client/hud/panel/strafehud/util.qh
new file mode 100644 (file)
index 0000000..11d8064
--- /dev/null
@@ -0,0 +1,23 @@
+#pragma once
+#include "../strafehud.qh"
+
+float StrafeHUD_AngleToWidth(float, float);
+float StrafeHUD_AngleToOffset(float, float);
+float StrafeHUD_Project(float, float, bool);
+float StrafeHUD_ProjectOffset(float, float, bool);
+float StrafeHUD_ProjectWidth(float, float, float);
+
+float StrafeHUD_GetLengthUnitFactor(int);
+string StrafeHUD_GetLengthUnit(int);
+
+float StrafeHUD_DetermineWaterLevel(entity);
+float StrafeHUD_DetermineFrameTime();
+float StrafeHUD_DetermineWishAngle(vector, int, bool);
+int StrafeHUD_DetermineForwardKeys(vector, int, bool);
+float StrafeHUD_DetermineHudAngle(float, float, float);
+float StrafeHUD_DetermineDirection(float, float, float);
+bool StrafeHUD_DetermineJumpHeld(entity, int, bool);
+
+vector StrafeHUD_MixColors(vector, vector, float);
+
+vector StrafeHUD_CalculateTextIndicatorPosition(vector);