]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge remote-tracking branch 'origin/bones_was_here/q3compat_gravity' into morosophos...
authorNick S <nick@teichisma.info>
Tue, 7 Feb 2023 13:24:53 +0000 (15:24 +0200)
committerNick S <nick@teichisma.info>
Tue, 7 Feb 2023 13:24:53 +0000 (15:24 +0200)
47 files changed:
_hud_common.cfg
bal-wep-mario.cfg
bal-wep-nexuiz25.cfg
bal-wep-samual.cfg
bal-wep-xdf.cfg
bal-wep-xonotic.cfg
qcsrc/client/hud/panel/scoreboard.qc
qcsrc/client/hud/panel/strafehud.qc
qcsrc/client/hud/panel/strafehud.qh
qcsrc/common/command/generic.qc
qcsrc/common/constants.qh
qcsrc/common/gamemodes/gamemode/ctf/sv_ctf.qc
qcsrc/common/gamemodes/gamemode/cts/sv_cts.qc
qcsrc/common/mapobjects/target/_mod.inc
qcsrc/common/mapobjects/target/_mod.qh
qcsrc/common/mapobjects/target/speed.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/speed.qh [new file with mode: 0644]
qcsrc/common/mapobjects/teleporters.qc
qcsrc/common/mapobjects/trigger/jumppads.qc
qcsrc/common/mapobjects/trigger/jumppads.qh
qcsrc/common/mapobjects/trigger/teleport.qc
qcsrc/common/mapobjects/trigger/teleport.qh
qcsrc/common/mutators/mutator/overkill/okhmg.qc
qcsrc/common/mutators/mutator/overkill/okmachinegun.qc
qcsrc/common/mutators/mutator/overkill/oknex.qc
qcsrc/common/mutators/mutator/overkill/okrpc.qc
qcsrc/common/mutators/mutator/overkill/okshotgun.qc
qcsrc/common/physics/movetypes/movetypes.qc
qcsrc/common/physics/movetypes/movetypes.qh
qcsrc/common/scores.qh
qcsrc/common/stats.qh
qcsrc/common/weapons/weapon/blaster.qc
qcsrc/common/weapons/weapon/blaster.qh
qcsrc/common/weapons/weapon/crylink.qc
qcsrc/common/weapons/weapon/crylink.qh
qcsrc/common/weapons/weapon/vaporizer.qc
qcsrc/menu/xonotic/dialog_hudpanel_strafehud.qc
qcsrc/menu/xonotic/dialog_hudpanel_strafehud.qh
qcsrc/server/_mod.inc
qcsrc/server/_mod.qh
qcsrc/server/command/sv_cmd.qc
qcsrc/server/main.qc
qcsrc/server/race.qc
qcsrc/server/race.qh
qcsrc/server/strafe.qc [new file with mode: 0644]
qcsrc/server/strafe.qh [new file with mode: 0644]
xonotic-server.cfg

index 766f89819bc9d1ef4095699a0c7eb3465cd23fe4..cc2556852e04377a27134f0b2e0a167a99a75fb2 100644 (file)
@@ -155,50 +155,76 @@ 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 "0" "the angle range up to 360 degrees displayed on the strafehud, \"0\" = dynamic (chooses the minimum range required to still see the whole area needed for accelerating at once)"
-seta hud_panel_strafehud_style "1" "\"0\" = no styling, \"1\" = progress bar style for the strafe bar, \"2\" = gradient for the strafe bar"
+seta hud_panel_strafehud_range "90" "the angle range up to 360 degrees displayed on the strafehud, \"-1\" = current fov, \"0\" = dynamic (chooses the minimum range required to still see the whole area needed for accelerating)"
+seta hud_panel_strafehud_style "2" "\"0\" = no styling, \"1\" = progress bar style for the strafe bar, \"2\" = gradient for the strafe bar"
 seta hud_panel_strafehud_unit "1" "speed unit (1 = qu/s, 2 = m/s, 3 = km/h, 4 = mph, 5 = knots), length unit (1 = qu, 2 = m, 3 = km, 4 = mi, 5 = nmi)"
 seta hud_panel_strafehud_unit_show "1" "show units"
+seta hud_panel_strafehud_uncapped "0" "set to \"1\" to remove some safety restrictions, useful to set thinner indicator lines down to 1px or for trying out higher values for some performance degrading operations (warning: elements may turn invisible if too thin, other configurations may crash your game or look horribly ugly)"
+seta hud_panel_strafehud_bar_preaccel "1" "set to \"1\" to extend the acceleration zone by the strafe meter zone before full acceleration can be achieved"
 seta hud_panel_strafehud_bar_neutral_color "1 1 1" "color of the strafe meter neutral zone"
-seta hud_panel_strafehud_bar_neutral_alpha "0.3" "opacity of the strafe meter neutral zone"
+seta hud_panel_strafehud_bar_neutral_alpha "0.1" "opacity of the strafe meter neutral zone"
 seta hud_panel_strafehud_bar_accel_color "0 1 0" "color of the strafe meter acceleration zone"
-seta hud_panel_strafehud_bar_accel_alpha "0.3" "opacity of the strafe meter acceleration zone"
+seta hud_panel_strafehud_bar_accel_alpha "0.5" "opacity of the strafe meter acceleration zone"
 seta hud_panel_strafehud_bar_overturn_color "1 0 1" "color of the strafe meter overturn zone"
-seta hud_panel_strafehud_bar_overturn_alpha "0.3" "opacity of the strafe meter overturn zone"
+seta hud_panel_strafehud_bar_overturn_alpha "0.5" "opacity of the strafe meter overturn zone"
+seta hud_panel_strafehud_angle_style "0" "set the angle indicator style: 0 = none, 1 = solid line, 2 = dashed line"
+seta hud_panel_strafehud_angle_dashes "4" "determines the amount of dashes if the angle indicator uses a dashed line"
 seta hud_panel_strafehud_angle_alpha "0.8" "opacity of the indicator showing the player's current angle"
-seta hud_panel_strafehud_angle_height "1.5" "height of the indicator showing the player's current angle (relative to the panel height)"
-seta hud_panel_strafehud_angle_width "0.005" "width of the indicator showing the player's current angle (relative to the panel width)"
-seta hud_panel_strafehud_angle_neutral_color "1 1 0" "color of the indicator showing the player's current angle if the player's angle is within the neutral zone"
-seta hud_panel_strafehud_angle_accel_color "0 1 1" "color of the indicator showing the player's current angle if the player's angle is within the acceleration zone"
-seta hud_panel_strafehud_angle_overturn_color "1 0 1" "color of the indicator showing the player's current angle if the player's angle is within the overturn zone"
-seta hud_panel_strafehud_switch_minspeed "-1" "minimum speed in qu/s at which switch indicators which are used to aid changing strafe direction will be shown (uses physics maxspeed + antiflicker speed if negative)"
-seta hud_panel_strafehud_switch_active_color "0 1 0" "color of the switch indicator on the current side"
-seta hud_panel_strafehud_switch_active_alpha "1" "opacity of the switch indicator on the current side"
-seta hud_panel_strafehud_switch_inactive_color "1 1 0" "color of the switch indicator on the opposite side"
-seta hud_panel_strafehud_switch_inactive_alpha "1" "opacity of the switch indicator on the opposite side"
-seta hud_panel_strafehud_switch_width "0.0075" "width of the strafe angle indicators (relative to the strafe bar width)"
+seta hud_panel_strafehud_angle_height "1" "height of the indicator showing the player's current angle (relative to the panel height)"
+seta hud_panel_strafehud_angle_width "0.001" "width of the indicator showing the player's current angle (relative to the panel width)"
+seta hud_panel_strafehud_angle_neutral_color "1 1 0" "color of the indicator showing the player's current angle if it is within the neutral zone"
+seta hud_panel_strafehud_angle_accel_color "0 1 1" "color of the indicator showing the player's current angle if it is within the acceleration zone"
+seta hud_panel_strafehud_angle_overturn_color "1 0 1" "color of the indicator showing the player's current angle if it is within the overturn zone"
+seta hud_panel_strafehud_angle_arrow "1" "set the angle indicator's arrow style: 0 = none, 1 = top, 2 = bottom, 3 = both"
+seta hud_panel_strafehud_angle_arrow_size "0.5" "size of the arrow (relative to the panel height)"
+seta hud_panel_strafehud_bestangle "1" "set to \"1\" to enable a ghost angle indicator showing the best angle to gain maximum acceleration"
+seta hud_panel_strafehud_bestangle_color "1 1 1" "color of the indicator showing the best angle to gain maximum acceleration"
+seta hud_panel_strafehud_bestangle_alpha "0.5" "opacity of the indicator showing the best angle to gain maximum acceleration"
+seta hud_panel_strafehud_switch "1" "set to \"1\" to enable the switch indicator showing the angle to move to when switching sides"
+seta hud_panel_strafehud_switch_minspeed "-1" "minimum speed in qu/s at which switch indicator(s) which are used to aid changing strafe direction will be shown (set to -1 for dynamic minspeed)"
+seta hud_panel_strafehud_switch_color "1 1 0" "color of the switch indicator"
+seta hud_panel_strafehud_switch_alpha "1" "opacity of the switch indicator"
+seta hud_panel_strafehud_switch_width "0.003" "width of the strafe angle indicator(s) (relative to the strafe bar width)"
+seta hud_panel_strafehud_direction "0" "set to \"1\" to enable the direction caps to see in which direction you are currently strafing"
 seta hud_panel_strafehud_direction_color "0 0.5 1" "color of the direction caps which indicate the direction the player is currently strafing towards"
 seta hud_panel_strafehud_direction_alpha "1" "opacity of the direction caps which indicate the direction the player is currently strafing towards"
 seta hud_panel_strafehud_direction_width "0.25" "stroke width of the direction caps which indicate the direction the player is currently strafing towards (relative to the panel height)"
 seta hud_panel_strafehud_direction_length "0.02" "length of the horizontal component of the direction caps which indicate the direction the player is currently strafing towards (relative to the panel width)"
-seta hud_panel_strafehud_slickdetector_range "0" "range of the slick detector in qu, \"0\" to disable"
-seta hud_panel_strafehud_slickdetector_granularity "2" "value from 0 to 4 which defines how exact the search for slick should be, higher values may yield better results but require more computation"
+seta hud_panel_strafehud_slickdetector "1" "set to \"1\" to enable the slick detector which notifies you if there is slick near you"
+seta hud_panel_strafehud_slickdetector_range "200" "range of the slick detector in qu"
+seta hud_panel_strafehud_slickdetector_granularity "1" "value from 0 to 4 which defines how exact the search for slick should be, higher values may yield better results but require more computation"
 seta hud_panel_strafehud_slickdetector_color "0 1 1" "color of the slick detector indicator"
 seta hud_panel_strafehud_slickdetector_alpha "0.5" "opacity of the slick detector indicator"
 seta hud_panel_strafehud_slickdetector_height "0.125" "height of the slick detector indicator (relative to the panel height)"
-seta hud_panel_strafehud_startspeed_fade "0" "fade time (in seconds) of the start speed text or \"0\" to disable"
+seta hud_panel_strafehud_startspeed "1" "set to \"1\" to enable the start speed indicator which shows you the speed you had while passing the start trigger of a race map"
+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_size "1.5" "size of the start speed text (relative to the panel height)"
-seta hud_panel_strafehud_jumpheight_fade "0" "fade time (in seconds) of the jump height text or \"0\" to disable"
+seta hud_panel_strafehud_jumpheight "0" "set to \"1\" to 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_size "1.5" "size of the jump height text (relative to the panel height)"
-seta hud_panel_strafehud_timeout_air "0.1" "time (in seconds) after take off before changing to air strafe physics when not jumping (visually more consistent hud while on slick downwards ramps)"
-seta hud_panel_strafehud_timeout_ground "0.03333333" "time (in seconds) after landing before changing to non-air strafe physics (visually more consistent hud while strafe turning when touching the floor after every hop)"
+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 slick downwards 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_timeout_direction "0.5" "time (in seconds) it takes until direction changes (forward or backward movement) are applied (set to zero if you intend to sideways strafe)"
 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_antiflicker_speed "0.0001" "how many qu/s 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" "set to \"1\" to 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" "set to \"1\" to 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_size "1" "size of the vertical angle 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 adb7cc654fa3565e82441a1ca9115936191ebfc1..526fd0e544a72ea742526216b454c5f49e55ba4d 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 5
 set g_balance_crylink_secondary_speed 4000
 set g_balance_crylink_secondary_spread 0.08
 set g_balance_crylink_secondary_spreadtype 0
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0.2
 set g_balance_crylink_switchdelay_raise 0.2
 set g_balance_crylink_weaponreplace ""
index 17f5b31cab3b29b313e70f27606e6cf42bd7e082..e91d40b42e531892bbea48070e810c5f48dd98a9 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 7
 set g_balance_crylink_secondary_speed 7000
 set g_balance_crylink_secondary_spread 0.08
 set g_balance_crylink_secondary_spreadtype 0
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0.15
 set g_balance_crylink_switchdelay_raise 0.15
 set g_balance_crylink_weaponreplace ""
index 2799a2496b47fb4debea904aa6b051b95c59a621..e7e94f1e6ab3392e357e7469faa32c5d47d918a3 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 5
 set g_balance_crylink_secondary_speed 3000
 set g_balance_crylink_secondary_spread 0.01
 set g_balance_crylink_secondary_spreadtype 1
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0.2
 set g_balance_crylink_switchdelay_raise 0.2
 set g_balance_crylink_weaponreplace ""
index 09466acce81bcb3949c256133dce2a9675c89acc..ac85665d49fd9b3179c662d5ed2ed81fa4fd93dc 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 1
 set g_balance_crylink_secondary_speed 2000
 set g_balance_crylink_secondary_spread 0
 set g_balance_crylink_secondary_spreadtype 1
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0
 set g_balance_crylink_switchdelay_raise 0
 set g_balance_crylink_weaponreplace ""
index 319d77923035e150b23bf90fa1ca4321d5564e55..6b4c25b97ac310e3757bbab11b67c93047489940 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 5
 set g_balance_crylink_secondary_speed 3000
 set g_balance_crylink_secondary_spread 0.01
 set g_balance_crylink_secondary_spreadtype 1
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0.2
 set g_balance_crylink_switchdelay_raise 0.2
 set g_balance_crylink_weaponreplace ""
index c47d9611594a9b0daa6bd5a9a0a6e256412fef27..88de6990c5d514d6ab3cf07bcf0bca9ffa3edaf0 100644 (file)
@@ -154,6 +154,10 @@ string Label_getInfo(string label, int mode)
                case "revivals":     if (!mode) return CTX(_("SCO^revivals"));     else LOG_HELP(strcat("^3", "revivals", "           ^7", _("Number of revivals")));
                case "rounds":       if (!mode) return CTX(_("SCO^rounds won"));   else LOG_HELP(strcat("^3", "rounds", "             ^7", _("Number of rounds won")));
                case "score":        if (!mode) return CTX(_("SCO^score"));        else LOG_HELP(strcat("^3", "score", "              ^7", _("Total score")));
+               case "avgspeed":     if (!mode) return CTX(_("SCO^average speed"));else LOG_HELP(strcat("^3", "avgspeed", "           ^7", _("Average speed (CTS)")));
+               case "topspeed":     if (!mode) return CTX(_("SCO^top speed"));    else LOG_HELP(strcat("^3", "topspeed", "           ^7", _("Top speed (CTS)")));
+               case "startspeed":   if (!mode) return CTX(_("SCO^start speed"));  else LOG_HELP(strcat("^3", "startspeed", "         ^7", _("Start speed (CTS)")));
+               case "strafe":       if (!mode) return CTX(_("SCO^strafe"));       else LOG_HELP(strcat("^3", "strafe", "             ^7", _("Strafe efficiency (CTS)")));
                case "suicides":     if (!mode) return CTX(_("SCO^suicides"));     else LOG_HELP(strcat("^3", "suicides", "           ^7", _("Number of suicides")));
                case "sum":          if (!mode) return CTX(_("SCO^sum"));          else LOG_HELP(strcat("^3", "sum", "                ^7", _("Number of kills minus deaths")));
                case "takes":        if (!mode) return CTX(_("SCO^takes"));        else LOG_HELP(strcat("^3", "takes", "              ^7", _("Number of domination points taken (Domination)")));
@@ -728,7 +732,7 @@ void Cmd_Scoreboard_Help()
 " +ctf/pickups +ctf/fckills +ctf/returns +ctf/caps +ons/takes +ons/caps" \
 " +lms/lives +lms/rank" \
 " +kh/kckills +kh/losses +kh/caps" \
-" ?+rc/laps ?+rc/time +rc,cts/fastest" \
+" ?+rc/laps ?+rc/time ?+cts/strafe ?+cts/startspeed ?+cts/avgspeed ?+cts/topspeed +rc,cts/fastest" \
 " +as/objectives +nb/faults +nb/goals" \
 " +ka/pickups +ka/bckills +ka/bctime +ft/revivals" \
 " +dom/ticks +dom/takes" \
@@ -1070,6 +1074,23 @@ string Scoreboard_GetField(entity pl, PlayerScoreField field)
                case SP_DMG: case SP_DMGTAKEN:
                        return sprintf("%.1f k", pl.(scores(field)) / 1000);
 
+               case SP_CTS_STRAFE:
+               {
+                       float strafe_efficiency = pl.(scores(field)) / 1000;
+                       if(strafe_efficiency < -1) return "";
+                       sbt_field_rgb = '1 1 1' - (strafe_efficiency > 0 ? '1 0 1' : '0 1 1') * fabs(strafe_efficiency);
+                       return sprintf("%.1f%%", strafe_efficiency * 100);
+               }
+
+               case SP_CTS_STARTSPEED:
+               case SP_CTS_AVGSPEED:
+               case SP_CTS_TOPSPEED:
+               {
+                       float speed = pl.(scores(field)) * GetSpeedUnitFactor(autocvar_hud_panel_physics_speed_unit);
+                       if(speed < 0) return "";
+                       return sprintf("%d%s", speed, GetSpeedUnit(autocvar_hud_panel_physics_speed_unit));
+               }
+
                default: case SP_SCORE:
                        tmp = pl.(scores(field));
                        f = scores_flags(field);
index e4e339c44a53497c54e60485d00ce88b23468134..ec811bcc9615aa3ac23c423ccb4be4c81adc999b 100644 (file)
@@ -3,15 +3,32 @@
 #include "strafehud.qh"
 
 #include <client/draw.qh>
-#include <client/hud/panel/racetimer.qh>
-#include <client/view.qh>
-#include <common/animdecide.qh>
-#include <common/ent_cs.qh>
-#include <common/mapinfo.qh>
-#include <common/physics/movetypes/movetypes.qh>
-#include <common/physics/player.qh>
-#include <common/resources/cl_resources.qh>
 #include <lib/csqcmodel/cl_player.qh>
+#include <common/physics/player.qh>
+#include <common/physics/movetypes/movetypes.qh>
+
+// non-essential
+#include <client/view.qh> // for v_flipped state
+
+// non-local players
+#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
+
+AUTOCVAR_SAVE(hud_panel_strafehud_pos, string, "0.320000 0.570000", "position of this base of the panel");
+AUTOCVAR_SAVE(hud_panel_strafehud_size, string, "0.360000 0.020000", "size of this panel");
+AUTOCVAR_SAVE(hud_panel_strafehud_bg, string, "0", "if set to something else than \"\" = override default background");
+AUTOCVAR_SAVE(hud_panel_strafehud_bg_color, string, "", "if set to something else than \"\" = override default panel background color");
+AUTOCVAR_SAVE(hud_panel_strafehud_bg_color_team, string, "", "override panel color with team color in team based games");
+AUTOCVAR_SAVE(hud_panel_strafehud_bg_alpha, string, "0.7", "if set to something else than \"\" = override default panel background alpha");
+AUTOCVAR_SAVE(hud_panel_strafehud_bg_border, string, "", "if set to something else than \"\" = override default size of border around the background");
+AUTOCVAR_SAVE(hud_panel_strafehud_bg_padding, string, "", "if set to something else than \"\" = override default padding of contents from border");
 
 // StrafeHUD (#25)
 
@@ -20,46 +37,32 @@ void HUD_StrafeHUD_Export(int fh)
     // allow saving cvars that aesthetically change the panel into hud skin files
 }
 
-float hidden_width;
-int direction;
-float demo_angle = -37;
-float demo_direction = 1;
-float demo_time = 0;
-bool state_onground = false;
-float state_onground_time = 0;
-bool state_strafekeys = false;
-float state_strafekeys_time = 0;
-bool turn = false;
-float turnangle;
-bool fwd = true;
-bool state_fwd = true;
-bool state_fwd_prev = true;
-float state_fwd_time = 0;
-float starttime = 0;
-float startspeed = -1;
-
-// provide basic panel cvars to old clients
-// TODO remove them after a future release (0.8.2+)
-noref string autocvar_hud_panel_strafehud_pos = "0.320000 0.570000";
-noref string autocvar_hud_panel_strafehud_size = "0.360000 0.020000";
-noref string autocvar_hud_panel_strafehud_bg = "0";
-noref string autocvar_hud_panel_strafehud_bg_color = "";
-noref string autocvar_hud_panel_strafehud_bg_color_team = "";
-noref string autocvar_hud_panel_strafehud_bg_alpha = "0.7";
-noref string autocvar_hud_panel_strafehud_bg_border = "";
-noref string autocvar_hud_panel_strafehud_bg_padding = "";
+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;
 
+    if(false)
+    {
+        autocvar_hud_panel_strafehud_pos = autocvar_hud_panel_strafehud_pos;
+        autocvar_hud_panel_strafehud_size = autocvar_hud_panel_strafehud_size;
+        autocvar_hud_panel_strafehud_bg = autocvar_hud_panel_strafehud_bg;
+        autocvar_hud_panel_strafehud_bg_color = autocvar_hud_panel_strafehud_bg_color;
+        autocvar_hud_panel_strafehud_bg_color_team = autocvar_hud_panel_strafehud_bg_color_team;
+        autocvar_hud_panel_strafehud_bg_alpha = autocvar_hud_panel_strafehud_bg_alpha;
+        autocvar_hud_panel_strafehud_bg_border = autocvar_hud_panel_strafehud_bg_border;
+        autocvar_hud_panel_strafehud_bg_padding = autocvar_hud_panel_strafehud_bg_padding;
+    }
+
     // 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))) return;
+           (autocvar_hud_panel_strafehud == 3 && !MUTATOR_CALLHOOK(HUD_StrafeHUD_showoptional))) { hud_lasttime = time; return; }
     }
 
     HUD_Panel_LoadCvars();
@@ -96,44 +99,85 @@ void HUD_StrafeHUD()
     // draw strafehud
     if(csqcplayer && strafeplayer)
     {
+        float strafe_waterlevel;
+
+        // check the player waterlevel without affecting the player entity, this way we can fetch waterlevel even if client prediction is disabled
+        {
+            // store old values
+            void old_contentstransition(int, int) = strafeplayer.contentstransition;
+            float old_watertype = strafeplayer.watertype;
+            float old_waterlevel = strafeplayer.waterlevel;
+
+            strafeplayer.contentstransition = func_null; // unset the contentstransition function if present
+            _Movetype_CheckWater(strafeplayer);
+            strafe_waterlevel = strafeplayer.waterlevel; // store the player waterlevel
+
+            // restore old values
+            strafeplayer.contentstransition = old_contentstransition;
+            strafeplayer.watertype = old_watertype;
+            strafeplayer.waterlevel = old_waterlevel;
+        }
+
+        // persistent
+        static float onground_lasttime       = 0;
+        static bool  onslick_last            = false;
+        static float turn_lasttime           = 0;
+        static bool  turn                    = false;
+        static float turnangle;
+        static float dt_update               = 0;
+        static int   dt_time                 = 0;
+        static float dt_sum                  = 0;
+        static float dt                      = 0;
+
         // physics
-        bool   onground                      = islocal ? IS_ONGROUND(strafeplayer) : !(strafeplayer.anim_implicit_state & ANIMIMPLICITSTATE_INAIR);
+        int    keys                          = STAT(PRESSED_KEYS);
+        bool   jumpheld                      = (islocal ? ((PHYS_INPUT_BUTTON_JUMP(strafeplayer) || PHYS_INPUT_BUTTON_JETPACK(strafeplayer))) && !PHYS_CL_TRACK_CANJUMP(strafeplayer) : (keys & KEY_JUMP)) && !PHYS_TRACK_CANJUMP(strafeplayer); // try to ignore if track_canjump is enabled, doesn't work in spectator mode if spectated player uses +jetpack or cl_movement_track_canjump
+        bool   real_onground                 = islocal ? IS_ONGROUND(strafeplayer) : !(strafeplayer.anim_implicit_state & ANIMIMPLICITSTATE_INAIR); // doesn't get changed by ground timeout and isn't affected by jump input
+        bool   real_onslick                  = false; // doesn't get changed by ground timeout
+        bool   onground                      = real_onground && !jumpheld; // if jump is held assume we are in air, avoids flickering of the hud when hitting the ground
+        bool   onslick                       = real_onslick;
+        bool   onground_expired;
         bool   strafekeys;
-        bool   swimming                      = strafeplayer.waterlevel >= WATERLEVEL_SWIMMING;
-        bool   spectating                    = entcs_GetSpecState(strafeplayer.sv_entnum) == ENTCS_SPEC_PURE;
+        bool   swimming                      = strafe_waterlevel >= WATERLEVEL_SWIMMING; // the hud will not work well while swimming
         float  speed                         = !autocvar__hud_configure ? vlen(vec2(csqcplayer.velocity)) : 1337; // use local csqcmodel entity for this even when spectating, flickers too much otherwise
-        float  maxspeed_crouch_mod           = IS_DUCKED(strafeplayer) && !swimming ? .5 : 1;
-        float  maxspeed_water_mod            = swimming ? .7 : 1; // very simplified water physics, the hud will not work well (and is not supposed to) while swimming
+        float  maxspeed_mod                  = IS_DUCKED(csqcplayer) ? .5 : 1; // only the local csqcplayer entity contains this information even when spectating
         float  maxspeed_phys                 = onground ? PHYS_MAXSPEED(strafeplayer) : PHYS_MAXAIRSPEED(strafeplayer);
-        float  maxspeed                      = !autocvar__hud_configure ? maxspeed_phys * maxspeed_crouch_mod * maxspeed_water_mod : 320;
-        float  vel_angle                     = vectoangles(strafeplayer.velocity).y;
-        float  view_angle                    = PHYS_INPUT_ANGLES(strafeplayer).y + 180;
+        float  maxspeed                      = !autocvar__hud_configure ? maxspeed_phys * maxspeed_mod : 320;
+        float  movespeed;
+        float  bestspeed;
+        float  maxaccel_phys                 = onground ? PHYS_ACCELERATE(strafeplayer) : PHYS_AIRACCELERATE(strafeplayer);
+        float  maxaccel                      = !autocvar__hud_configure ? maxaccel_phys : 1;
+        float  vel_angle                     = vectoangles(strafeplayer.velocity).y - (vectoangles(strafeplayer.velocity).y > 180 ? 360 : 0); // change the range from 0° - 360° to -180° - 180° to match how view_angle represents angles
+        float  view_angle                    = PHYS_INPUT_ANGLES(strafeplayer).y;
         float  angle;
         vector movement                      = PHYS_INPUT_MOVEVALUES(strafeplayer);
-        int    keys                          = STAT(PRESSED_KEYS);
+        bool   fwd;
         int    keys_fwd;
-        float  wishangle                     = 0;
+        float  wishangle;
+        int    direction;
 
         // HUD
-        int    mode                          = autocvar_hud_panel_strafehud_mode >= 0 && autocvar_hud_panel_strafehud_mode <= 1 ? autocvar_hud_panel_strafehud_mode : 0;
+        int    mode                          = autocvar_hud_panel_strafehud_mode >= 0 && autocvar_hud_panel_strafehud_mode <= 1 ? autocvar_hud_panel_strafehud_mode : STRAFEHUD_MODE_VIEW_CENTERED;
         float  speed_conversion_factor       = GetSpeedUnitFactor(autocvar_hud_panel_strafehud_unit);
         float  length_conversion_factor      = GetLengthUnitFactor(autocvar_hud_panel_strafehud_unit);
         int    length_decimals               = autocvar_hud_panel_strafehud_unit >= 3 && autocvar_hud_panel_strafehud_unit <= 5 ? 6 : 2; // use more decimals when displaying km or miles
         float  antiflicker_angle             = bound(0, autocvar_hud_panel_strafehud_antiflicker_angle, 180);
-        float  antiflicker_speed             = max(0, autocvar_hud_panel_strafehud_antiflicker_speed);
         float  minspeed;
         float  shift_offset                  = 0;
         bool   straight_overturn             = false;
-        bool   immobile                      = speed <= (swimming ? antiflicker_speed : 0);
+        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             = '0 0 0';
+        vector currentangle_size;
+        float  real_bestangle; // positive with no wishangle offset
+        float  real_prebestangle;  // positive with no wishangle offset
         float  bestangle;
+        float  prebestangle;
         float  odd_bestangle;
-        bool   bestangle_anywhere            = false;
         float  bestangle_offset;
         float  switch_bestangle_offset;
         bool   odd_angles                    = false;
@@ -143,42 +187,115 @@ void HUD_StrafeHUD()
         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';
+        vector direction_size_vertical;
+        vector direction_size_horizontal;
         float  range_minangle;
+        float  arrow_size = max(panel_size.y * min(autocvar_hud_panel_strafehud_angle_arrow_size, 10), 0); // there's only one size cvar for the arrows, they will always have a 45° angle to ensure proper rendering without antialiasing
+        float  text_offset_top = 0;
+        float  text_offset_bottom = 0;
+        float  hfov                     = getproperty(VF_FOVX);
+
+        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;
+
+            onground_lasttime = time;
+            onslick_last = onslick;
+        }
+        else if(jumpheld || swimming) onground_lasttime = 0;
+
+        if(onground_lasttime == 0)
+            onground_expired = true;
+        else
+            onground_expired = (time - onground_lasttime) >= autocvar_hud_panel_strafehud_timeout_ground; // timeout for slick ramps
+
+        if(!onground && !onground_expired) // if ground timeout hasn't expired yet use ground physics
+        {
+            onground = true;
+            onslick = onslick_last;
+
+            if(!autocvar__hud_configure)
+            {
+                maxspeed = PHYS_MAXSPEED(strafeplayer) * maxspeed_mod;
+                maxaccel = PHYS_ACCELERATE(strafeplayer);
+            }
+        }
+
+        movespeed = vlen(vec2(movement));
+        if(movespeed == 0) movespeed = maxspeed;
+        else movespeed = min(movespeed, maxspeed);
+
+        if(!autocvar_hud_panel_strafehud_uncapped)
+            arrow_size = max(arrow_size, 1);
+
+        // determine frametime
+        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
+                dt_client /= 2; // doesn't ensure frames are smaller than 50 ms, just splits large frames in half, matches server behaviour
+
+            // calculate average frametime
+            dt_sum += dt_client*dt_client;
+            dt_time += dt_client;
+
+            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;
+        }
 
         // determine whether the player is pressing forwards or backwards keys
         if(islocal) // if entity is local player
         {
             if(movement.x > 0)
             {
-                keys_fwd = 1;
+                keys_fwd = STRAFEHUD_KEYS_FORWARD;
             }
             else if(movement.x < 0)
             {
-                keys_fwd = -1;
+                keys_fwd = STRAFEHUD_KEYS_BACKWARD;
             }
             else
             {
-                keys_fwd = 0;
+                keys_fwd = STRAFEHUD_KEYS_NONE;
             }
         }
         else // alternatively determine direction by querying pressed keys
         {
             if((keys & KEY_FORWARD) && !(keys & KEY_BACKWARD))
             {
-                keys_fwd = 1;
+                keys_fwd = STRAFEHUD_KEYS_FORWARD;
             }
             else if(!(keys & KEY_FORWARD) && (keys & KEY_BACKWARD))
             {
-                keys_fwd = -1;
+                keys_fwd = STRAFEHUD_KEYS_BACKWARD;
             }
             else
             {
-                keys_fwd = 0;
+                keys_fwd = STRAFEHUD_KEYS_NONE;
             }
         }
 
@@ -239,7 +356,7 @@ void HUD_StrafeHUD()
             }
         }
 
-        strafekeys = fabs(wishangle) == 90;
+        strafekeys = fabs(wishangle) > 45;
 
         // determine minimum required angle to display full strafe range
         range_minangle = fabs(wishangle) % 90; // maximum range is 90 degree
@@ -261,57 +378,88 @@ void HUD_StrafeHUD()
                 hudangle = range_minangle; // use minimum angle required if dynamically setting hud angle
             }
         }
-        else
+        else if(autocvar_hud_panel_strafehud_range < 0)
         {
-            hudangle = bound(0, fabs(autocvar_hud_panel_strafehud_range), 360); // limit HUD range to 360 degrees, higher values don't make sense
+            hudangle = hfov;
         }
-
-        // detect air strafe turning
-        if(onground != state_onground)
+        else
         {
-            state_onground_time = time;
+            hudangle = bound(0, fabs(autocvar_hud_panel_strafehud_range), 360); // limit HUD range to 360 degrees, higher values don't make sense
         }
-        state_onground = onground;
 
-        if(strafekeys != state_strafekeys)
-        {
-            state_strafekeys_time = time;
+        // limit strafe-meter angle to values suitable for the current projection mode
+        switch(autocvar_hud_panel_strafehud_projection) {
+            case STRAFEHUD_PROJECTION_PERSPECTIVE:
+                hudangle = min(hudangle, 170);
+                break;
+            case STRAFEHUD_PROJECTION_PANORAMIC:
+                hudangle = min(hudangle, 350);
+                break;
         }
-        state_strafekeys = strafekeys;
 
-        if((keys & KEY_FORWARD) || (keys & KEY_BACKWARD) || swimming || autocvar__hud_configure)
+        // detect air strafe turning
+        if((!strafekeys && vlen(vec2(movement)) > 0) || onground || autocvar__hud_configure)
         {
             turn = false;
         }
-        else if(onground)
-        {
-            if((time - state_onground_time) >= autocvar_hud_panel_strafehud_timeout_ground) // timeout for strafe jumping in general
-            {
-                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(strafekeys)
+                turn = true;
+            else if(turn_expired)
+                turn = false;
+
+            if(turn) // CPMA turning
             {
-                if(((time - state_onground_time) >= autocvar_hud_panel_strafehud_timeout_air) || (keys & KEY_JUMP)) // timeout for slick ramps
+                if(strafekeys)
                 {
-                    turn = true; // CPMA turning
+                    turn_lasttime = time;
                     turnangle = wishangle;
                 }
-            }
-            else if((time - state_strafekeys_time) >= autocvar_hud_panel_strafehud_timeout_turn) // timeout for jumping with strafe keys only
-            {
-                turn = false;
+                else // retain last state until strafe turning times out
+                {
+                    wishangle = turnangle;
+                }
+
+                // calculate the maximum air strafe speed and acceleration
+                float 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));
+                }
             }
         }
-        if(turn)
+
+        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);
+
+            frictionspeed = speed * dt * strafefriction * max(PHYS_STOPSPEED(strafeplayer) / speed, 1);
+            strafespeed = max(speed - frictionspeed, 0);
+        }
+        else
         {
-            maxspeed = PHYS_MAXAIRSTRAFESPEED(strafeplayer); // no modifiers here because they don't affect air strafing
-            wishangle = turnangle;
+            frictionspeed = 0;
+            strafespeed = speed;
         }
 
-        minspeed = autocvar_hud_panel_strafehud_switch_minspeed < 0 ? maxspeed + antiflicker_speed : autocvar_hud_panel_strafehud_switch_minspeed;
+        minspeed = autocvar_hud_panel_strafehud_switch_minspeed;
+        if(minspeed < 0) minspeed = bestspeed + frictionspeed;
 
         // get current strafing angle ranging from -180° to +180°
         if(!autocvar__hud_configure)
@@ -321,29 +469,25 @@ void HUD_StrafeHUD()
                 // 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
+                // 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;
 
-                // shift the strafe angle by 180° for hud calculations
-                if(angle < 0) angle += 180;
-                else angle -= 180;
-
                 // determine whether the player is strafing forwards or backwards
                 // if the player isn't strafe turning use forwards/backwards keys to determine direction
-                if(!strafekeys)
+                if(fabs(wishangle) != 90)
                 {
-                    if(keys_fwd > 0)
+                    if(keys_fwd == STRAFEHUD_KEYS_FORWARD)
                     {
-                        state_fwd = true;
+                        fwd = true;
                     }
-                    else if(keys_fwd < 0)
+                    else if(keys_fwd == STRAFEHUD_KEYS_BACKWARD)
                     {
-                        state_fwd = false;
+                        fwd = false;
                     }
                     else
                     {
-                        state_fwd = fabs(angle) <= 90;
+                        fwd = fabs(angle) <= 90;
                     }
                 }
                 // otherwise determine by examining the strafe angle
@@ -351,25 +495,14 @@ void HUD_StrafeHUD()
                 {
                     if(wishangle < 0) // detect direction using wishangle since the direction is not yet set
                     {
-                        state_fwd = angle <= -wishangle;
+                        fwd = angle <= -wishangle;
                     }
                     else
                     {
-                        state_fwd = angle >= -wishangle;
+                        fwd = angle >= -wishangle;
                     }
                 }
 
-                if(state_fwd_prev != state_fwd)
-                {
-                    state_fwd_time = time;
-                }
-                state_fwd_prev = state_fwd;
-
-                if((time - state_fwd_time) >= autocvar_hud_panel_strafehud_timeout_direction || speed < maxspeed || (strafekeys && mode == 0)) // timeout when changing between forwards and backwards movement
-                {
-                    fwd = state_fwd;
-                }
-
                 // shift the strafe angle by 180° when strafing backwards
                 if(!fwd)
                 {
@@ -386,22 +519,35 @@ void HUD_StrafeHUD()
             else
             {
                 angle = 0;
+                fwd = true;
             }
         }
         else // simulate turning for HUD setup
         {
-            fwd = true;
-            if(autocvar__hud_panel_strafehud_demo && ((time - demo_time) >= .025))
+            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
+
+            if(autocvar__hud_panel_strafehud_demo)
             {
-                demo_time = time;
-                demo_angle += demo_direction;
-                if(fabs(demo_angle) >= 55)
-                {
-                    demo_direction = -demo_direction;
-                }
+                float demo_dt = time - hud_lasttime;
+                float demo_step = (demo_turnspeed / demo_maxangle) * demo_dt;
+                demo_position = ((demo_position + demo_step) % 4 + 4) % 4;
             }
-            angle = demo_angle;
-            wishangle = 45 * (demo_angle > 0 ? 1 : -1);
+
+            // 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;
+
+            fwd = true;
+            wishangle = 45;
+            if(angle < 0) wishangle *= -1;
         }
 
         // invert the wish angle when strafing backwards
@@ -418,26 +564,61 @@ void HUD_StrafeHUD()
         }
 
         // determine whether the player is strafing left or right
-        if(wishangle != 0)
+        if(wishangle > 0)
         {
-            direction = wishangle > 0 ? 1 : -1;
+            direction = STRAFEHUD_DIRECTION_RIGHT;
+        }
+        else if(wishangle < 0)
+        {
+            direction = STRAFEHUD_DIRECTION_LEFT;
         }
         else
         {
-            direction = (angle > antiflicker_angle && angle < (180 - antiflicker_angle)) ? 1 : (angle < -antiflicker_angle && angle > (-180 + antiflicker_angle)) ? -1 : 0;
+            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;
         }
 
         // best angle to strafe at
-        bestangle = (speed > maxspeed ? acos(maxspeed / speed) : 0) * RAD2DEG * (direction < 0 ? -1 : 1);
+        // in case of ground friction we may decelerate if the acceleration is smaller than the speed loss from friction
+        real_bestangle = bestangle = (strafespeed > bestspeed ? acos(bestspeed / strafespeed) * RAD2DEG : 0);
+        real_prebestangle = prebestangle = (strafespeed > movespeed ? acos(movespeed / strafespeed) * RAD2DEG : 0);
+        if(direction == STRAFEHUD_DIRECTION_LEFT) // the angle becomes negative in case we strafe left
+        {
+            bestangle *= -1;
+            prebestangle *= -1;
+        }
         odd_bestangle = -bestangle - wishangle;
         bestangle -= wishangle;
+        prebestangle -= 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 = max(panel_size.x * autocvar_hud_panel_strafehud_angle_width, 1);
-        if(mode == 0)
+        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;
         }
@@ -445,25 +626,31 @@ void HUD_StrafeHUD()
         {
             currentangle_offset = bound(-hudangle/2, angle, hudangle/2)/hudangle * panel_size.x + panel_size.x/2;
         }
-        currentangle_size.y = max(panel_size.y * min(autocvar_hud_panel_strafehud_angle_height, 2), 1);
         // best strafe acceleration angle
         bestangle_offset        =  bestangle/hudangle * panel_size.x + panel_size.x/2;
         switch_bestangle_offset = -bestangle/hudangle * panel_size.x + panel_size.x/2;
-        bestangle_width = max(panel_size.x * autocvar_hud_panel_strafehud_switch_width, 1);
+        bestangle_width = panel_size.x * autocvar_hud_panel_strafehud_switch_width;
+        if(!autocvar_hud_panel_strafehud_uncapped)
+            bestangle_width = max(bestangle_width, 1);
 
-        if(((angle > -wishangle && direction < 0) || (angle < -wishangle && direction > 0)) && (direction != 0))
+        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;
         }
         // direction indicator
-        direction_size_vertical.x = max(panel_size.y * min(autocvar_hud_panel_strafehud_direction_width, .5), 1);
-        direction_size_vertical.y = panel_size.y;
-        direction_size_horizontal.x = max(panel_size.x * min(autocvar_hud_panel_strafehud_direction_length, .5), direction_size_vertical.x);
+        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;
-        // overturn
-        overturn_width = 180/hudangle * panel_size.x;
+        direction_size_horizontal.z = 0;
 
         // the neutral zone fills the whole strafe bar
         if(immobile)
@@ -474,11 +661,11 @@ void HUD_StrafeHUD()
                 switch(autocvar_hud_panel_strafehud_style)
                 {
                     default:
-                    case 0:
+                    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 1:
+                    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);
                 }
             }
@@ -486,26 +673,36 @@ void HUD_StrafeHUD()
         else
         {
             // calculate various zones of the strafe-o-meter
-            accelzone_width = overturn_offset = (90 - fabs(bestangle + wishangle))/hudangle * panel_size.x;
-            accelzone_right_offset = 0;
-            accelzone_left_offset = overturn_offset + overturn_width;
-            neutral_width = 360/hudangle * panel_size.x - accelzone_width*2 - overturn_width;
-            neutral_offset = direction < 0 ? accelzone_left_offset + accelzone_width : -neutral_width;
-
-            // remove switch indicator width from offset
-            if(direction < 0)
-            {
-                bestangle_offset -= bestangle_width;
-                switch_odd_bestangle_offset -= bestangle_width;
-            }
+            if(autocvar_hud_panel_strafehud_bar_preaccel)
+                preaccelzone_width = (fabs(bestangle - prebestangle))/hudangle * panel_size.x;
             else
+                preaccelzone_width = 0;
+            accelzone_width = (90 - fabs(bestangle + wishangle))/hudangle * panel_size.x;
+            overturn_width = 180/hudangle * panel_size.x;
+            neutral_width = 360/hudangle * panel_size.x - accelzone_width*2 - preaccelzone_width*2 - overturn_width;
+
             {
-                switch_bestangle_offset -= bestangle_width;
-                odd_bestangle_offset -= bestangle_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;
+
+                preaccelzone_left_offset = current_offset;
+                current_offset += preaccelzone_width;
+
+                neutral_offset = direction == STRAFEHUD_DIRECTION_LEFT ? current_offset : -neutral_width; // the wrapping code may struggle if we always append it on the right side
             }
 
             // shift hud if operating in view angle centered mode
-            if(mode == 0)
+            if(mode == STRAFEHUD_MODE_VIEW_CENTERED)
             {
                 shift_offset = -currentangle_offset;
                 bestangle_offset += shift_offset;
@@ -513,92 +710,97 @@ void HUD_StrafeHUD()
                 odd_bestangle_offset += shift_offset;
                 switch_odd_bestangle_offset += shift_offset;
             }
-            if(direction < 0) shift_offset += -360/hudangle * panel_size.x;
+            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
-            HUD_Panel_DrawStrafeHUD(accelzone_left_offset, accelzone_width, autocvar_hud_panel_strafehud_bar_accel_color, autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha, autocvar_hud_panel_strafehud_style, 1);
+            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, true, hudangle);
+            if(autocvar_hud_panel_strafehud_bar_preaccel)
+                HUD_Panel_DrawStrafeHUD(preaccelzone_left_offset, preaccelzone_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, true, hudangle);
 
             // draw right acceleration zone
-            HUD_Panel_DrawStrafeHUD(accelzone_right_offset, accelzone_width, autocvar_hud_panel_strafehud_bar_accel_color, autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha, autocvar_hud_panel_strafehud_style, 2);
+            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, true, hudangle);
+            if(autocvar_hud_panel_strafehud_bar_preaccel)
+                HUD_Panel_DrawStrafeHUD(preaccelzone_right_offset, preaccelzone_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, true, hudangle);
 
-            // draw overturn zone
-            HUD_Panel_DrawStrafeHUD(overturn_offset, overturn_width, autocvar_hud_panel_strafehud_bar_overturn_color, autocvar_hud_panel_strafehud_bar_overturn_alpha * panel_fg_alpha, autocvar_hud_panel_strafehud_style, 3);
+            // draw overturn zone (technically incorrect, acceleration decreases at 90 degrees but speed loss happens a little bit after 90 degrees, however due to sv_airstopaccelerate that's hard to calculate)
+            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, true, hudangle);
 
             // draw neutral zone
-            HUD_Panel_DrawStrafeHUD(neutral_offset, neutral_width, autocvar_hud_panel_strafehud_bar_neutral_color, autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha, autocvar_hud_panel_strafehud_style, 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, true, hudangle);
 
-            if(direction != 0 && direction_size_vertical.x > 0 && autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha > 0)
+            if(autocvar_hud_panel_strafehud_switch && speed >= minspeed && bestangle_width > 0 && autocvar_hud_panel_strafehud_switch_alpha > 0) // only draw indicators if minspeed is reached
             {
-                bool indicator_direction = direction < 0;
-                // 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)
+                // draw the switch indicator(s)
+                float offset = !odd_angles ? bestangle_offset : odd_bestangle_offset;
+                float switch_offset = !odd_angles ? switch_bestangle_offset : switch_odd_bestangle_offset;
+
+                offset = StrafeHUD_projectOffset(offset, hudangle);
+                switch_offset = StrafeHUD_projectOffset(switch_offset, hudangle);
+
+                // remove switch indicator width from offset
+                if(direction == STRAFEHUD_DIRECTION_LEFT)
                 {
-                    indicator_direction = !indicator_direction;
+                    if(!odd_angles)
+                        offset -= bestangle_width;
+                    else
+                        switch_offset -= bestangle_width;
+                }
+                else
+                {
+                    if(!odd_angles)
+                        switch_offset -= bestangle_width;
+                    else
+                        offset -= bestangle_width;
                 }
-                // draw the direction indicator caps at the sides of the hud
-                // vertical line
-                if(direction_size_vertical.y > 0) drawfill(panel_pos + 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 ? -direction_size_vertical.x : panel_size.x - direction_size_horizontal.x + direction_size_vertical.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 ? -direction_size_vertical.x : panel_size.x - direction_size_horizontal.x + direction_size_vertical.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);
-            }
 
-            if(speed >= minspeed) // only draw indicators if minspeed is reached
-            {
-                // draw best angles for acceleration
-                float offset = !odd_angles ? bestangle_offset : odd_bestangle_offset;
-                float switch_offset = !odd_angles ? switch_bestangle_offset : switch_odd_bestangle_offset;
-                // both indicators are inactive if no direction can be determined
-                vector switch_color = direction != 0 ? autocvar_hud_panel_strafehud_switch_active_color : autocvar_hud_panel_strafehud_switch_inactive_color;
-                float switch_alpha = direction != 0 ? autocvar_hud_panel_strafehud_switch_active_alpha : autocvar_hud_panel_strafehud_switch_inactive_alpha;
-                // draw the switch indicators
-                HUD_Panel_DrawStrafeHUD(switch_offset, bestangle_width, autocvar_hud_panel_strafehud_switch_inactive_color, autocvar_hud_panel_strafehud_switch_inactive_alpha * panel_fg_alpha, 0, 0);
-                HUD_Panel_DrawStrafeHUD(offset, bestangle_width, switch_color, switch_alpha * panel_fg_alpha, 0, 0);
+                HUD_Panel_DrawStrafeHUD(switch_offset, 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, false, hudangle);
+                if(direction == STRAFEHUD_DIRECTION_NONE) HUD_Panel_DrawStrafeHUD(offset, 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, false, hudangle);
             }
         }
 
-        // experimental: slick detector
-        slickdetector_height = panel_size.y * bound(0, autocvar_hud_panel_strafehud_slickdetector_height, 0.5);
-        if(autocvar_hud_panel_strafehud_slickdetector_range > 0 && autocvar_hud_panel_strafehud_slickdetector_alpha > 0 && slickdetector_height > 0 && panel_size.x > 0) // dunno if slick detection works in spectate
+        // 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 = 90 / pow(2, bound(0, autocvar_hud_panel_strafehud_slickdetector_granularity, 4));
+            float slicksteps = max(autocvar_hud_panel_strafehud_slickdetector_granularity, 0);
             bool slickdetected = false;
 
-            slickdetected = IS_ONSLICK(strafeplayer); // don't need to traceline if already touching slick
+            if(!autocvar_hud_panel_strafehud_uncapped)
+                slicksteps = min(slicksteps, 4);
+            slicksteps = 90 / 2 ** slicksteps;
+
+            slickdetected = real_onslick; // don't need to traceline if already touching slick
 
             // traceline into every direction
             trace_dphitq3surfaceflags = 0;
-            for(float i = 0; i < 360 && !slickdetected; i += slicksteps)
+            vector traceorigin = strafeplayer.origin + eZ * strafeplayer.mins.z;
+            for(float i = 0; i < 90 && !slickdetected; i += slicksteps)
             {
                 vector slickoffset;
                 float slickrotate;
                 slickoffset.z = -cos(i * DEG2RAD) * autocvar_hud_panel_strafehud_slickdetector_range;
                 slickrotate = sin(i * DEG2RAD) * autocvar_hud_panel_strafehud_slickdetector_range;
-                if(i != 0 && i != 180)
-                {
-                    for(float j = 0; j < 180 && !slickdetected; j += slicksteps)
-                    {
-                        slickoffset.x = sin(j * DEG2RAD) * slickrotate;
-                        slickoffset.y = cos(j * DEG2RAD) * slickrotate;
 
-                        traceline(strafeplayer.origin, strafeplayer.origin + slickoffset, MOVE_WORLDONLY, NULL);
-                        if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_SLICK) slickdetected = true;
-                    }
-                }
-                else
+                for(float j = 0; j < 360 && !slickdetected; j += slicksteps)
                 {
-                    slickoffset.x = slickoffset.y = 0;
-                    traceline(strafeplayer.origin, strafeplayer.origin + slickoffset, MOVE_WORLDONLY, NULL);
-                    if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_SLICK) slickdetected = true;
+                    slickoffset.x = sin(j * DEG2RAD) * slickrotate;
+                    slickoffset.y = cos(j * DEG2RAD) * 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;
                 }
             }
 
@@ -611,153 +813,295 @@ void HUD_StrafeHUD()
                 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);
+
+                text_offset_top = text_offset_bottom = slickdetector_height;
             }
         }
 
-        draw_beginBoldFont();
-        // show speed when crossing the start trigger
-        if(autocvar_hud_panel_strafehud_startspeed_fade > 0)
+        if(autocvar_hud_panel_strafehud_direction && direction != STRAFEHUD_DIRECTION_NONE && direction_size_vertical.x > 0 && autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha > 0)
         {
-            float text_alpha = 0;
-            if(race_checkpoint == 254) // checkpoint 254 is the start trigger
+            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)
             {
-                if(starttime != race_checkpointtime)
-                {
-                    starttime = race_checkpointtime;
-                    startspeed = speed;
-                }
-            }
-            if(startspeed >= 0)
-            {
-                text_alpha = cos(((time - starttime) / autocvar_hud_panel_strafehud_startspeed_fade) * 90 * DEG2RAD); // fade non-linear like the physics panel does
-                if((time - starttime) > autocvar_hud_panel_strafehud_startspeed_fade)
-                {
-                    startspeed = -1;
-                }
-            }
-            if(startspeed >= 0 && text_alpha > 0 && autocvar_hud_panel_strafehud_startspeed_size > 0)
-            {
-                vector startspeed_size = panel_size;
-                startspeed_size.y = panel_size.y * min(autocvar_hud_panel_strafehud_startspeed_size, 5);
-                string speed_unit = GetSpeedUnit(autocvar_hud_panel_strafehud_unit);
-                drawstring_aspect(panel_pos + eY * panel_size.y, strcat(ftos_decimals(startspeed * speed_conversion_factor, 2), autocvar_hud_panel_strafehud_unit_show ? speed_unit : ""), startspeed_size, autocvar_hud_panel_strafehud_startspeed_color, text_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+                indicator_direction = !indicator_direction;
             }
+            // draw the direction indicator caps at the sides of the hud
+            // vertical line
+            if(direction_size_vertical.y > 0) drawfill(panel_pos - eY * direction_size_horizontal.y + eX * (indicator_direction ? -direction_size_vertical.x : panel_size.x), direction_size_vertical, autocvar_hud_panel_strafehud_direction_color, autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+            // top horizontal line
+            drawfill(panel_pos + eX * (indicator_direction ? 0 : panel_size.x - direction_size_horizontal.x) - eY * direction_size_horizontal.y, direction_size_horizontal, autocvar_hud_panel_strafehud_direction_color, autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+            // bottom horizontal line
+            drawfill(panel_pos + eX * (indicator_direction ? 0 : panel_size.x - direction_size_horizontal.x) + eY * panel_size.y, direction_size_horizontal, autocvar_hud_panel_strafehud_direction_color, autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
         }
-        else
-        {
-            starttime = 0;
-            startspeed = -1;
+
+        string newsound = autocvar_hud_panel_strafehud_sonar_audio;
+        static string cursound = string_null;
+        static string sonarsound = string_null;
+        if(newsound == "") {
+            cursound = sonarsound = string_null;
+        }
+        else if(newsound != cursound) {
+            strfree(cursound);
+            cursound = strzone(newsound);
+
+            strfree(sonarsound);
+            sonarsound = _Sound_fixpath(newsound);
+            if(sonarsound) {
+                sonarsound = strzone(sonarsound);
+                precache_sound(sonarsound);
+            }
         }
 
-        // show height achieved by a single jump
-        if(autocvar_hud_panel_strafehud_jumpheight_fade > 0 && autocvar_hud_panel_strafehud_jumpheight_size > 0)
-        {
-            static float height_min = 0, height_max = 0; // ground and peak of jump z coordinates
-            static float jumpheight = 0, jumptime = 0;   // displayed value and timestamp for fade out
+        // draw the actual strafe angle
+        if(!immobile) {
+            float moveangle = fabs(angle + wishangle);
+            float strafe_ratio = 0;
 
-            // tries to catch kill and spectate but those are not reliable, should just hook to kill/spectate/teleport and reset jump height there
-            if((strafeplayer.velocity.z <= 0 && height_max >= strafeplayer.origin.z) || IS_ONGROUND(strafeplayer) || swimming || IS_DEAD(strafeplayer) || spectating)
+            // player is overturning
+            if(moveangle >= 90)
             {
-                height_min = height_max = strafeplayer.origin.z;
+                currentangle_color = autocvar_hud_panel_strafehud_angle_overturn_color;
+                strafe_ratio = (moveangle - 90) / 90;
+                if(strafe_ratio > 1) strafe_ratio = 2 - strafe_ratio;
+                strafe_ratio *= -1;
             }
-            else if(strafeplayer.origin.z > height_max)
+            // player gains speed by strafing
+            else if(moveangle >= real_bestangle)
             {
-                height_max = strafeplayer.origin.z;
-                jumpheight = (height_max - height_min) * length_conversion_factor;
-
-                if(jumpheight > max(autocvar_hud_panel_strafehud_jumpheight_min, 0))
-                    jumptime = time;
+                currentangle_color = autocvar_hud_panel_strafehud_angle_accel_color;
+                strafe_ratio = (90 - moveangle) / (90 - real_bestangle);
+            }
+            else if(moveangle >= real_prebestangle)
+            {
+                if(autocvar_hud_panel_strafehud_bar_preaccel)
+                    currentangle_color = autocvar_hud_panel_strafehud_angle_accel_color;
+                strafe_ratio = (moveangle - real_prebestangle) / (real_bestangle - real_prebestangle);
             }
 
-            if((time - jumptime) <= autocvar_hud_panel_strafehud_jumpheight_fade)
+            if(autocvar_hud_panel_strafehud_style == STRAFEHUD_STYLE_GRADIENT)
             {
-                float text_alpha = cos(((time - jumptime) / autocvar_hud_panel_strafehud_jumpheight_fade) * 90 * DEG2RAD); // fade non-linear like the physics panel does
-                vector jumpheight_size = panel_size;
-                jumpheight_size.y = panel_size.y * min(autocvar_hud_panel_strafehud_jumpheight_size, 5);
-                string length_unit = GetLengthUnit(autocvar_hud_panel_strafehud_unit);
-                drawstring_aspect(panel_pos - eY * jumpheight_size.y, strcat(ftos_decimals(jumpheight, length_decimals), autocvar_hud_panel_strafehud_unit_show ? length_unit : ""), jumpheight_size, autocvar_hud_panel_strafehud_jumpheight_color, text_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+                currentangle_color = StrafeHUD_mixColors(autocvar_hud_panel_strafehud_angle_neutral_color, currentangle_color, fabs(strafe_ratio));
             }
-        }
-        draw_endBoldFont();
 
-        if(speed < (maxspeed + antiflicker_speed) && !immobile)
-        {
-            bestangle_anywhere = true; // moving forward should suffice to gain speed
-        }
+            // reuse strafe ratio for strafe sonar
+            static float sonar_time = 0;
 
-        // draw the actual strafe angle
-        if(!bestangle_anywhere && !immobile) // player gains speed with strafing
-        {
-            if((direction > 0 && (angle >= bestangle || angle <= -(bestangle + wishangle*2))) ||
-               (direction < 0 && (angle <= bestangle || angle >= -(bestangle + wishangle*2))))
-            currentangle_color = autocvar_hud_panel_strafehud_angle_accel_color;
+            float sonar_start = bound(0, autocvar_hud_panel_strafehud_sonar_start, 1);
+            float sonar_ratio = strafe_ratio - sonar_start;
+            if(sonar_start != 1)
+                sonar_ratio /= 1 - sonar_start;
+            else
+                sonar_ratio = 1;
+
+            float sonar_interval = max(0, autocvar_hud_panel_strafehud_sonar_interval_start);
+            sonar_interval += autocvar_hud_panel_strafehud_sonar_interval_range * sonar_ratio ** max(1, autocvar_hud_panel_strafehud_sonar_interval_exponent);
+            bool sonar_ready = (sonar_time == 0) || ((time - sonar_time) >= sonar_interval);
+            if(autocvar_hud_panel_strafehud_sonar && sonar_ready && (strafe_ratio >= sonar_start)) {
+                sonar_time = time;
+
+                float sonar_volume = bound(0, autocvar_hud_panel_strafehud_sonar_volume_start, 1);
+                sonar_volume += autocvar_hud_panel_strafehud_sonar_volume_range * sonar_ratio ** max(1, autocvar_hud_panel_strafehud_sonar_volume_exponent);
+
+                float sonar_pitch = max(0, autocvar_hud_panel_strafehud_sonar_pitch_start);
+                sonar_pitch += autocvar_hud_panel_strafehud_sonar_pitch_range * sonar_ratio ** max(1, autocvar_hud_panel_strafehud_sonar_pitch_exponent);
+
+                if(sonarsound && (sonar_volume > 0)) {
+                    sound7(csqcplayer, CH_INFO, sonarsound, bound(0, sonar_volume, 1) * VOL_BASE, ATTN_NONE, max(0.000001, sonar_pitch * 100), 0);
+                }
+            }
         }
 
-        if(fabs(angle + wishangle) > 90) // player is overturning
+        if(mode == STRAFEHUD_MODE_VIEW_CENTERED || straight_overturn)
         {
-            currentangle_color = autocvar_hud_panel_strafehud_angle_overturn_color;
+            currentangle_offset = panel_size.x/2;
         }
-        else if(bestangle_anywhere) // player gains speed without strafing
+
+        float angleheight_offset = currentangle_size.y;
+        float ghost_offset = 0;
+        if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE)
         {
-            currentangle_color = autocvar_hud_panel_strafehud_angle_accel_color;
+            ghost_offset = bound(0, (odd_angles ? odd_bestangle_offset : bestangle_offset), panel_size.x);
         }
 
-        if(mode == 0 || straight_overturn)
+        currentangle_offset = StrafeHUD_projectOffset(currentangle_offset, hudangle);
+        ghost_offset = StrafeHUD_projectOffset(ghost_offset, hudangle);
+
+        switch(autocvar_hud_panel_strafehud_angle_style)
         {
-            currentangle_offset = panel_size.x/2;
+            case STRAFEHUD_INDICATOR_SOLID:
+                if(currentangle_size.x > 0 && currentangle_size.y > 0)
+                {
+                    if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE) drawfill(panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2) + eX * (ghost_offset - currentangle_size.x/2), currentangle_size, autocvar_hud_panel_strafehud_bestangle_color, autocvar_hud_panel_strafehud_bestangle_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+                    drawfill(panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2) + eX * (currentangle_offset - currentangle_size.x/2), currentangle_size, currentangle_color, autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+                }
+                break;
+            case STRAFEHUD_INDICATOR_DASHED:
+                if(currentangle_size.x > 0 && currentangle_size.y > 0)
+                {
+                    vector line_size = currentangle_size;
+                    line_size.y = currentangle_size.y / (bound(2, autocvar_hud_panel_strafehud_angle_dashes, currentangle_size.y)*2-1);
+                    for(float i = 0; i < currentangle_size.y; i += line_size.y*2)
+                    {
+                        if(i + line_size.y*2 >= currentangle_size.y) line_size.y = currentangle_size.y - i;
+                        if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE) drawfill(panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2 - i) + eX * (ghost_offset - line_size.x/2), line_size, autocvar_hud_panel_strafehud_bestangle_color, autocvar_hud_panel_strafehud_bestangle_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+                        drawfill(panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2 - i) + eX * (currentangle_offset - line_size.x/2), line_size, currentangle_color, autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+                    }
+                }
+                break;
+            case STRAFEHUD_INDICATOR_NONE:
+            default:
+                // don't offset text and arrows if the angle indicator line isn't drawn
+                angleheight_offset = panel_size.y;
         }
 
-        if(autocvar_hud_panel_strafehud_style == 2 && !immobile)
+        float angle_offset_top = 0, angle_offset_bottom = 0;
+
+        // offset text if any angle indicator is drawn
+        if((autocvar_hud_panel_strafehud_angle_alpha > 0) || (autocvar_hud_panel_strafehud_bestangle && autocvar_hud_panel_strafehud_bestangle_alpha > 0))
+            angle_offset_top = angle_offset_bottom = (angleheight_offset - panel_size.y) / 2; // offset text by amount the angle indicator extrudes from the strafehud bar
+
+        if(autocvar_hud_panel_strafehud_angle_arrow > 0)
         {
-            float moveangle = angle + wishangle;
-            float strafeangle = (bestangle + wishangle) * (direction < 0 ? -1 : 1);
-            float strafe_ratio = 0;
-            if(fabs(moveangle) > 90)
+            if(arrow_size > 0)
             {
-                strafe_ratio = -((fabs(moveangle) - 90) / 90);
-                if(strafe_ratio < -1) strafe_ratio = -2 - strafe_ratio;
-            }
-            else
-            {
-                if(moveangle >= strafeangle)
+                if(autocvar_hud_panel_strafehud_angle_arrow == 1 || autocvar_hud_panel_strafehud_angle_arrow >= 3)
                 {
-                    strafe_ratio = 1 - (moveangle - strafeangle) / (90 - strafeangle);
+                    if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE) StrafeHUD_drawStrafeArrow(panel_pos + eY * ((panel_size.y - angleheight_offset) / 2) + eX * ghost_offset, arrow_size, autocvar_hud_panel_strafehud_bestangle_color, autocvar_hud_panel_strafehud_bestangle_alpha * panel_fg_alpha, true);
+                    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);
+
+                    angle_offset_top += arrow_size; // further offset the top text offset if the top arrow is drawn
                 }
-                else if(moveangle <= -strafeangle)
+                if(autocvar_hud_panel_strafehud_angle_arrow >= 2)
                 {
-                    strafe_ratio = 1 - (moveangle + strafeangle) / (-90 + strafeangle);
+                    if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE) StrafeHUD_drawStrafeArrow(panel_pos + eY * ((panel_size.y - angleheight_offset) / 2 + angleheight_offset) + eX * ghost_offset, arrow_size, autocvar_hud_panel_strafehud_bestangle_color, autocvar_hud_panel_strafehud_bestangle_alpha * panel_fg_alpha, false);
+                    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);
+
+                    angle_offset_bottom += arrow_size; // further offset the bottom text offset if the bottom arrow is drawn
                 }
             }
-            if(strafe_ratio < 0)
+        }
+
+        // make sure text doesn't draw inside the strafehud bar
+        text_offset_top = max(angle_offset_top, text_offset_top);
+        text_offset_bottom = max(angle_offset_bottom, text_offset_bottom);
+
+        // vertical angle for weapon jumps
+        {
+            if(autocvar_hud_panel_strafehud_vangle)
             {
-                currentangle_color = StrafeHUD_mixColors(autocvar_hud_panel_strafehud_angle_neutral_color, autocvar_hud_panel_strafehud_angle_overturn_color, -strafe_ratio);
+                float vangle = -PHYS_INPUT_ANGLES(strafeplayer).x;
+                float vangle_height = autocvar_hud_panel_strafehud_vangle_size * panel_size.y;
+                string vangle_text = strcat(ftos_decimals(vangle, 2), "°");
+
+                if(StrafeHUD_drawTextIndicator(vangle_text, vangle_height, autocvar_hud_panel_strafehud_vangle_color, 1, time, text_offset_bottom, STRAFEHUD_TEXT_BOTTOM))
+                    text_offset_bottom += vangle_height;
             }
-            else
+        }
+
+        draw_beginBoldFont();
+
+        // show speed when crossing the start trigger
+        {
+            static float startspeed = 0, starttime = 0; // displayed value and timestamp for fade out
+
+            if((race_nextcheckpoint == 1) || (race_checkpoint == 254 && race_nextcheckpoint == 255)) // check if the start trigger was hit (will also trigger if the finish trigger was hit if those have the same ID)
             {
-                currentangle_color = StrafeHUD_mixColors(autocvar_hud_panel_strafehud_angle_neutral_color, autocvar_hud_panel_strafehud_angle_accel_color, strafe_ratio);
+                if((race_checkpointtime > 0) && (starttime != race_checkpointtime))
+                {
+                    starttime = race_checkpointtime;
+                    startspeed = speed;
+                }
+            }
+
+            if(autocvar_hud_panel_strafehud_startspeed)
+            {
+                float startspeed_height = autocvar_hud_panel_strafehud_startspeed_size * panel_size.y;
+                string startspeed_text = ftos_decimals(startspeed * speed_conversion_factor, 2);
+                if(autocvar_hud_panel_strafehud_unit_show)
+                    startspeed_text = strcat(startspeed_text, GetSpeedUnit(autocvar_hud_panel_strafehud_unit));
+
+                if(StrafeHUD_drawTextIndicator(startspeed_text, startspeed_height, autocvar_hud_panel_strafehud_startspeed_color, autocvar_hud_panel_strafehud_startspeed_fade, starttime, text_offset_bottom, STRAFEHUD_TEXT_BOTTOM))
+                    text_offset_bottom += startspeed_height;
             }
         }
 
-        if(currentangle_size.x > 0 && currentangle_size.y > 0 && autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha > 0)
+        // show height achieved by a single jump
+        // FIXME: checking z position differences is unreliable (warpzones, teleporter, kill, etc) but using velocity to calculate jump height would be
+        //        inaccurate in hud code (possibly different tick rate than physics, doesn't run when hud isn't drawn, rounding errors)
         {
-            drawfill(panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2) + eX * (currentangle_offset - currentangle_size.x/2), currentangle_size, currentangle_color, autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+            static float height_min = 0, height_max = 0; // ground and peak of jump z coordinates
+            static float jumpheight = 0, jumptime = 0;   // displayed value and timestamp for fade out
+
+            // tries to catch kill and spectate but those are not reliable
+            if((strafeplayer.velocity.z <= 0) || real_onground || swimming || IS_DEAD(strafeplayer) || !IS_PLAYER(strafeplayer))
+            {
+                height_min = height_max = strafeplayer.origin.z;
+            }
+            else if(strafeplayer.origin.z > height_max)
+            {
+                height_max = strafeplayer.origin.z;
+                float jumpheight_new = height_max - height_min;
+
+                if((jumpheight_new * length_conversion_factor) > max(autocvar_hud_panel_strafehud_jumpheight_min, 0))
+                {
+                    jumpheight = jumpheight_new;
+                    jumptime = time;
+                }
+            }
+
+            if(autocvar_hud_panel_strafehud_jumpheight)
+            {
+                float jumpheight_height = autocvar_hud_panel_strafehud_jumpheight_size * panel_size.y;
+                string jumpheight_text = ftos_decimals(jumpheight * length_conversion_factor, length_decimals);
+                if(autocvar_hud_panel_strafehud_unit_show)
+                    jumpheight_text = strcat(jumpheight_text, GetLengthUnit(autocvar_hud_panel_strafehud_unit));
+
+                if(StrafeHUD_drawTextIndicator(jumpheight_text, jumpheight_height, autocvar_hud_panel_strafehud_jumpheight_color, autocvar_hud_panel_strafehud_jumpheight_fade, jumptime, text_offset_top, STRAFEHUD_TEXT_TOP))
+                    text_offset_top += jumpheight_height;
+            }
         }
+
+        draw_endBoldFont();
+    }
+    hud_lasttime = time;
+}
+
+float StrafeHUD_projectOffset(float offset, float range) {
+    range *= DEG2RAD / 2;
+    float angle = (offset - (panel_size.x/2)) / (panel_size.x/2) * range;
+    switch(autocvar_hud_panel_strafehud_projection) {
+        default:
+        case STRAFEHUD_PROJECTION_LINEAR:
+            return offset;
+        case STRAFEHUD_PROJECTION_PERSPECTIVE:
+            offset = tan(angle) / tan(range);
+            break;
+        case STRAFEHUD_PROJECTION_PANORAMIC:
+            offset = tan(angle/2) / tan(range/2);
+            break;
     }
+    offset = offset * (panel_size.x/2) + (panel_size.x/2);
+    return offset;
+}
+
+float StrafeHUD_projectWidth(float offset, float width, float range) {
+    return StrafeHUD_projectOffset(offset + width, range) - StrafeHUD_projectOffset(offset, range);
 }
 
 // functions to make hud elements align perfectly in the hud area
-void HUD_Panel_DrawStrafeHUD(float offset, float width, vector color, float alpha, int type, int gradientType)
+void HUD_Panel_DrawStrafeHUD(float offset, float width, float hidden_width, vector color, float alpha, int type, int gradientType, bool doProject, float range)
 {
     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
 
-    float original_width = width;
-    float hiddencolor_width;
+    if(type == STRAFEHUD_STYLE_GRADIENT && gradientType == STRAFEHUD_GRADIENT_NONE) type = STRAFEHUD_STYLE_DRAWFILL;
 
-    if(alpha <= 0 && type != 2 || width <= 0) return;
-
-    if(type == 2 && gradientType == 0) type = 0;
+    if(alpha <= 0 && type != STRAFEHUD_STYLE_GRADIENT || width <= 0) return;
 
     if(offset < 0)
     {
@@ -771,54 +1115,83 @@ void HUD_Panel_DrawStrafeHUD(float offset, float width, vector color, float alph
         mirror_width = min(offset + width - panel_size.x - hidden_width, width);
         mirror_offset = max(offset - panel_size.x - hidden_width, 0);
     }
+
+    width = max(width, 0);
     if((offset + width) > panel_size.x)
     {
+        overflow_width = (offset + width) - panel_size.x;
         width = panel_size.x - offset;
     }
+    size.x = width;
+
+    vector original_size = size;
+    float original_offset = offset;
+    if(doProject) {
+        if(size.x > 0) size.x = StrafeHUD_projectWidth(offset, size.x, range);
+        offset = StrafeHUD_projectOffset(offset, range);
+    }
+
     if(mirror_offset < 0)
     {
         mirror_width += mirror_offset;
         mirror_offset = 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;
     }
+    mirror_size.x = mirror_width;
 
-    if(width < 0) width = 0;
-    if(mirror_width < 0) mirror_width = 0;
-    hiddencolor_width = original_width - width - mirror_width;
-
-    if(direction < 0) // swap mirror and non-mirror values if direction points left
-    {
-        offset += mirror_offset;
-        mirror_offset = offset - mirror_offset;
-        offset -= mirror_offset;
-
-        width += mirror_width;
-        mirror_width = width - mirror_width;
-        width -= mirror_width;
+    vector original_mirror_size = mirror_size;
+    float original_mirror_offset = mirror_offset;
+    if(doProject) {
+        if(mirror_size.x > 0) mirror_size.x = StrafeHUD_projectWidth(mirror_offset, mirror_size.x, range);
+        mirror_offset = StrafeHUD_projectOffset(mirror_offset, range);
     }
 
-    size.x = width;
-    mirror_size.x = mirror_width;
-
     switch(type)
     {
         default:
-        case 0: // no styling (drawfill)
+        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 1: // progress bar style
+        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 2: // gradient style (types: 1 = left, 2 = right, 3 = both)
-            StrafeHUD_drawGradient(color, autocvar_hud_panel_strafehud_bar_neutral_color, mirror_size, original_width, mirror_offset, alpha, width + (mirror_offset == 0 ? hiddencolor_width : 0), gradientType);
-            StrafeHUD_drawGradient(color, autocvar_hud_panel_strafehud_bar_neutral_color, size, original_width, offset, alpha, (offset == 0 ? hiddencolor_width : 0), gradientType);
+        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;
+            }
+
+            StrafeHUD_drawGradient(color, autocvar_hud_panel_strafehud_bar_neutral_color, original_mirror_size, original_width, original_mirror_offset, alpha, gradient_mirror_offset, gradientType, doProject, range);
+            StrafeHUD_drawGradient(color, autocvar_hud_panel_strafehud_bar_neutral_color, original_size, original_width, original_offset, alpha, gradient_offset, gradientType, doProject, range);
     }
 }
 
@@ -833,53 +1206,96 @@ vector StrafeHUD_mixColors(vector color1, vector color2, float ratio)
     return mixedColor;
 }
 
-void StrafeHUD_drawGradient(vector color1, vector color2, vector size, float original_width, float offset, float alpha, float gradientOffset, int gradientType)
+void StrafeHUD_drawGradient(vector color1, vector color2, vector size, float original_width, float offset, float alpha, float gradientOffset, int gradientType, bool doProject, float range)
 {
     float color_ratio, alpha1, alpha2;
-    vector gradient_size = size;
+    vector segment_size = size;
     alpha1 = bound(0, alpha, 1);
     alpha2 = bound(0, autocvar_hud_panel_strafehud_bar_neutral_alpha, 1);
     if((alpha1+alpha2) == 0) return;
     color_ratio = alpha1/(alpha1+alpha2);
     for(int i = 0; i < size.x; ++i)
     {
-        float ratio, alpha_ratio, combine_ratio1, combine_ratio2;
-        gradient_size.x = size.x - i < 1 ? size.x - i : 1;
-        ratio = (i + gradientOffset) / original_width * (gradientType == 3 ? 2 : 1);
+        float ratio, alpha_ratio, combine_ratio1, combine_ratio2, segment_offset;
+        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
+        segment_offset = offset + i;
+        ratio = (i + segment_size.x/2 + gradientOffset) / original_width * (gradientType == STRAFEHUD_GRADIENT_BOTH ? 2 : 1);
+        if(doProject)
+        {
+            segment_size.x = StrafeHUD_projectWidth(segment_offset, segment_size.x, range);
+            segment_offset = StrafeHUD_projectOffset(segment_offset, range);
+        }
         if(ratio > 1) ratio = 2 - ratio;
-        if(gradientType != 2) ratio = 1 - 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), gradient_size, StrafeHUD_mixColors(color1, color2, ratio), alpha_ratio, DRAWFLAG_NORMAL);
+        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)
+{
+    origin = HUD_Shift(origin);
+    size = HUD_ScaleX(size);
+    if(flipped) origin -= size*eY;
+    R_BeginPolygon("", DRAWFLAG_NORMAL, true);
+    R_PolygonVertex(origin + (flipped ? size*eY : '0 0 0')          , '0 0 0', color, alpha);
+    R_PolygonVertex(origin + (flipped ? '0 0 0' : size*eY) - size*eX, '0 0 0', color, alpha);
+    R_PolygonVertex(origin + (flipped ? '0 0 0' : size*eY) + size*eX, '0 0 0', color, alpha);
+    R_EndPolygon();
+}
+
+// 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;
+
+    float alpha = cos(((time - lasttime) / fadetime) * 90 * DEG2RAD); // fade non-linear like the physics panel does
+    vector size = panel_size;
+    size.y = height;
+
+    switch(position) {
+        case STRAFEHUD_TEXT_TOP:
+            offset += size.y;
+            offset *= -1;
+            break;
+        case STRAFEHUD_TEXT_BOTTOM:
+            offset += panel_size.y;
+            break;
     }
+
+    drawstring_aspect(panel_pos + eY * offset, text, size, color, alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+    return true;
 }
 
 // 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;
-       }
+    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 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"));
-       }
+    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"));
+    }
 }
index 2fbd9b27526ca84e467cf83c23f13aff8dd8a9ef..4cbfbeddf58e9265010cd641bb2d717e0a827053 100644 (file)
 #pragma once
 #include "../panel.qh"
 
-int autocvar_hud_panel_strafehud = 3;
-bool autocvar__hud_panel_strafehud_demo = false;
-bool autocvar_hud_panel_strafehud_dynamichud    = true;
-int autocvar_hud_panel_strafehud_mode = 0;
-float autocvar_hud_panel_strafehud_range = 0;
-int autocvar_hud_panel_strafehud_style = 1;
-int autocvar_hud_panel_strafehud_unit = 1;
-bool autocvar_hud_panel_strafehud_unit_show = true;
-vector autocvar_hud_panel_strafehud_bar_neutral_color = '1 1 1';
-float autocvar_hud_panel_strafehud_bar_neutral_alpha = 0.3;
-vector autocvar_hud_panel_strafehud_bar_accel_color = '0 1 0';
-float autocvar_hud_panel_strafehud_bar_accel_alpha = 0.3;
-vector autocvar_hud_panel_strafehud_bar_overturn_color = '1 0 1';
-float autocvar_hud_panel_strafehud_bar_overturn_alpha = 0.3;
-float autocvar_hud_panel_strafehud_angle_alpha = 0.8;
-float autocvar_hud_panel_strafehud_angle_height = 1.5;
-float autocvar_hud_panel_strafehud_angle_width = 0.005;
-vector autocvar_hud_panel_strafehud_angle_neutral_color = '1 1 0';
-vector autocvar_hud_panel_strafehud_angle_accel_color = '0 1 1';
-vector autocvar_hud_panel_strafehud_angle_overturn_color = '1 0 1';
-float autocvar_hud_panel_strafehud_switch_minspeed = -1;
-vector autocvar_hud_panel_strafehud_switch_active_color = '0 1 0';
-float autocvar_hud_panel_strafehud_switch_active_alpha = 1;
-vector autocvar_hud_panel_strafehud_switch_inactive_color = '1 1 0';
-float autocvar_hud_panel_strafehud_switch_inactive_alpha = 1;
-float autocvar_hud_panel_strafehud_switch_width = 0.0075;
-vector autocvar_hud_panel_strafehud_direction_color = '0 0.5 1';
-float autocvar_hud_panel_strafehud_direction_alpha = 1;
-float autocvar_hud_panel_strafehud_direction_width = 0.25;
-float autocvar_hud_panel_strafehud_direction_length = 0.02;
-float autocvar_hud_panel_strafehud_slickdetector_range = 0;
-int autocvar_hud_panel_strafehud_slickdetector_granularity = 2;
-vector autocvar_hud_panel_strafehud_slickdetector_color = '0 1 1';
-float autocvar_hud_panel_strafehud_slickdetector_alpha = 0.5;
-float autocvar_hud_panel_strafehud_slickdetector_height = 0.125;
-float autocvar_hud_panel_strafehud_startspeed_fade = 0;
-vector autocvar_hud_panel_strafehud_startspeed_color = '1 0.75 0';
-float autocvar_hud_panel_strafehud_startspeed_size = 1.5;
-float autocvar_hud_panel_strafehud_jumpheight_fade = 0;
-float autocvar_hud_panel_strafehud_jumpheight_min = 50;
-vector autocvar_hud_panel_strafehud_jumpheight_color = '0 1 0.75';
-float autocvar_hud_panel_strafehud_jumpheight_size = 1.5;
-float autocvar_hud_panel_strafehud_timeout_air = 0.1;
-float autocvar_hud_panel_strafehud_timeout_ground = 0.03333333;
-float autocvar_hud_panel_strafehud_timeout_turn = 0.1;
-float autocvar_hud_panel_strafehud_timeout_direction = 0.5;
-float autocvar_hud_panel_strafehud_antiflicker_angle = 0.01;
-float autocvar_hud_panel_strafehud_antiflicker_speed = 0.0001;
-
-void HUD_Panel_DrawStrafeHUD(float, float, vector, float, int, int);
+AUTOCVAR_SAVE(hud_panel_strafehud, int, 3, "enable this panel, 1 = show if not observing, 2 = show always, 3 = show only in race/cts if not observing");
+AUTOCVAR_SAVE(_hud_panel_strafehud_demo, bool, false, "strafehud changes angle during configure");
+AUTOCVAR_SAVE(hud_panel_strafehud_dynamichud, bool, true, "apply the dynamic hud effects to this panel");
+AUTOCVAR_SAVE(hud_panel_strafehud_mode, int, 0, "strafehud mode which controls whether the strafehud is centered at \"0\" = view angle, \"1\" = velocity angle");
+AUTOCVAR_SAVE(hud_panel_strafehud_range, float, 90, "the angle range up to 360 degrees displayed on the strafehud, \"-1\" = current fov, \"0\" = dynamic (chooses the minimum range required to still see the whole area needed for accelerating)");
+AUTOCVAR_SAVE(hud_panel_strafehud_style, int, 2, "\"0\" = no styling, \"1\" = progress bar style for the strafe bar, \"2\" = gradient for the strafe bar");
+AUTOCVAR_SAVE(hud_panel_strafehud_unit, int, 1, "speed unit (1 = qu/s, 2 = m/s, 3 = km/h, 4 = mph, 5 = knots), length unit (1 = qu, 2 = m, 3 = km, 4 = mi, 5 = nmi)");
+AUTOCVAR_SAVE(hud_panel_strafehud_unit_show, bool, true, "show units");
+AUTOCVAR_SAVE(hud_panel_strafehud_uncapped, bool, false, "set to \"1\" to remove some safety restrictions, useful to set thinner indicator lines down to 1px or for trying out higher values for some performance degrading operations (warning: elements may turn invisible if too thin, other configurations may crash your game or look horribly ugly)");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_preaccel, bool, true, "set to \"1\" to extend the acceleration zone by the strafe meter zone before full acceleration can be achieved");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_neutral_color, vector, '1 1 1', "color of the strafe meter neutral zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_neutral_alpha, float, 0.1, "opacity of the strafe meter neutral zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_accel_color, vector, '0 1 0', "color of the strafe meter acceleration zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_accel_alpha, float, 0.5, "opacity of the strafe meter acceleration zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_overturn_color, vector, '1 0 1', "color of the strafe meter overturn zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_overturn_alpha, float, 0.5, "opacity of the strafe meter overturn zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_style, int, 0, "set the angle indicator style: 0 = none, 1 = solid line, 2 = dashed line");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_dashes, int, 4, "determines the amount of dashes if the angle indicator uses a dashed line");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_alpha, float, 0.8, "opacity of the indicator showing the player's current angle");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_height, float, 1, "height of the indicator showing the player's current angle (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_width, float, 0.001, "width of the indicator showing the player's current angle (relative to the panel width)");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_neutral_color, vector, '1 1 0', "color of the indicator showing the player's current angle if it is within the neutral zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_accel_color, vector, '0 1 1', "color of the indicator showing the player's current angle if it is within the acceleration zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_overturn_color, vector, '1 0 1', "color of the indicator showing the player's current angle if it is within the overturn zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_arrow, int, 1, "set the angle indicator's arrow style: 0 = none, 1 = top, 2 = bottom, 3 = both");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_arrow_size, float, 0.5, "size of the arrow (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_bestangle, bool, true, "set to \"1\" to enable a ghost angle indicator showing the best angle to gain maximum acceleration");
+AUTOCVAR_SAVE(hud_panel_strafehud_bestangle_color, vector, '1 1 1', "color of the indicator showing the best angle to gain maximum acceleration");
+AUTOCVAR_SAVE(hud_panel_strafehud_bestangle_alpha, float, 0.5, "opacity of the indicator showing the best angle to gain maximum acceleration");
+AUTOCVAR_SAVE(hud_panel_strafehud_switch, bool, true, "set to \"1\" to enable the switch indicator showing the angle to move to when switching sides");
+AUTOCVAR_SAVE(hud_panel_strafehud_switch_minspeed, float, -1, "minimum speed in qu/s at which switch indicator(s) which are used to aid changing strafe direction will be shown (set to -1 for dynamic minspeed)");
+AUTOCVAR_SAVE(hud_panel_strafehud_switch_color, vector, '1 1 0', "color of the switch indicator");
+AUTOCVAR_SAVE(hud_panel_strafehud_switch_alpha, float, 1, "opacity of the switch indicator");
+AUTOCVAR_SAVE(hud_panel_strafehud_switch_width, float, 0.003, "width of the strafe angle indicator(s) (relative to the strafe bar width)");
+AUTOCVAR_SAVE(hud_panel_strafehud_direction, bool, false, "set to \"1\" to enable the direction caps to see in which direction you are currently strafing");
+AUTOCVAR_SAVE(hud_panel_strafehud_direction_color, vector, '0 0.5 1', "color of the direction caps which indicate the direction the player is currently strafing towards");
+AUTOCVAR_SAVE(hud_panel_strafehud_direction_alpha, float, 1, "opacity of the direction caps which indicate the direction the player is currently strafing towards");
+AUTOCVAR_SAVE(hud_panel_strafehud_direction_width, float, 0.25, "stroke width of the direction caps which indicate the direction the player is currently strafing towards (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_direction_length, float, 0.02, "length of the horizontal component of the direction caps which indicate the direction the player is currently strafing towards (relative to the panel width)");
+AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector, bool, true, "set to \"1\" to enable the slick detector which notifies you if there is slick near you");
+AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_range, float, 200, "range of the slick detector in qu");
+AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_granularity, int, 1, "value from 0 to 4 which defines how exact the search for slick should be, higher values may yield better results but require more computation");
+AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_color, vector, '0 1 1', "color of the slick detector indicator");
+AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_alpha, float, 0.5, "opacity of the slick detector indicator");
+AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_height, float, 0.125, "height of the slick detector indicator (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_startspeed, bool, true, "set to \"1\" to enable the start speed indicator which shows you the speed you had while passing the start trigger of a race map");
+AUTOCVAR_SAVE(hud_panel_strafehud_startspeed_fade, float, 4, "fade time (in seconds) of the start speed text");
+AUTOCVAR_SAVE(hud_panel_strafehud_startspeed_color, vector, '1 0.75 0', "color of the start speed text");
+AUTOCVAR_SAVE(hud_panel_strafehud_startspeed_size, float, 1.5, "size of the start speed text (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight, bool, false, "set to \"1\" to enable the jump height indicator which tells you how high you jumped");
+AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight_fade, float, 4, "fade time (in seconds) of the jump height text");
+AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight_min, float, 50, "minimum jump height to display in the selected unit");
+AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight_color, vector, '0 1 0.75', "color of the jump height text");
+AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight_size, float, 1.5, "size of the jump height text (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_timeout_ground, float, 0.1, "time (in seconds) after take off before changing to air strafe physics when not jumping (visually more consistent hud while on slick downwards ramps)");
+AUTOCVAR_SAVE(hud_panel_strafehud_timeout_turn, float, 0.1, "time (in seconds) after releasing the strafe keys before changing mode (visually more consistent hud while switching between left/right strafe turning)");
+AUTOCVAR_SAVE(hud_panel_strafehud_antiflicker_angle, float, 0.01, "how many degrees from 0° to 180° the hud ignores if it could cause visual disturbances otherwise (and to counter rounding errors)");
+AUTOCVAR_SAVE(hud_panel_strafehud_fps_update, float, 0.5, "update interval (in seconds) of the frametime to calculate the optimal angle, smaller values may cause flickering");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar, bool, false, "set to \"1\" to enable the strafe sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_audio, string, "misc/talk", "audio to play for sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_start, float, 0.5, "how optimal from 0 to 1 your strafing angle has to be for the strafe sonar to activate");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_interval_start, float, 0.333333, "strafe sonar sound interval in seconds");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_interval_range, float, -0.222222, "dynamic sound interval range in seconds of the strafe sonar as you approach the optimal angle");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_interval_exponent, float, 1, "exponent of the dynamic sound interval range of the strafe sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_volume_start, float, 0.333333, "sound volume of the strafe sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_volume_range, float, 0.666666, "dynamic volume range of the strafe sonar as you approach the optimal angle");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_volume_exponent, float, 1, "exponent of the dynamic volume range of the strafe sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_pitch_start, float, 0.9, "playback speed of the strafe sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_pitch_range, float, 0.1, "dynamic playback speed range of the strafe sonar as you approach the optimal angle");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_pitch_exponent, float, 1, "exponent of the dynamic playback speed range of the strafe sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_vangle, bool, false, "set to \"1\" to enable the vertical angle indicator");
+AUTOCVAR_SAVE(hud_panel_strafehud_vangle_color, vector, '0.75 0.75 0.75', "color of the vertical angle text");
+AUTOCVAR_SAVE(hud_panel_strafehud_vangle_size, float, 1, "size of the vertical angle text (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_projection, int, 0, "strafehud projection mode, \"0\" = linear, \"1\" = perspective, \"2\" = panoramic");
+
+void HUD_Panel_DrawStrafeHUD(float, float, float, vector, float, int, int, bool, float);
 vector StrafeHUD_mixColors(vector, vector, float);
-void StrafeHUD_drawGradient(vector, vector, vector, float, float, float, float, int);
+void StrafeHUD_drawGradient(vector, vector, vector, float, float, float, float, int, bool, float);
 float GetLengthUnitFactor(int);
 string GetLengthUnit(int);
+void StrafeHUD_drawStrafeArrow(vector, float, vector, float, bool);
+bool StrafeHUD_drawTextIndicator(string, float, vector, float, float, float, int);
+float StrafeHUD_projectOffset(float, float);
+float StrafeHUD_projectWidth(float, float, float);
+
+const int STRAFEHUD_MODE_VIEW_CENTERED = 0;
+const int STRAFEHUD_MODE_VELOCITY_CENTERED = 1;
+
+const int STRAFEHUD_DIRECTION_NONE = 0;
+const int STRAFEHUD_DIRECTION_LEFT = 1;
+const int STRAFEHUD_DIRECTION_RIGHT = 2;
+
+const int STRAFEHUD_KEYS_NONE = 0;
+const int STRAFEHUD_KEYS_FORWARD = 1;
+const int STRAFEHUD_KEYS_BACKWARD = 2;
+
+const int STRAFEHUD_STYLE_DRAWFILL = 0;
+const int STRAFEHUD_STYLE_PROGRESSBAR = 1;
+const int STRAFEHUD_STYLE_GRADIENT = 2;
+
+const int STRAFEHUD_GRADIENT_NONE = 0;
+const int STRAFEHUD_GRADIENT_LEFT = 1;
+const int STRAFEHUD_GRADIENT_RIGHT = 2;
+const int STRAFEHUD_GRADIENT_BOTH = 3;
+
+const int STRAFEHUD_INDICATOR_NONE = 0;
+const int STRAFEHUD_INDICATOR_SOLID = 1;
+const int STRAFEHUD_INDICATOR_DASHED = 2;
+
+const int STRAFEHUD_TEXT_TOP = 0;
+const int STRAFEHUD_TEXT_BOTTOM = 1;
+
+const int STRAFEHUD_PROJECTION_LINEAR = 0;
+const int STRAFEHUD_PROJECTION_PERSPECTIVE = 1;
+const int STRAFEHUD_PROJECTION_PANORAMIC = 2;
index 929d3765345084e1e85917705e498417e46563c0..ec6894a6a9de23c50a3eecad790facb1e45fcdcb 100644 (file)
@@ -270,6 +270,12 @@ void GenericCommand_maplist(int request, int argc)
                                        return;
                                }
 
+                        case "rebuild": // rebuilds maplist to include available maps, useful after doing fs_rescan
+                          {
+                            cvar_set("g_maplist", MapInfo_ListAllowedMaps(MapInfo_CurrentGametype(), MapInfo_RequiredFlags(), MapInfo_ForbiddenFlags()));
+                            return;
+                          }
+
                                case "remove": // scans maplist and only adds back whatever maps were not provided in argv(2)
                                {
                                        if(argc == 3)
index e0c17a7a128a0e623be23e7048a6afd4bec0e108..50d7b0ba57c9d5058f823ccae2defabd14ef1237 100644 (file)
@@ -28,7 +28,7 @@ const int SPECIES_ROBOT_SHINY = 5;
 const int SPECIES_RESERVED = 15;
 
 #ifdef GAMEQC
-const int RANKINGS_CNT = 99;
+const int RANKINGS_CNT = 256;
 
 ///////////////////////////
 // keys pressed
index f8933bcb06afd22034aa155a1461af68fff2aa44..bd3cf8a03b5b9083c64d1053d726552c2a2a975c 100644 (file)
@@ -131,7 +131,7 @@ void ctf_CaptureRecord(entity flag, entity player)
                ctf_captimerecord = cap_time;
                db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
                db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
-               write_recordmarker(player, flag.ctf_pickuptime, cap_time);
+               write_recordmarker(player, 1, flag.ctf_pickuptime, cap_time);
        }
 
        if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
index 0fbab7ae51f54159e1913b7fe723bd4795b77ed4..defeb686345f41da4ff1825d169d64beb61a0f1d 100644 (file)
@@ -57,6 +57,10 @@ void cts_ScoreRules()
     GameRules_score_enabled(false);
     GameRules_scoring(0, 0, 0, {
         if (g_race_qualifying) {
+            field(SP_CTS_STRAFE, "strafe", 0);
+            field(SP_CTS_STARTSPEED, "startspeed", 0);
+            field(SP_CTS_AVGSPEED, "avgspeed", 0);
+            field(SP_CTS_TOPSPEED, "topspeed", 0);
             field(SP_RACE_FASTEST, "fastest", SFL_SORT_PRIO_PRIMARY | SFL_LOWER_IS_BETTER | SFL_TIME);
         } else {
             field(SP_RACE_LAPS, "laps", SFL_SORT_PRIO_PRIMARY);
@@ -141,6 +145,21 @@ MUTATOR_HOOKFUNCTION(cts, PlayerPhysics)
                                CS(player).movement_y = -M_SQRT1_2 * wishspeed;
                }
        }
+       player.strafe_efficiency_sum += calculate_strafe_efficiency(player, CS(player).movement, dt) * dt;
+       if(player.race_started)
+       {
+               float current_speed = vlen(vec2(player.velocity));
+               if(player.race_topspeed < current_speed)
+               {
+                       player.race_topspeed = current_speed;
+               }
+               player.race_avgspeed_sum += current_speed * dt;
+               player.race_avgspeed_time += dt;
+       }
+       else
+       {
+               player.race_startspeed = player.race_avgspeed_sum = player.race_avgspeed_time = player.race_topspeed = 0;
+       }
 }
 
 MUTATOR_HOOKFUNCTION(cts, reset_map_global)
@@ -153,6 +172,15 @@ MUTATOR_HOOKFUNCTION(cts, reset_map_global)
        PlayerScore_Sort(race_place, 0, true, false);
 
        FOREACH_CLIENT(true, {
+               it.strafe_efficiency_best = -2;
+               it.strafe_efficiency_sum = it.strafe_efficiency_time = 0;
+               PlayerScore_Set(it, SP_CTS_STRAFE, -20000);
+               it.race_startspeed_best = it.race_avgspeed_best = it.race_topspeed_best = -1;
+               it.race_startspeed = it.race_avgspeed_sum = it.race_avgspeed_time = it.race_topspeed = 0;
+               PlayerScore_Set(it, SP_CTS_STARTSPEED, -1);
+               PlayerScore_Set(it, SP_CTS_AVGSPEED, -1);
+               PlayerScore_Set(it, SP_CTS_TOPSPEED, -1);
+
                if(it.race_place)
                {
                        s = GameRules_scoring_add(it, RACE_FASTEST, 0);
@@ -179,6 +207,14 @@ MUTATOR_HOOKFUNCTION(cts, ClientConnect)
 
        race_PreparePlayer(player);
        player.race_checkpoint = -1;
+       player.strafe_efficiency_best = -2;
+       player.strafe_efficiency_sum = player.strafe_efficiency_time = 0;
+       PlayerScore_Set(player, SP_CTS_STRAFE, -20000);
+       player.race_startspeed_best = player.race_avgspeed_best = player.race_topspeed_best = -1;
+       player.race_startspeed = player.race_avgspeed_sum = player.race_avgspeed_time = player.race_topspeed = 0;
+       PlayerScore_Set(player, SP_CTS_STARTSPEED, -1);
+       PlayerScore_Set(player, SP_CTS_AVGSPEED, -1);
+       PlayerScore_Set(player, SP_CTS_TOPSPEED, -1);
 
        race_SendAll(player, false);
 }
@@ -248,6 +284,9 @@ MUTATOR_HOOKFUNCTION(cts, PlayerDies)
        frag_target.respawn_flags |= RESPAWN_FORCE;
        race_AbandonRaceCheck(frag_target);
 
+       frag_target.strafe_efficiency_sum = frag_target.strafe_efficiency_time = 0;
+       frag_target.race_startspeed = frag_target.race_avgspeed_sum = frag_target.race_avgspeed_time = frag_target.race_topspeed = 0;
+
        if(autocvar_g_cts_removeprojectiles)
        {
                IL_EACH(g_projectiles, it.owner == frag_target && (it.flags & FL_PROJECTILE),
index f9cc536eda728e8e402121af6c5fe33a702cc179..9d97d66101451fa042925a756b13015da9fede91 100644 (file)
@@ -7,4 +7,5 @@
 #include <common/mapobjects/target/spawn.qc>
 #include <common/mapobjects/target/spawnpoint.qc>
 #include <common/mapobjects/target/speaker.qc>
+#include <common/mapobjects/target/speed.qc>
 #include <common/mapobjects/target/voicescript.qc>
index e3f4cede9912314c03bbd2cef0d9a5cc494e20b4..48c57c42d9f12b0090ece1df715229177569591f 100644 (file)
@@ -7,4 +7,5 @@
 #include <common/mapobjects/target/spawn.qh>
 #include <common/mapobjects/target/spawnpoint.qh>
 #include <common/mapobjects/target/speaker.qh>
+#include <common/mapobjects/target/speed.qh>
 #include <common/mapobjects/target/voicescript.qh>
diff --git a/qcsrc/common/mapobjects/target/speed.qc b/qcsrc/common/mapobjects/target/speed.qc
new file mode 100644 (file)
index 0000000..35a5baf
--- /dev/null
@@ -0,0 +1,190 @@
+#include "speed.qh"
+
+#define XYZ_ARRAY(name) float name[3]
+#define ARRAY_AS_VECTOR(a) ((a)[0] * '1 0 0' + (a)[1] * '0 1 0' + (a)[2] * '0 0 1')
+#define VECTOR_TO_ARRAY(a, e) { vector v = (e); (a)[0] = v.x; (a)[1] = v.y; (a)[2] = v.z; }
+#define FOR_XYZ(idx) for(int idx = 0; idx < 3; ++idx)
+vector target_speed_calculatevelocity(entity this, float speed, entity pushed_entity)
+{
+       bool is_percentage = boolean(this.spawnflags & SPEED_PERCENTAGE);
+       bool is_add = boolean(this.spawnflags & SPEED_ADD);
+       bool is_launcher = boolean(this.spawnflags & SPEED_LAUNCHER);
+
+       bool is_positive[3];
+       is_positive[0] = boolean(this.spawnflags & SPEED_POSITIVE_X);
+       is_positive[1] = boolean(this.spawnflags & SPEED_POSITIVE_Y);
+       is_positive[2] = boolean(this.spawnflags & SPEED_POSITIVE_Z);
+
+       bool is_negative[3];
+       is_negative[0] = boolean(this.spawnflags & SPEED_NEGATIVE_X);
+       is_negative[1] = boolean(this.spawnflags & SPEED_NEGATIVE_Y);
+       is_negative[2] = boolean(this.spawnflags & SPEED_NEGATIVE_Z);
+
+       if(!is_add)
+       {
+               speed = max(speed, 0); // speed cannot be negative except when subtracting
+       }
+
+       XYZ_ARRAY(pushvel);
+       VECTOR_TO_ARRAY(pushvel, pushed_entity.velocity);
+
+       FOR_XYZ(i)
+       {
+               if(is_launcher && is_positive[i] && is_negative[i])
+               {
+                       is_positive[i] = is_negative[i] = false; // launcher can only be either positive or negative not both
+               }
+
+               if(!is_positive[i] && !is_negative[i])
+               {
+                       pushvel[i] = 0; // ignore this direction
+               }
+       }
+
+       float oldspeed = vlen(ARRAY_AS_VECTOR(pushvel));
+
+       if(is_percentage)
+       {
+               speed = oldspeed * speed / 100; // the speed field is used to specify the percentage of the current speed
+       }
+
+       float launcherspeed = 0;
+
+       if(!STAT(Q3COMPAT, pushed_entity)) // no need to simulate this bug
+       {
+               launcherspeed += speed;
+               if(is_add) launcherspeed += oldspeed; // add the add speed in the same variable as it goes in the same direction
+       }
+
+       FOR_XYZ(i)
+       {
+               if(((pushvel[i] != 0) || is_launcher) && (is_positive[i] != is_negative[i]))
+               {
+                       if(is_launcher)
+                       {
+                               pushvel[i] = 1; // every direction weighs the same amount on launchers, movedir does not matter
+                               if(STAT(Q3COMPAT, pushed_entity))
+                               {
+                                       launcherspeed += speed;
+                                       if(is_add) launcherspeed += oldspeed; // add the add speed in the same variable as it goes in the same direction
+                               }
+                       }
+
+                       if(is_positive[i])
+                       {
+                               pushvel[i] = copysign(pushvel[i], 1);
+                       }
+                       else if(is_negative[i])
+                       {
+                               pushvel[i] = copysign(pushvel[i], -1);
+                       }
+               }
+       }
+
+       XYZ_ARRAY(oldvel);
+       VECTOR_TO_ARRAY(oldvel, pushed_entity.velocity);
+
+       if(is_launcher)
+       {
+               VECTOR_TO_ARRAY(pushvel, normalize(ARRAY_AS_VECTOR(pushvel)) * fabs(launcherspeed)); // launcher will always launch you in the correct direction even if speed is negative, fabs() is correct
+       }
+       else
+       {
+               VECTOR_TO_ARRAY(pushvel, normalize(ARRAY_AS_VECTOR(pushvel)) * speed)
+
+               if(is_add)
+               {
+                       VECTOR_TO_ARRAY(pushvel, ARRAY_AS_VECTOR(pushvel) + ARRAY_AS_VECTOR(oldvel));
+               }
+       }
+
+       FOR_XYZ(i)
+       {
+               if(!is_positive[i] && !is_negative[i])
+               {
+                       pushvel[i] = oldvel[i]; // preserve unaffected directions
+               }
+       }
+
+       return ARRAY_AS_VECTOR(pushvel);
+}
+#undef XYZ_ARRAY
+#undef ARRAY_AS_VECTOR
+#undef VECTOR_TO_ARRAY
+#undef FOR_XYZ
+
+REGISTER_NET_LINKED(ENT_CLIENT_TARGET_SPEED)
+
+void target_speed_use(entity this, entity actor, entity trigger)
+{
+       if(this.active != ACTIVE_ACTIVE)
+               return;
+
+       actor.velocity = target_speed_calculatevelocity(this, this.speed, actor);
+}
+
+void target_speed_reset(entity this)
+{
+       this.active = ACTIVE_ACTIVE;
+}
+
+#ifdef SVQC
+void target_speed_link(entity this);
+
+/*
+ * ENTITY PARAMETERS:
+ *
+ *   targetname:  Activating trigger points to this.
+ *   speed:       Speed value to set (default: 100).
+ */
+spawnfunc(target_speed)
+{
+       this.active = ACTIVE_ACTIVE;
+       this.setactive = generic_netlinked_setactive;
+       this.use = target_speed_use;
+       this.reset = target_speed_reset;
+
+       // FIXME: zero and unset cannot be disambiguated in xonotic
+       //if (!this.speed)
+       //      this.speed = 100;
+
+       target_speed_link(this);
+}
+
+bool target_speed_send(entity this, entity to, float sf)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_TARGET_SPEED);
+
+       WriteInt24_t(MSG_ENTITY, this.spawnflags);
+       WriteByte(MSG_ENTITY, this.active);
+       WriteString(MSG_ENTITY, this.targetname);
+       WriteCoord(MSG_ENTITY, this.speed);
+
+       return true;
+}
+
+void target_speed_link(entity this)
+{
+       Net_LinkEntity(this, false, 0, target_speed_send);
+}
+
+#elif defined(CSQC)
+
+void target_speed_remove(entity this)
+{
+       strfree(this.targetname);
+}
+
+NET_HANDLE(ENT_CLIENT_TARGET_SPEED, bool isnew)
+{
+       this.spawnflags = ReadInt24_t();
+       this.active = ReadByte();
+       this.targetname = strzone(ReadString());
+       this.speed = ReadCoord();
+
+       this.use = target_speed_use;
+       this.entremove = target_speed_remove;
+
+       return true;
+}
+#endif
diff --git a/qcsrc/common/mapobjects/target/speed.qh b/qcsrc/common/mapobjects/target/speed.qh
new file mode 100644 (file)
index 0000000..f5f0319
--- /dev/null
@@ -0,0 +1,12 @@
+#pragma once
+
+
+const int SPEED_PERCENTAGE = BIT(0);
+const int SPEED_ADD = BIT(1);
+const int SPEED_POSITIVE_X = BIT(2);
+const int SPEED_NEGATIVE_X = BIT(3);
+const int SPEED_POSITIVE_Y = BIT(4);
+const int SPEED_NEGATIVE_Y = BIT(5);
+const int SPEED_POSITIVE_Z = BIT(6);
+const int SPEED_NEGATIVE_Z = BIT(7);
+const int SPEED_LAUNCHER = BIT(8);
index c8f9ad245d0ca7c24a7311de92005e62dc9acb16..cadd95f5ce5388c7d77bec29cea69e820425f31c 100644 (file)
@@ -223,9 +223,10 @@ entity Simple_TeleportPlayer(entity teleporter, entity player)
                if(vdist(player.velocity, >, e.speed))
                        player.velocity = normalize(player.velocity) * max(0, e.speed);
 
-       if(STAT(TELEPORT_MAXSPEED, player))
-               if(vdist(player.velocity, >, STAT(TELEPORT_MAXSPEED, player)))
-                       player.velocity = normalize(player.velocity) * max(0, STAT(TELEPORT_MAXSPEED, player));
+       if(!(teleporter.classname == "trigger_teleport" && teleporter.spawnflags & TELEPORT_KEEP_SPEED))
+               if(STAT(TELEPORT_MAXSPEED, player))
+                       if(vdist(player.velocity, >, STAT(TELEPORT_MAXSPEED, player)))
+                               player.velocity = normalize(player.velocity) * max(0, STAT(TELEPORT_MAXSPEED, player));
 
        locout = e.origin + '0 0 1' * (1 - player.mins.z - 24);
 
index 72d9916d1e9a4e1656c8e872776b3eab19e18e28..9c786dd98585ea355d18a9ed36ccc4be7f597ea7 100644 (file)
@@ -14,6 +14,7 @@ void trigger_push_use(entity this, entity actor, entity trigger)
 #endif
 
 REGISTER_NET_LINKED(ENT_CLIENT_TRIGGER_PUSH)
+REGISTER_NET_LINKED(ENT_CLIENT_TRIGGER_PUSH_VELOCITY)
 REGISTER_NET_LINKED(ENT_CLIENT_TARGET_PUSH)
 
 /*
@@ -135,22 +136,192 @@ vector trigger_push_calculatevelocity(vector org, entity tgt, float ht, entity p
        return sdir * vs + '0 0 1' * vz;
 }
 
-bool jumppad_push(entity this, entity targ)
+vector trigger_push_velocity_calculatevelocity(entity this, vector org, entity tgt, float speed, float count, entity pushed_entity, bool is_pushed)
+{
+       bool is_playerdir_xy = boolean(this.spawnflags & PUSH_VELOCITY_PLAYERDIR_XY);
+       bool is_add_xy = boolean(this.spawnflags & PUSH_VELOCITY_ADD_XY);
+       bool is_playerdir_z = boolean(this.spawnflags & PUSH_VELOCITY_PLAYERDIR_Z);
+       bool is_add_z = boolean(this.spawnflags & PUSH_VELOCITY_ADD_Z);
+       bool is_bidirectional_xy = boolean(this.spawnflags & PUSH_VELOCITY_BIDIRECTIONAL_XY);
+       bool is_bidirectional_z = boolean(this.spawnflags & PUSH_VELOCITY_BIDIRECTIONAL_Z);
+       bool is_clamp_negative_adds = boolean(this.spawnflags & PUSH_VELOCITY_CLAMP_NEGATIVE_ADDS);
+
+       vector sdir = normalize(vec2(pushed_entity.velocity));
+       float zdir = pushed_entity.velocity.z;
+       if(zdir != 0) zdir = copysign(1, zdir);
+
+       vector vs_tgt = '0 0 0';
+       float vz_tgt = 0;
+       if (!is_playerdir_xy || !is_playerdir_z)
+       {
+               vector vel_tgt = trigger_push_calculatevelocity(org, tgt, 0, pushed_entity);
+               vs_tgt = vec2(vel_tgt);
+               vz_tgt = vel_tgt.z;
+
+               // bidirectional jump pads do not play nicely with xonotic's jump pad targets
+               if (is_bidirectional_xy)
+               {
+                       if (normalize(vs_tgt) * sdir < 0)
+                       {
+                               vs_tgt *= -1;
+                       }
+               }
+
+               if (is_bidirectional_z)
+               {
+                       if (signbit(vz_tgt) != signbit(zdir))
+                       {
+                               vz_tgt *= -1;
+                       }
+               }
+       }
+
+       vector vs;
+       if (is_playerdir_xy)
+       {
+               vs = sdir * speed;
+       }
+       else
+       {
+               vs = vs_tgt;
+       }
+
+       float vz;
+       if (is_playerdir_z)
+       {
+               vz = zdir * count;
+       }
+       else
+       {
+               vz = vz_tgt;
+       }
+
+       if (is_add_xy)
+       {
+               vector vs_add = vec2(pushed_entity.velocity);
+               if (is_pushed)
+               {
+                       vs = vs_add;
+               }
+               else
+               {
+                       vs += vs_add;
+
+                       if (is_clamp_negative_adds)
+                       {
+                               if ((normalize(vs) * sdir) < 0)
+                               {
+                                       vs = '0 0 0';
+                               }
+                       }
+               }
+       }
+
+       if (is_add_z)
+       {
+               float vz_add = pushed_entity.velocity.z;
+               if (is_pushed)
+               {
+                       vz = vz_add;
+               }
+               else
+               {
+                       vz += vz_add;
+
+                       if (is_clamp_negative_adds)
+                       {
+                               if (signbit(vz) != signbit(zdir))
+                               {
+                                       vz = 0;
+                               }
+                       }
+               }
+       }
+
+       return vs + '0 0 1' * vz;
+}
+
+// TODO: move this check to player/projectile physics?
+void check_pushed(entity this) // first jump pad to think thinks for every jump pad
+{
+       IL_EACH(are_pushed, true,
+       {
+               bool empty = true;
+               for(int i = 0; i < MAX_PUSHED; ++i)
+               {
+                       if(!it.has_pushed[i]) continue;
+                       if(WarpZoneLib_ExactTrigger_Touch(it.has_pushed[i], it))
+                       {
+                               it.has_pushed[i] = NULL;
+                               continue;
+                       }
+                       empty = false;
+               }
+               if(empty)
+               {
+                       IL_REMOVE(are_pushed, it);
+               }
+       });
+
+       if(!IL_EMPTY(are_pushed))
+       {
+               this.nextthink = time;
+       }
+}
+
+bool jumppad_push(entity this, entity targ, bool is_velocity_pad)
 {
        if (!isPushable(targ))
                return false;
 
        vector org = targ.origin;
 
-       if(STAT(Q3COMPAT))
+       if(STAT(Q3COMPAT, targ) || this.spawnflags & PUSH_STATIC)
        {
-               org.z += targ.mins_z;
-               org.z += 1; // off by 1!
+               org = (this.absmin + this.absmax) * 0.5;
+       }
+
+       bool is_pushed = false;
+       if(is_velocity_pad)
+       {
+               for(int i = 0; i < MAX_PUSHED; ++i)
+               {
+                       if(this == targ.has_pushed[i])
+                       {
+                               is_pushed = true;
+                               break;
+                       }
+               }
+
+               if(!is_pushed) // remember velocity jump pads
+               {
+                       bool limit_reached = true;
+                       for(int i = 0; i < MAX_PUSHED; ++i)
+                       {
+                               if(targ.has_pushed[i]) continue;
+                               limit_reached = false;
+                               targ.has_pushed[i] = this; // may be briefly out of sync between client and server if client prediction is toggled
+                               break;
+                       }
+                       if(limit_reached)
+                       {
+                               return false; // too many overlapping jump pads
+                       }
+                       IL_PUSH(are_pushed, targ);
+                       this.nextthink = time;
+               }
        }
 
        if(this.enemy)
        {
-               targ.velocity = trigger_push_calculatevelocity(org, this.enemy, this.height, targ);
+               if(!is_velocity_pad)
+               {
+                       targ.velocity = trigger_push_calculatevelocity(org, this.enemy, this.height, targ);
+               }
+               else
+               {
+                       targ.velocity = trigger_push_velocity_calculatevelocity(this, org, this.enemy, this.speed, this.count, targ, is_pushed);
+               }
        }
        else if(this.target && this.target != "")
        {
@@ -163,14 +334,31 @@ bool jumppad_push(entity this, entity targ)
                        else
                                RandomSelection_AddEnt(e, 1, 1);
                }
-               targ.velocity = trigger_push_calculatevelocity(org, RandomSelection_chosen_ent, this.height, targ);
+               if(!is_velocity_pad)
+               {
+                       targ.velocity = trigger_push_calculatevelocity(org, RandomSelection_chosen_ent, this.height, targ);
+               }
+               else
+               {
+                       targ.velocity = trigger_push_velocity_calculatevelocity(this, org, RandomSelection_chosen_ent, this.speed, this.count, targ, is_pushed);
+               }
        }
        else
        {
-               targ.velocity = this.movedir;
+               if(!is_velocity_pad)
+               {
+                       targ.velocity = this.movedir;
+               }
+               else
+               {
+#ifdef SVQC
+                       objerror (this, "Jumppad with no target");
+#endif
+                       return false;
+               }
        }
 
-       UNSET_ONGROUND(targ);
+       if(!is_velocity_pad) UNSET_ONGROUND(targ);
 
 #ifdef CSQC
        if (targ.flags & FL_PROJECTILE)
@@ -198,7 +386,7 @@ bool jumppad_push(entity this, entity targ)
 
                // prevent sound spam when a player hits the jumppad more than once
                // or when a dead player gets stuck in the jumppad for some reason
-               if(this.pushltime < time && !(IS_DEAD(targ) && targ.velocity == '0 0 0'))
+               if(!is_pushed && this.pushltime < time && !(IS_DEAD(targ) && targ.velocity == '0 0 0'))
                {
                        // flash when activated
                        Send_Effect(EFFECT_JUMPPAD, targ.origin, targ.velocity, 1);
@@ -275,7 +463,7 @@ void trigger_push_touch(entity this, entity toucher)
 
        EXACTTRIGGER_TOUCH(this, toucher);
 
-       noref bool success = jumppad_push(this, toucher);
+       noref bool success = jumppad_push(this, toucher, false);
 
 #ifdef SVQC
        if (success && (this.spawnflags & PUSH_ONCE))
@@ -287,6 +475,19 @@ void trigger_push_touch(entity this, entity toucher)
 #endif
 }
 
+void trigger_push_velocity_touch(entity this, entity toucher)
+{
+       if (this.active == ACTIVE_NOT)
+               return;
+
+       if(this.team && DIFF_TEAM(this, toucher))
+               return;
+
+       EXACTTRIGGER_TOUCH(this, toucher);
+
+       noref bool success = jumppad_push(this, toucher, true);
+}
+
 #ifdef SVQC
 void trigger_push_link(entity this);
 void trigger_push_updatelink(entity this);
@@ -582,6 +783,21 @@ float trigger_push_send(entity this, entity to, float sf)
        return true;
 }
 
+float trigger_push_velocity_send(entity this, entity to, float sf)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_TRIGGER_PUSH_VELOCITY);
+
+       WriteByte(MSG_ENTITY, this.team);
+       WriteInt24_t(MSG_ENTITY, this.spawnflags);
+       WriteByte(MSG_ENTITY, this.active);
+       WriteCoord(MSG_ENTITY, this.speed);
+       WriteCoord(MSG_ENTITY, this.count);
+
+       trigger_common_write(this, true);
+
+       return true;
+}
+
 void trigger_push_updatelink(entity this)
 {
        this.SendFlags |= SF_TRIGGER_INIT;
@@ -592,6 +808,11 @@ void trigger_push_link(entity this)
        trigger_link(this, trigger_push_send);
 }
 
+void trigger_push_velocity_link(entity this)
+{
+       trigger_link(this, trigger_push_velocity_send);
+}
+
 /*
  * ENTITY PARAMETERS:
  *
@@ -631,6 +852,31 @@ spawnfunc(trigger_push)
        InitializeEntity(this, trigger_push_findtarget, INITPRIO_FINDTARGET);
 }
 
+/*
+ * ENTITY PARAMETERS:
+ *
+ *   target:  this points to the target_position to which the player will jump.
+ *   speed:   XY speed for player-directional velocity pads - either sets or adds to the player's horizontal velocity.
+ *   count:   Z speed for player-directional velocity pads - either sets or adds to the player's vertical velocity.
+ */
+spawnfunc(trigger_push_velocity)
+{
+       trigger_init(this);
+
+       this.active = ACTIVE_ACTIVE;
+       this.use = trigger_push_use;
+       settouch(this, trigger_push_velocity_touch);
+
+       // normal push setup
+       if (!this.noise)
+               this.noise = "misc/jumppad.wav";
+       precache_sound (this.noise);
+
+       trigger_push_velocity_link(this); // link it now
+
+       setthink(this, check_pushed);
+}
+
 
 bool target_push_send(entity this, entity to, float sf)
 {
@@ -650,7 +896,7 @@ void target_push_use(entity this, entity actor, entity trigger)
        if(trigger.classname == "trigger_push" || trigger == this)
                return; // WTF, why is this a thing
 
-       jumppad_push(this, actor);
+       jumppad_push(this, actor, false);
 }
 
 void target_push_link(entity this)
@@ -712,6 +958,25 @@ NET_HANDLE(ENT_CLIENT_TRIGGER_PUSH, bool isnew)
        return true;
 }
 
+NET_HANDLE(ENT_CLIENT_TRIGGER_PUSH_VELOCITY, bool isnew)
+{
+       int mytm = ReadByte(); if(mytm) { this.team = mytm - 1; }
+       this.spawnflags = ReadInt24_t();
+       this.active = ReadByte();
+       this.speed = ReadCoord();
+       this.count = ReadCoord();
+
+       trigger_common_read(this, true);
+
+       this.entremove = trigger_remove_generic;
+       this.solid = SOLID_TRIGGER;
+       settouch(this, trigger_push_velocity_touch);
+       this.move_time = time;
+       setthink(this, check_pushed);
+
+       return true;
+}
+
 void target_push_remove(entity this)
 {
        // strfree(this.classname);
index 268134e806757aa4f459295ad818bb4a8db013bc..70845017c62051bda45c425400e10bc8a675cdfd 100644 (file)
@@ -3,6 +3,15 @@
 
 const int PUSH_ONCE = BIT(0); // legacy, deactivate with relay instead
 const int PUSH_SILENT = BIT(1); // not used?
+const int PUSH_STATIC = BIT(12);
+
+const int PUSH_VELOCITY_PLAYERDIR_XY = BIT(0);
+const int PUSH_VELOCITY_ADD_XY = BIT(1);
+const int PUSH_VELOCITY_PLAYERDIR_Z = BIT(2);
+const int PUSH_VELOCITY_ADD_Z = BIT(3);
+const int PUSH_VELOCITY_BIDIRECTIONAL_XY = BIT(4);
+const int PUSH_VELOCITY_BIDIRECTIONAL_Z = BIT(5);
+const int PUSH_VELOCITY_CLAMP_NEGATIVE_ADDS = BIT(6);
 
 IntrusiveList g_jumppads;
 STATIC_INIT(g_jumppads) { g_jumppads = IL_NEW(); }
@@ -11,6 +20,11 @@ STATIC_INIT(g_jumppads) { g_jumppads = IL_NEW(); }
 .bool istypefrag;
 .float height;
 
+const int MAX_PUSHED = 16; // maximum amount of jump pads which are allowed to push simultaneously
+.entity has_pushed[MAX_PUSHED];
+IntrusiveList are_pushed;
+STATIC_INIT(are_pushed) { are_pushed = IL_NEW(); }
+
 const int NUM_JUMPPADSUSED = 3;
 .float jumppadcount;
 .entity jumppadsused[NUM_JUMPPADSUSED];
@@ -43,7 +57,7 @@ bool trigger_push_test(entity this, entity item);
 void trigger_push_findtarget(entity this);
 
 /*
- * ENTITY PARAMETERS:
+ * ENTITY PARAMETERS trigger_push:
  *
  *   target:  target of jump
  *   height:  the absolute value is the height of the highest point of the jump
@@ -54,8 +68,17 @@ void trigger_push_findtarget(entity this);
  *            values to target a point on the ceiling.
  *   movedir: if target is not set, this * speed * 10 is the velocity to be reached.
  */
+
+/*
+ * ENTITY PARAMETERS trigger_push_velocity:
+ *
+ *   target:  this points to the target_position to which the player will jump.
+ *   speed:   XY speed for player-directional velocity pads - either sets or adds to the player's horizontal velocity.
+ *   count:   Z speed for player-directional velocity pads - either sets or adds to the player's vertical velocity.
+ */
 #ifdef SVQC
 spawnfunc(trigger_push);
+spawnfunc(trigger_push_velocity);
 
 spawnfunc(target_push);
 spawnfunc(info_notnull);
index ae82a82c1003a8b5da7f8e4e084143a919d5e770..42e1bd24fa06c4b5f93039725d81c461a579ffa7 100644 (file)
@@ -27,6 +27,9 @@ bool Teleport_Active(entity this, entity player)
 
        if(IS_TURRET(player))
                return false;
+
+       if((this.spawnflags & TELEPORT_SPECTATOR) && !IS_SPEC(player))
+               return false;
 #elif defined(CSQC)
        if(!IS_PLAYER(player))
                return false;
index 6f70f09beec2219624baeca92e2cd7deaa104fb4..ff3fa079bd6187ef089916baf58800cbba8ea74c 100644 (file)
@@ -1 +1,5 @@
 #pragma once
+
+
+const int TELEPORT_SPECTATOR = BIT(0);
+const int TELEPORT_KEEP_SPEED = BIT(1);
index 1d8c5e87f9aa043252c00dac81e78152066dd2b6..fe959f1ab7a6985a3dd1312206832c14b9c1d5b4 100644 (file)
@@ -64,7 +64,8 @@ METHOD(OverkillHeavyMachineGun, wr_think, void(entity thiswep, entity actor, .en
        {
                // Secondary uses it's own refire timer if refire_type is 1.
                actor.jump_interval = time + WEP_CVAR_SEC(okhmg, refire) * W_WeaponRateFactor(actor);
-               BLASTER_SECONDARY_ATTACK(okhmg, actor, weaponentity);
+               makevectors(actor.v_angle);
+               W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
                if ((actor.(weaponentity).wframe == WFRAME_IDLE) ||
                        (actor.(weaponentity).wframe == WFRAME_FIRE2))
                {
@@ -101,7 +102,8 @@ METHOD(OverkillHeavyMachineGun, wr_think, void(entity thiswep, entity actor, .en
                {
                        return;
                }
-               BLASTER_SECONDARY_ATTACK(okhmg, actor, weaponentity);
+               makevectors(actor.v_angle);
+               W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
                weapon_thinkf(actor, weaponentity, WFRAME_FIRE2, WEP_CVAR_SEC(okhmg, animtime), w_ready);
        }
 }
index a86cdc5194f3f2e21f7c936423a5bc41a913922b..7f204d1e10ea0a8328ad229e82dee527af73b003 100644 (file)
@@ -58,7 +58,8 @@ METHOD(OverkillMachineGun, wr_think, void(entity thiswep, entity actor, .entity
        {
                // Secondary uses it's own refire timer if refire_type is 1.
                actor.jump_interval = time + WEP_CVAR_SEC(okmachinegun, refire) * W_WeaponRateFactor(actor);
-               BLASTER_SECONDARY_ATTACK(okmachinegun, actor, weaponentity);
+               makevectors(actor.v_angle);
+               W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
                if ((actor.(weaponentity).wframe == WFRAME_IDLE) ||
                        (actor.(weaponentity).wframe == WFRAME_FIRE2))
                {
@@ -95,7 +96,8 @@ METHOD(OverkillMachineGun, wr_think, void(entity thiswep, entity actor, .entity
                {
                        return;
                }
-               BLASTER_SECONDARY_ATTACK(okmachinegun, actor, weaponentity);
+               makevectors(actor.v_angle);
+               W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
                weapon_thinkf(actor, weaponentity, WFRAME_FIRE2, WEP_CVAR_SEC(okmachinegun, animtime), w_ready);
        }
 }
index 85f33714054e1560f40c0ccf28a12a83b1b0c048..b24aac74f5a25d196fd955d144fe07637c9093b9 100644 (file)
@@ -141,7 +141,8 @@ METHOD(OverkillNex, wr_think, void(entity thiswep, entity actor, .entity weapone
        {
                // Secondary uses it's own refire timer if refire_type is 1.
                actor.jump_interval = time + WEP_CVAR_SEC(oknex, refire) * W_WeaponRateFactor(actor);
-               BLASTER_SECONDARY_ATTACK(oknex, actor, weaponentity);
+               makevectors(actor.v_angle);
+               W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
                if ((actor.(weaponentity).wframe == WFRAME_IDLE) ||
                        (actor.(weaponentity).wframe == WFRAME_FIRE2))
                {
@@ -180,7 +181,8 @@ METHOD(OverkillNex, wr_think, void(entity thiswep, entity actor, .entity weapone
                {
                        return;
                }
-               BLASTER_SECONDARY_ATTACK(oknex, actor, weaponentity);
+               makevectors(actor.v_angle);
+               W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
                weapon_thinkf(actor, weaponentity, WFRAME_FIRE2, WEP_CVAR_SEC(oknex, animtime), w_ready);
                return;
        }
index 4610a0b1d852924623d7d2e22b1985e07b1cc1d5..dca2610fe4498f5c9a59255560e89a5917416645 100644 (file)
@@ -139,7 +139,8 @@ METHOD(OverkillRocketPropelledChainsaw, wr_think, void(entity thiswep, entity ac
        {
                // Secondary uses it's own refire timer if refire_type is 1.
                actor.jump_interval = time + WEP_CVAR_SEC(okrpc, refire) * W_WeaponRateFactor(actor);
-               BLASTER_SECONDARY_ATTACK(okrpc, actor, weaponentity);
+               makevectors(actor.v_angle);
+               W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
                if ((actor.(weaponentity).wframe == WFRAME_IDLE) ||
                        (actor.(weaponentity).wframe == WFRAME_FIRE2))
                {
@@ -176,7 +177,8 @@ METHOD(OverkillRocketPropelledChainsaw, wr_think, void(entity thiswep, entity ac
                {
                        return;
                }
-               BLASTER_SECONDARY_ATTACK(okrpc, actor, weaponentity);
+               makevectors(actor.v_angle);
+               W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
                weapon_thinkf(actor, weaponentity, WFRAME_FIRE2, WEP_CVAR_SEC(okrpc, animtime), w_ready);
        }
 }
index 6b4b3aa8e76ee0d4063ff4d6816f72e0607bc377..cd1d618a8c9816adf00e3eda54096c96f285880b 100644 (file)
@@ -19,7 +19,8 @@ METHOD(OverkillShotgun, wr_think, void(entity thiswep, entity actor, .entity wea
        {
                // Secondary uses it's own refire timer if refire_type is 1.
                actor.jump_interval = time + WEP_CVAR_SEC(okshotgun, refire) * W_WeaponRateFactor(actor);
-               BLASTER_SECONDARY_ATTACK(okshotgun, actor, weaponentity);
+               makevectors(actor.v_angle);
+               W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
                if ((actor.(weaponentity).wframe == WFRAME_IDLE) ||
                        (actor.(weaponentity).wframe == WFRAME_FIRE2))
                {
@@ -63,7 +64,8 @@ METHOD(OverkillShotgun, wr_think, void(entity thiswep, entity actor, .entity wea
                {
                        return;
                }
-               BLASTER_SECONDARY_ATTACK(okshotgun, actor, weaponentity);
+               makevectors(actor.v_angle);
+               W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
                weapon_thinkf(actor, weaponentity, WFRAME_FIRE2, WEP_CVAR_SEC(okshotgun, animtime), w_ready);
        }
 }
index a2feb35dd8f98894f4cdc9a5ab4c01b694e59f56..519a526dbc71f5d85c33d3aa061f4489bdff8a26 100644 (file)
@@ -4,7 +4,7 @@
 void set_movetype(entity this, int mt)
 {
        this.move_movetype = mt;
-       this.move_qcphysics = (mt != MOVETYPE_PHYSICS);
+       this.move_qcphysics = (mt != MOVETYPE_PHYSICS && !use_engine_physics);
        if(!IL_CONTAINS(g_moveables, this))
                IL_PUSH(g_moveables, this); // add it to the moveable entities list (even if it doesn't move!) logic: if an object never sets its movetype, we assume it never does anything notable
        this.movetype = (this.move_qcphysics) ? MOVETYPE_QCENTITY : mt;
index e8cdc059beb3bed5f54da4d7b7ebcbff0e2ce780..db4c80d6001e5af82f1b26b19107c3e16a901518 100644 (file)
@@ -3,6 +3,8 @@
 #ifdef SVQC
 // undefined on client, engine cvar
 bool autocvar_physics_ode;
+
+bool use_engine_physics; // debug option for testing legacy engine code
 #endif
 
 // water levels
index 3449f2b53ee5b950b2d2916d8e593509decdece3..8ff5304d5fba965c628df1c827b5dcc7471304b4 100644 (file)
@@ -26,6 +26,11 @@ REGISTER_SP(RACE_LAPS);
 REGISTER_SP(RACE_TIME);
 REGISTER_SP(RACE_FASTEST);
 
+REGISTER_SP(CTS_STRAFE);
+REGISTER_SP(CTS_STARTSPEED);
+REGISTER_SP(CTS_AVGSPEED);
+REGISTER_SP(CTS_TOPSPEED);
+
 REGISTER_SP(ASSAULT_OBJECTIVES);
 
 REGISTER_SP(CTF_CAPS);
index 53f78cc5ad2d8b3b077441f9f0d8f5291c63a5e0..3e51fb26da1d4dbcf9e5e32740cdf5e8c93b4ac7 100644 (file)
@@ -354,7 +354,11 @@ bool autocvar_sv_slick_applygravity;
 #endif
 REGISTER_STAT(SLICK_APPLYGRAVITY, bool, autocvar_sv_slick_applygravity)
 
+#ifdef SVQC
+int autocvar_sv_q3compat_jumppads;
+#endif
 REGISTER_STAT(Q3COMPAT, int, q3compat)
+REGISTER_STAT(Q3COMPAT_JUMPPADS, int, autocvar_sv_q3compat_jumppads)
 
 #ifdef SVQC
 #include "physics/movetypes/movetypes.qh"
index 6902f48532d0e13ea3f1deb4c148530a28e63a2c..d603e09fe53a1fa403294c26fc12f78d6ddcc35c 100644 (file)
@@ -7,20 +7,21 @@ void W_Blaster_Touch(entity this, entity toucher)
        PROJECTILE_TOUCH(this, toucher);
 
        this.event_damage = func_null;
+       bool isprimary = !(this.projectiledeathtype & HITTYPE_SECONDARY);
 
        RadiusDamageForSource(
                this,
                (this.origin + (this.mins + this.maxs) * 0.5),
                this.velocity,
                this.realowner,
-               this.blaster_damage,
-               this.blaster_edgedamage,
-               this.blaster_radius,
+               WEP_CVAR_BOTH(blaster, isprimary, damage),
+               WEP_CVAR_BOTH(blaster, isprimary, edgedamage),
+               WEP_CVAR_BOTH(blaster, isprimary, radius),
                NULL,
                NULL,
                false,
-               this.blaster_force,
-               this.blaster_force_zscale,
+               WEP_CVAR_BOTH(blaster, isprimary, force),
+               WEP_CVAR_BOTH(blaster, isprimary, force_zscale),
                this.projectiledeathtype,
                this.weaponentity_fld,
                toucher
@@ -33,25 +34,19 @@ void W_Blaster_Think(entity this)
 {
        set_movetype(this, MOVETYPE_FLY);
        setthink(this, SUB_Remove);
-       this.nextthink = time + this.blaster_lifetime;
+       bool isprimary = !(this.projectiledeathtype & HITTYPE_SECONDARY);
+       this.nextthink = time + WEP_CVAR_BOTH(blaster, isprimary, lifetime);
        CSQCProjectile(this, true, PROJECTILE_BLASTER, true);
 }
 
 void W_Blaster_Attack(
        entity actor,
        .entity weaponentity,
-       float atk_deathtype,
-       float atk_shotangle,
-       float atk_damage,
-       float atk_edgedamage,
-       float atk_radius,
-       float atk_force,
-       float atk_force_zscale,
-       float atk_speed,
-       float atk_spread,
-       float atk_delay,
-       float atk_lifetime)
+       float atk_deathtype)
 {
+       bool isprimary = !(atk_deathtype & HITTYPE_SECONDARY);
+       float atk_shotangle = WEP_CVAR_BOTH(blaster, isprimary, shotangle);
+       float atk_damage = WEP_CVAR_BOTH(blaster, isprimary, damage);
        vector s_forward = v_forward * cos(atk_shotangle * DEG2RAD) + v_up * sin(atk_shotangle * DEG2RAD);
 
        W_SetupShot_Dir(actor, weaponentity, s_forward, false, 3, SND_LASERGUN_FIRE, CH_WEAPON_B, atk_damage, atk_deathtype);
@@ -63,26 +58,12 @@ void W_Blaster_Attack(
        missile.bot_dodgerating = atk_damage;
        PROJECTILE_MAKETRIGGER(missile);
 
-       missile.blaster_damage = atk_damage;
-       missile.blaster_edgedamage = atk_edgedamage;
-       missile.blaster_radius = atk_radius;
-       missile.blaster_force = atk_force;
-       missile.blaster_force_zscale = atk_force_zscale;
-       missile.blaster_lifetime = atk_lifetime;
-
        setorigin(missile, w_shotorg);
        setsize(missile, '0 0 0', '0 0 0');
 
-       W_SetupProjVelocity_Explicit(
-               missile,
-               w_shotdir,
-               v_up,
-               atk_speed,
-               0,
-               0,
-               atk_spread,
-               false
-       );
+       float atk_speed = WEP_CVAR_BOTH(blaster, isprimary, speed);
+       float atk_spread = WEP_CVAR_BOTH(blaster, isprimary, spread);
+       W_SetupProjVelocity_Explicit(missile, w_shotdir, v_up, atk_speed, 0, 0, atk_spread, false);
 
        missile.angles = vectoangles(missile.velocity);
 
@@ -97,7 +78,7 @@ void W_Blaster_Attack(
        missile.projectiledeathtype = atk_deathtype;
        missile.weaponentity_fld = weaponentity;
        setthink(missile, W_Blaster_Think);
-       missile.nextthink = time + atk_delay;
+       missile.nextthink = time + WEP_CVAR_BOTH(blaster, isprimary, delay);
 
        MUTATOR_CALLHOOK(EditProjectile, actor, missile);
 
@@ -126,21 +107,7 @@ METHOD(Blaster, wr_think, void(Blaster thiswep, entity actor, .entity weaponenti
     {
         if(weapon_prepareattack(thiswep, actor, weaponentity, false, WEP_CVAR_PRI(blaster, refire)))
         {
-            W_Blaster_Attack(
-                actor,
-                weaponentity,
-                WEP_BLASTER.m_id,
-                WEP_CVAR_PRI(blaster, shotangle),
-                WEP_CVAR_PRI(blaster, damage),
-                WEP_CVAR_PRI(blaster, edgedamage),
-                WEP_CVAR_PRI(blaster, radius),
-                WEP_CVAR_PRI(blaster, force),
-                WEP_CVAR_PRI(blaster, force_zscale),
-                WEP_CVAR_PRI(blaster, speed),
-                WEP_CVAR_PRI(blaster, spread),
-                WEP_CVAR_PRI(blaster, delay),
-                WEP_CVAR_PRI(blaster, lifetime)
-            );
+            W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id);
             weapon_thinkf(actor, weaponentity, WFRAME_FIRE1, WEP_CVAR_PRI(blaster, animtime), w_ready);
         }
     }
@@ -159,21 +126,7 @@ METHOD(Blaster, wr_think, void(Blaster thiswep, entity actor, .entity weaponenti
             {
                 if(weapon_prepareattack(thiswep, actor, weaponentity, true, WEP_CVAR_SEC(blaster, refire)))
                 {
-                    W_Blaster_Attack(
-                        actor,
-                        weaponentity,
-                        WEP_BLASTER.m_id | HITTYPE_SECONDARY,
-                        WEP_CVAR_SEC(blaster, shotangle),
-                        WEP_CVAR_SEC(blaster, damage),
-                        WEP_CVAR_SEC(blaster, edgedamage),
-                        WEP_CVAR_SEC(blaster, radius),
-                        WEP_CVAR_SEC(blaster, force),
-                        WEP_CVAR_SEC(blaster, force_zscale),
-                        WEP_CVAR_SEC(blaster, speed),
-                        WEP_CVAR_SEC(blaster, spread),
-                        WEP_CVAR_SEC(blaster, delay),
-                        WEP_CVAR_SEC(blaster, lifetime)
-                    );
+                    W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
                     weapon_thinkf(actor, weaponentity, WFRAME_FIRE2, WEP_CVAR_SEC(blaster, animtime), w_ready);
                 }
 
@@ -211,7 +164,8 @@ METHOD(OffhandBlaster, offhand_think, void(OffhandBlaster this, entity actor, bo
        }
        actor.jump_interval = time + WEP_CVAR_SEC(blaster, refire) * W_WeaponRateFactor(actor);
        .entity weaponentity = weaponentities[1];
-       BLASTER_SECONDARY_ATTACK(blaster, actor, weaponentity);
+       makevectors(actor.v_angle);
+       W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
 }
 
 #endif
index 6eebd4c39e6d6b60e34b4b051910c89430ce0f97..944e86405d595f3882532c0e887ef419e1b308c0 100644 (file)
@@ -55,32 +55,3 @@ CLASS(OffhandBlaster, OffhandWeapon)
 ENDCLASS(OffhandBlaster)
 OffhandBlaster OFFHAND_BLASTER;
 STATIC_INIT(OFFHAND_BLASTER) { OFFHAND_BLASTER = NEW(OffhandBlaster); }
-
-#ifdef SVQC
-.float blaster_damage;
-.float blaster_edgedamage;
-.float blaster_radius;
-.float blaster_force;
-.float blaster_force_zscale;
-.float blaster_lifetime;
-
-// Will be demacroed after WEP_CVAR macros are also demacroed.
-#define BLASTER_SECONDARY_ATTACK(weapon_name, actor, weaponentity) \
-       makevectors(actor.v_angle); \
-       W_Blaster_Attack( \
-               actor, \
-               weaponentity, \
-               WEP_BLASTER.m_id | HITTYPE_SECONDARY, \
-               WEP_CVAR_SEC(weapon_name, shotangle), \
-               WEP_CVAR_SEC(weapon_name, damage), \
-               WEP_CVAR_SEC(weapon_name, edgedamage), \
-               WEP_CVAR_SEC(weapon_name, radius), \
-               WEP_CVAR_SEC(weapon_name, force), \
-               WEP_CVAR_SEC(weapon_name, force_zscale), \
-               WEP_CVAR_SEC(weapon_name, speed), \
-               WEP_CVAR_SEC(weapon_name, spread), \
-               WEP_CVAR_SEC(weapon_name, delay), \
-               WEP_CVAR_SEC(weapon_name, lifetime) \
-       );
-
-#endif
index 86dd8fc929db6113253b97ddd37ae402cf31cc4a..fdeff1b6fa902d1919c0ba0e9af6697f33ddd56b 100644 (file)
@@ -532,7 +532,11 @@ METHOD(Crylink, wr_think, void(entity thiswep, entity actor, .entity weaponentit
         thiswep.wr_reload(thiswep, actor, weaponentity);
     }
 
-    if(fire & 1)
+    // attack swapping is useful for emulating BFG behavior in XDF
+    int primary_fire = autocvar_g_balance_crylink_swap_attacks ? fire & 2 : fire & 1;
+    int secondary_fire = autocvar_g_balance_crylink_swap_attacks ? fire & 1 : fire & 2;
+
+    if(primary_fire)
     {
         if(actor.(weaponentity).crylink_waitrelease != 1)
         if(weapon_prepareattack(thiswep, actor, weaponentity, false, WEP_CVAR_PRI(crylink, refire)))
@@ -542,7 +546,7 @@ METHOD(Crylink, wr_think, void(entity thiswep, entity actor, .entity weaponentit
         }
     }
 
-    if((fire & 2) && autocvar_g_balance_crylink_secondary)
+    if((secondary_fire) && autocvar_g_balance_crylink_secondary)
     {
         if(actor.(weaponentity).crylink_waitrelease != 2)
         if(weapon_prepareattack(thiswep, actor, weaponentity, true, WEP_CVAR_SEC(crylink, refire)))
@@ -552,7 +556,7 @@ METHOD(Crylink, wr_think, void(entity thiswep, entity actor, .entity weaponentit
         }
     }
 
-    if((actor.(weaponentity).crylink_waitrelease == 1 && !(fire & 1)) || (actor.(weaponentity).crylink_waitrelease == 2 && !(fire & 2)))
+    if((actor.(weaponentity).crylink_waitrelease == 1 && !(primary_fire)) || (actor.(weaponentity).crylink_waitrelease == 2 && !(secondary_fire)))
     {
         if(!actor.(weaponentity).crylink_lastgroup || time > actor.(weaponentity).crylink_lastgroup.teleport_time)
         {
index 2329c364e2998f16d425856292da6c6431b181e3..243407a00122e35a79a86f68ca60587da0d96c81 100644 (file)
@@ -49,6 +49,7 @@ CLASS(Crylink, Weapon)
         P(class, prefix, speed, float, BOTH) \
         P(class, prefix, spreadtype, float, SEC) \
         P(class, prefix, spread, float, BOTH) \
+        P(class, prefix, swap_attacks, float, NONE) \
         P(class, prefix, switchdelay_drop, float, NONE) \
         P(class, prefix, switchdelay_raise, float, NONE) \
         P(class, prefix, weaponreplace, string, NONE) \
index 25aa1b6658c62367fe0f9787f7230f2be7678a4c..a3b49ba614acb078da2c3192da5993e6b4303886 100644 (file)
@@ -297,7 +297,8 @@ METHOD(Vaporizer, wr_think, void(entity thiswep, entity actor, .entity weaponent
             if(WEP_CVAR_SEC(vaporizer, ammo))
                 W_DecreaseAmmo(thiswep, actor, WEP_CVAR_SEC(vaporizer, ammo), weaponentity);
 
-            BLASTER_SECONDARY_ATTACK(vaporizer, actor, weaponentity);
+            makevectors(actor.v_angle);
+            W_Blaster_Attack(actor, weaponentity, WEP_BLASTER.m_id | HITTYPE_SECONDARY);
 
             // now do normal refire
             weapon_thinkf(actor, weaponentity, WFRAME_FIRE2, WEP_CVAR_SEC(vaporizer, animtime), w_ready);
index 0344f3f1e5aa019ee7484738901ba25678e8b832..c94bef7630e297cc1d0d62d9eb3140f00a655716 100644 (file)
@@ -23,9 +23,8 @@ void StrafeHUD_ColorReset(entity btn, entity me)
     cvar_set("hud_panel_strafehud_angle_accel_color", cvar_defstring("hud_panel_strafehud_angle_accel_color"));
     cvar_set("hud_panel_strafehud_angle_neutral_color", cvar_defstring("hud_panel_strafehud_angle_neutral_color"));
     cvar_set("hud_panel_strafehud_angle_overturn_color", cvar_defstring("hud_panel_strafehud_angle_overturn_color"));
-    cvar_set("hud_panel_strafehud_switch_active_color", cvar_defstring("hud_panel_strafehud_switch_active_color"));
-    cvar_set("hud_panel_strafehud_switch_inactive_color", cvar_defstring("hud_panel_strafehud_switch_inactive_color"));
-    cvar_set("hud_panel_strafehud_direction_color", cvar_defstring("hud_panel_strafehud_direction_color"));
+    cvar_set("hud_panel_strafehud_switch_color", cvar_defstring("hud_panel_strafehud_switch_color"));
+    cvar_set("hud_panel_strafehud_bestangle_color", cvar_defstring("hud_panel_strafehud_bestangle_color"));
 }
 
 void XonoticHUDStrafeHUDDialog_fill(entity me)
@@ -121,32 +120,26 @@ void XonoticHUDStrafeHUDDialog_fill(entity me)
 
         me.TD(me, 1, 1.9, e = makeXonoticSlider(0, 1, 0.1, "hud_panel_strafehud_angle_alpha"));
     me.TR(me);
-        me.TD(me, 1, 1.9, e = makeXonoticTextLabel(0, _("Switch indicators:")));
+        me.TD(me, 1, 1.9, e = makeXonoticTextLabel(0, _("Switch indicator:")));
 
         me.TDempty(me, 0.2);
 
-        me.TD(me, 1, 1.9, e = makeXonoticTextLabel(0, _("Direction caps:")));
+        me.TD(me, 1, 1.9, e = makeXonoticTextLabel(0, _("Best angle indicator:")));
     me.TR(me);
-        me.TD(me, 1, 0.9, e = makeXonoticTextLabel(0, _("Active:")));
+        me.TD(me, 2, 1.9, e = makeXonoticColorpickerString("hud_panel_strafehud_switch_color", "hud_panel_strafehud_switch_color"));
 
         me.TDempty(me, 0.2);
 
-        me.TD(me, 1, 0.9, e = makeXonoticTextLabel(0, _("Inactive:")));
-    me.TR(me);
-        me.TD(me, 2, 0.85, e = makeXonoticColorpickerString("hud_panel_strafehud_switch_active_color", "hud_panel_strafehud_switch_active_color"));
-        me.TDempty(me, 0.2);
-        me.TD(me, 2, 0.85, e = makeXonoticColorpickerString("hud_panel_strafehud_switch_inactive_color", "hud_panel_strafehud_switch_inactive_color"));
-
-        me.TDempty(me, 0.2);
-
-        me.TD(me, 2, 1.9, e = makeXonoticColorpickerString("hud_panel_strafehud_direction_color", "hud_panel_strafehud_direction_color"));
+        me.TD(me, 2, 1.9, e = makeXonoticColorpickerString("hud_panel_strafehud_bestangle_color", "hud_panel_strafehud_bestangle_color"));
     me.TR(me);
     me.TR(me);
-        me.TD(me, 1, 0.9, e = makeXonoticSlider(0, 1, 0.1, "hud_panel_strafehud_switch_active_alpha"));
+        me.TD(me, 1, 0.9, e = makeXonoticSlider(0, 1, 0.1, "hud_panel_strafehud_switch_alpha"));
         me.TDempty(me, 0.1);
-        me.TD(me, 1, 0.9, e = makeXonoticSlider(0, 1, 0.1, "hud_panel_strafehud_switch_inactive_alpha"));
+        me.TD(me, 1, 0.9, e = makeXonoticCheckBoxString("1", "0", "hud_panel_strafehud_switch", _("Enable")));
 
         me.TDempty(me, 0.2);
 
-        me.TD(me, 1, 1.9, e = makeXonoticSlider(0, 1, 0.1, "hud_panel_strafehud_direction_alpha"));
+        me.TD(me, 1, 0.9, e = makeXonoticSlider(0, 1, 0.1, "hud_panel_strafehud_bestangle_alpha"));
+        me.TDempty(me, 0.1);
+        me.TD(me, 1, 0.9, e = makeXonoticCheckBoxString("1", "0", "hud_panel_strafehud_bestangle", _("Enable")));
 }
index f209b517be571c6346a9509d0212c9ef1e943403..50e18f9f57edf2dd5a90325e90de3e5ce6a5d1ce 100644 (file)
@@ -6,7 +6,7 @@ CLASS(XonoticHUDStrafeHUDDialog, XonoticRootDialog)
     ATTRIB(XonoticHUDStrafeHUDDialog, title, string, _("StrafeHUD Panel"));
     ATTRIB(XonoticHUDStrafeHUDDialog, color, vector, SKINCOLOR_DIALOG_TEAMSELECT);
     ATTRIB(XonoticHUDStrafeHUDDialog, intendedWidth, float, 0.4);
-    ATTRIB(XonoticHUDStrafeHUDDialog, rows, float, 23.5);
+    ATTRIB(XonoticHUDStrafeHUDDialog, rows, float, 22.5);
     ATTRIB(XonoticHUDStrafeHUDDialog, columns, float, 4);
     ATTRIB(XonoticHUDStrafeHUDDialog, name, string, "HUDstrafehud");
     ATTRIB(XonoticHUDStrafeHUDDialog, requiresConnection, float, true);
index c82e892f721815fdeff9c6b46d0260eee26fe86d..c62dbad50db98188ae950d32014101f13df51319 100644 (file)
@@ -25,6 +25,7 @@
 #include <server/scores_rules.qc>
 #include <server/spawnpoints.qc>
 #include <server/steerlib.qc>
+#include <server/strafe.qc>
 #include <server/teamplay.qc>
 #include <server/tests.qc>
 #include <server/world.qc>
index 52574efecc1ac689a2d9a341cc9a10a7d40c6943..cc41db238f1e65908a7a3c68f3a6957251d0e8ae 100644 (file)
@@ -25,6 +25,7 @@
 #include <server/scores_rules.qh>
 #include <server/spawnpoints.qh>
 #include <server/steerlib.qh>
+#include <server/strafe.qh>
 #include <server/teamplay.qh>
 #include <server/tests.qh>
 #include <server/world.qh>
index 799c3426f61f9c6ac32e9f91170230b61491f650..209040f9f4dc6313d4ca05fda1a69532d0849f72 100644 (file)
@@ -26,6 +26,7 @@
 #include <server/scores_rules.qh>
 #include <server/teamplay.qh>
 #include <server/world.qh>
+#include <lib/misc.qh>
 
 //  used by GameCommand_make_mapinfo()
 void make_mapinfo_Think(entity this)
@@ -1150,6 +1151,38 @@ void GameCommand_nospectators(int request)
        }
 }
 
+void GameCommand_printplayer(int request, int argc)
+{
+       switch (request)
+        {
+               case CMD_REQUEST_COMMAND:
+                {
+                       entity player = GetIndexedEntity(argc, 1);
+                        if (player.playerid)
+                        {
+                               GameLogEcho(strcat(
+                                                   strcat(
+                                                          ":playerinfo:", ftos(player.playerid),
+                                                          ":", ftos(etof(player)),
+                                                          ":", ftos(CS_CVAR(player).cvar_cl_allow_uidtracking),
+                                                          ":", ftos(CS_CVAR(player).cvar_cl_allow_uid2name)),
+                                                   strcat(
+                                                          ":", ftos(CS_CVAR(player).cvar_cl_allow_uidranking),
+                                                          ":", ((IS_REAL_CLIENT(player)) ? GameLog_ProcessIP(player.netaddress) : "bot"),
+                                                          ":", player.crypto_idfp,
+                                                          ":", playername(player.netname, player.team, false))));
+                        }
+                       return;
+                }
+               default:
+                case CMD_REQUEST_USAGE:
+                {
+                       LOG_HELP("Usage:^3 sv_cmd printplayer <player_entity_id>");
+                        return;
+                }
+        }
+}
+
 void GameCommand_printstats(int request)
 {
        switch (request)
@@ -1622,6 +1655,36 @@ void GameCommand_warp(int request, int argc)
        }
 }
 
+void IRCSay(string msgstr)
+{
+       if(msgstr == "")
+               return;
+
+       string prefix;
+       if(substring(msgstr, 0, 3) == "^4*") // actions
+               prefix = "\{3}";
+       else
+               prefix = "\{1}";
+
+       msgstr = strcat(prefix, strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint
+
+       FOREACH_CLIENTSLOT(true,
+       {
+               if(!intermission_running)
+               if((autocvar_g_chat_nospectators == 1) || (autocvar_g_chat_nospectators == 2 && !(warmup_stage || game_stopped)))
+               if(IS_PLAYER(it))
+                       continue;
+               if(IS_REAL_CLIENT(it))
+                       sprint(it, msgstr);
+       });
+}
+
+void GameCommand_ircmsg(int request, int argc, string command)
+{
+       IRCSay(substring(command, strlen(argv(0))+1, strlen(command)));
+        return;
+}
+
 /* use this when creating a new command, making sure to place it in alphabetical order... also,
 ** ADD ALL NEW COMMANDS TO commands.cfg WITH PROPER ALIASES IN THE SAME FASHION!
 void GameCommand_(int request)
@@ -1668,10 +1731,12 @@ SERVER_COMMAND(extendmatchtime, "Increase the timelimit value incrementally") {
 SERVER_COMMAND(gametype, "Simple command to change the active gametype") { GameCommand_gametype(request, arguments); }
 SERVER_COMMAND(gettaginfo, "Get specific information about a weapon model") { GameCommand_gettaginfo(request, arguments); }
 SERVER_COMMAND(gotomap, "Simple command to switch to another map") { GameCommand_gotomap(request, arguments); }
+SERVER_COMMAND(ircmsg, "Utility function to forward chat messages from IRC/discord/whatever") { GameCommand_ircmsg(request, arguments, command); }
 SERVER_COMMAND(lockteams, "Disable the ability for players to switch or enter teams") { GameCommand_lockteams(request); }
 SERVER_COMMAND(make_mapinfo, "Automatically rebuild mapinfo files") { GameCommand_make_mapinfo(request); }
 SERVER_COMMAND(moveplayer, "Change the team/status of a player") { GameCommand_moveplayer(request, arguments); }
 SERVER_COMMAND(nospectators, "Automatically remove spectators from a match") { GameCommand_nospectators(request); }
+SERVER_COMMAND(printplayer, "Print information about a player") { GameCommand_printplayer(request, arguments); }
 SERVER_COMMAND(printstats, "Dump eventlog player stats and other score information") { GameCommand_printstats(request); }
 SERVER_COMMAND(radarmap, "Generate a radar image of the map") { GameCommand_radarmap(request, arguments); }
 SERVER_COMMAND(reducematchtime, "Decrease the timelimit value incrementally") { GameCommand_reducematchtime(request); }
index 2b29422b8969362356b6a552106158c9b881d0e7..5f560022eecdcefaa4eb37ecc752cebc1f4fd648 100644 (file)
@@ -248,7 +248,7 @@ void remove_except_protected(entity e)
 void remove_unsafely(entity e)
 {
     if(e.classname == "spike")
-        error("Removing spikes is forbidden (crylink bug), please report");
+        LOG_WARN("Removing spikes is forbidden (crylink bug), please report");
     builtin_remove(e);
 }
 
index f8af3fc5f716c5ca12eb604c6f32ecd55c64d496..9286bffc3151592159a8dba2946ee3316b3d620b 100644 (file)
 #include <server/spawnpoints.qh>
 #include <server/weapons/common.qh>
 #include <server/world.qh>
+#include <server/strafe.qh>
 
 .string stored_netname; // TODO: store this information independently of race-based gamemodes
 
+.float race_startspeed;
+.float race_startspeed_best;
+.float race_avgspeed_sum;
+.float race_avgspeed_time;
+.float race_avgspeed_best;
+.float race_topspeed;
+.float race_topspeed_best;
+
 string uid2name(string myuid)
 {
        string s = db_get(ServerProgsDB, strcat("/uid2name/", myuid));
@@ -54,15 +63,23 @@ string uid2name(string myuid)
        return s;
 }
 
-void write_recordmarker(entity pl, float tstart, float dt)
+void write_recordmarker(entity pl, float newpos, float tstart, float dt)
 {
-    GameLogEcho(strcat(":recordset:", ftos(pl.playerid), ":", ftos(dt)));
+    GameLogEcho(strcat(":recordset:", ftos(newpos), ":", ftos(pl.playerid), ":", ftos(etof(pl)), ":", ftos(dt)));
 
     // also write a marker into demo files for demotc-race-record-extractor to find
-    stuffcmd(pl,
-             strcat(
-                 strcat("//", strconv(2, 0, 0, GetGametype()), " RECORD SET ", TIME_ENCODED_TOSTRING(TIME_ENCODE(dt), false)),
-                 " ", ftos(tstart), " ", ftos(dt), "\n"));
+    if (pl.crypto_idfp != "") {
+      stuffcmd(pl,
+               strcat(
+                   strcat("//", strconv(2, 0, 0, GetGametype()), " RECORD SET ", TIME_ENCODED_TOSTRING(TIME_ENCODE(dt), false)),
+                   strcat(" ", ftos(tstart), " ", ftos(dt), " ", ftos(newpos), " "),
+                   strcat(pl.crypto_idfp, "\n")));
+    } else {
+      stuffcmd(pl,
+               strcat(
+                   strcat("//", strconv(2, 0, 0, GetGametype()), " RECORD SET ", TIME_ENCODED_TOSTRING(TIME_ENCODE(dt), false)),
+                   strcat(" ", ftos(tstart), " ", ftos(dt), " ", ftos(newpos), " ANONYMOUS\n")));
+    }
 }
 
 IntrusiveList g_race_targets;
@@ -219,11 +236,19 @@ void race_SendNextCheckpoint(entity e, float spec) // qualifying only
                return;
 
        int cp = e.race_checkpoint;
-       float recordtime = race_checkpoint_records[cp];
-       float myrecordtime = e.race_checkpoint_record[cp];
-       string recordholder = race_checkpoint_recordholders[cp];
-       if(recordholder == e.netname)
+        float myrecordtime = e.race_checkpoint_record[cp];
+        float recordtime;
+        string recordholder;
+        if (autocvar_g_cts_cptimes_onlyself && (race_CheckpointNetworkID(cp) < 254)) {  // cp 254 - start line, cp 255 - finish line
+          recordtime = myrecordtime;
+          recordholder = "";
+        } else {
+          recordtime = race_checkpoint_records[cp];
+
+         recordholder = race_checkpoint_recordholders[cp];
+         if(recordholder == e.netname)
                recordholder = "";
+        }
 
        if(!IS_REAL_CLIENT(e))
                return;
@@ -394,6 +419,8 @@ void race_setTime(string map, float t, string myuid, string mynetname, entity e,
                return;
        }
 
+        write_recordmarker(e, newpos, time - TIME_DECODE(t), TIME_DECODE(t));
+
        // if we didn't hit a return yet, we have a new record!
 
        // if the player does not have a UID we can unfortunately not store the record, as the rankings system relies on UIDs
@@ -417,9 +444,8 @@ void race_setTime(string map, float t, string myuid, string mynetname, entity e,
        // store new ranking
        race_writeTime(GetMapname(), t, myuid);
 
-       if (newpos == 1 && showmessage)
+       if (showmessage)
        {
-               write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
                race_send_recordtime(MSG_ALL);
        }
 
@@ -486,7 +512,21 @@ void race_SendTime(entity e, float cp, float t, float tvalid)
        {
                int s = GameRules_scoring_add(e, RACE_FASTEST, 0);
                if(!s || t < s)
+               {
                        GameRules_scoring_add(e, RACE_FASTEST, t - s);
+
+                       e.strafe_efficiency_best = e.strafe_efficiency_sum / e.strafe_efficiency_time;
+                       PlayerScore_Set(e, SP_CTS_STRAFE, floor(e.strafe_efficiency_best * 1000 + .5));
+
+                       e.race_startspeed_best = e.race_startspeed;
+                       PlayerScore_Set(e, SP_CTS_STARTSPEED, floor(e.race_startspeed_best + .5));
+
+                       e.race_avgspeed_best = e.race_avgspeed_sum / e.race_avgspeed_time;
+                       PlayerScore_Set(e, SP_CTS_AVGSPEED, floor(e.race_avgspeed_best + .5));
+
+                       e.race_topspeed_best = e.race_topspeed;
+                       PlayerScore_Set(e, SP_CTS_TOPSPEED, floor(e.race_topspeed_best + .5));
+               }
                if(!g_race_qualifying)
                {
                        s = GameRules_scoring_add(e, RACE_TIME, 0);
@@ -518,12 +558,17 @@ void race_SendTime(entity e, float cp, float t, float tvalid)
 
                if(tvalid)
                {
-                       recordtime = race_checkpoint_records[cp];
-                       float myrecordtime = e.race_checkpoint_record[cp];
-                       recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
-                       if(recordholder == e.netname)
+                        float myrecordtime = e.race_checkpoint_record[cp];
+                        if (autocvar_g_cts_cptimes_onlyself && (race_CheckpointNetworkID(cp) < 254)) {  // cp 254 - start line, cp 255 - finish line
+                          recordtime = myrecordtime;
+                          recordholder = "";
+                        } else {
+                          recordtime = race_checkpoint_records[cp];
+                          recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it
+                          if(recordholder == e.netname)
                                recordholder = "";
 
+                        }
                        if(t != 0)
                        {
                                if(cp == race_timed_checkpoint)
@@ -771,6 +816,7 @@ void checkpoint_passed(entity this, entity player)
 
                if(!this.race_checkpoint) // start line
                {
+                       player.race_startspeed = vlen(vec2(player.velocity));
                        player.race_laptime = time;
                        player.race_movetime = player.race_movetime_frac = player.race_movetime_count = 0;
                        player.race_penalty_accumulator = 0;
index 1e851ee3041e7fd671397c917571292b5a5ef863..6cb7c6ad1f22c9977fd15ee13824c0b7bafd7b08 100644 (file)
@@ -8,6 +8,7 @@ float race_teams;
 const float ST_RACE_LAPS = 1;
 
 int autocvar_g_cts_send_rankings_cnt = 15;
+bool autocvar_g_cts_cptimes_onlyself = false;
 
 int g_race_qualifying;
 
@@ -34,7 +35,7 @@ float race_completing;
 .entity race_respawn_spotref; // try THIS spawn in case you respawn
 
 // definitions for functions used outside race.qc
-void write_recordmarker(entity pl, float tstart, float dt);
+void write_recordmarker(entity pl, float newpos, float tstart, float dt);
 
 float race_PreviousCheckpoint(float f);
 float race_NextCheckpoint(float f);
diff --git a/qcsrc/server/strafe.qc b/qcsrc/server/strafe.qc
new file mode 100644 (file)
index 0000000..c25b1ef
--- /dev/null
@@ -0,0 +1,188 @@
+#include "strafe.qh"
+
+#include <common/physics/movetypes/movetypes.qh>
+#include <common/physics/player.qh>
+#include <common/stats.qh>
+
+.float race_started;
+
+float calculate_strafe_efficiency(entity strafeplayer, vector movement, float dt)
+{
+    if(!strafeplayer) return 0;
+
+    bool swimming = strafeplayer.waterlevel >= WATERLEVEL_SWIMMING;
+    float speed = vlen(vec2(strafeplayer.velocity));
+
+    if(speed <= 0 || swimming || !strafeplayer.race_started) return 0; // only calculate the efficiency if all conditions are met
+    strafeplayer.strafe_efficiency_time += dt;
+
+    // physics
+    bool   onground                      = IS_ONGROUND(strafeplayer) && !(PHYS_INPUT_BUTTON_JUMP(strafeplayer) || PHYS_INPUT_BUTTON_JETPACK(strafeplayer));
+    bool   onslick                       = IS_ONSLICK(strafeplayer);
+    bool   strafekeys;
+    float  maxspeed_mod                  = IS_DUCKED(strafeplayer) ? .5 : 1;
+    float  maxspeed_phys                 = onground ? PHYS_MAXSPEED(strafeplayer) : PHYS_MAXAIRSPEED(strafeplayer);
+    float  maxspeed                      = maxspeed_phys * maxspeed_mod;
+    float  movespeed;
+    float  bestspeed;
+    float  maxaccel_phys                 = onground ? PHYS_ACCELERATE(strafeplayer) : PHYS_AIRACCELERATE(strafeplayer);
+    float  maxaccel                      = maxaccel_phys;
+    float  vel_angle                     = vectoangles(strafeplayer.velocity).y - (vectoangles(strafeplayer.velocity).y > 180 ? 360 : 0); // change the range from 0° - 360° to -180° - 180° to match how view_angle represents angles
+    float  view_angle                    = PHYS_INPUT_ANGLES(strafeplayer).y;
+    float  angle;
+    int    keys_fwd;
+    float  wishangle;
+    bool   fwd                           = true;
+
+    // determine whether the player is pressing forwards or backwards keys
+    if(movement.x > 0)
+    {
+        keys_fwd = 1;
+    }
+    else if(movement.x < 0)
+    {
+        keys_fwd = -1;
+    }
+    else
+    {
+        keys_fwd = 0;
+    }
+
+    // determine player wishdir
+    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 = -wishangle;
+            }
+        }
+    }
+
+    strafekeys = fabs(wishangle) == 90;
+
+    if(strafekeys && !onground && !swimming)
+    {
+        if(PHYS_MAXAIRSTRAFESPEED(strafeplayer) != 0)
+            maxspeed = min(PHYS_MAXAIRSTRAFESPEED(strafeplayer), PHYS_MAXAIRSPEED(strafeplayer) * maxspeed_mod);
+        if(PHYS_AIRSTRAFEACCELERATE(strafeplayer) != 0)
+            maxaccel = PHYS_AIRSTRAFEACCELERATE(strafeplayer);
+    }
+
+    movespeed = min(vlen(vec2(movement)), maxspeed);
+
+    maxaccel *= dt * movespeed;
+    bestspeed = max(movespeed - maxaccel, 0);
+
+    float strafespeed = speed; // speed minus friction
+
+    if((strafespeed > 0) && onground){
+        float strafefriction = onslick ? PHYS_FRICTION_SLICK(strafeplayer) : PHYS_FRICTION(strafeplayer);
+        float f = 1 - dt * strafefriction * max(PHYS_STOPSPEED(strafeplayer) / strafespeed, 1);
+
+        if(f <= 0)
+            strafespeed = 0;
+        else
+            strafespeed *= f;
+    }
+
+    // get current strafing angle ranging from -180° to +180°
+    // 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(!strafekeys)
+    {
+        if(keys_fwd > 0)
+        {
+            fwd = true;
+        }
+        else if(keys_fwd < 0)
+        {
+            fwd = false;
+        }
+        else
+        {
+            fwd = fabs(angle) <= 90;
+        }
+    }
+    // otherwise determine by examining the strafe angle
+    else
+    {
+        if(wishangle < 0) // detect direction 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;
+    }
+
+    // invert the wish angle when strafing backwards
+    if(!fwd)
+    {
+        wishangle = -wishangle;
+    }
+
+    // note about accuracy: a few ticks after dying do still have race_started set to true causing minimal interference in the efficiency total
+    float efficiency = 0;
+    float moveangle = fabs(angle + wishangle);
+    float bestangle = (strafespeed > bestspeed ? acos(bestspeed / strafespeed) : 0) * RAD2DEG;
+    float prebestangle = (strafespeed > movespeed ? acos(movespeed / strafespeed) : 0) * RAD2DEG;
+
+    if(fabs(vlen(vec2(movement))) > 0)
+    {
+        if(moveangle >= 90)
+        {
+            efficiency = (moveangle - 90) / 90;
+            if(efficiency > 1) efficiency = 2 - efficiency;
+            efficiency *= -1;
+        }
+        else if(moveangle >= bestangle)
+        {
+            efficiency = (90 - moveangle) / (90 - bestangle);
+        }
+        else if(moveangle >= prebestangle)
+        {
+            efficiency = (moveangle - prebestangle) / (bestangle - prebestangle);
+        }
+    }
+    return efficiency;
+}
diff --git a/qcsrc/server/strafe.qh b/qcsrc/server/strafe.qh
new file mode 100644 (file)
index 0000000..ceecbb5
--- /dev/null
@@ -0,0 +1,7 @@
+#pragma once
+
+.float strafe_efficiency_sum;
+.float strafe_efficiency_time;
+.float strafe_efficiency_best;
+
+float calculate_strafe_efficiency(entity, vector, float);
index 82cccae0f3b324fdcf0ce8ee999962c54772f483..1a4cd5e34dfff28d03bc295cddf9584f6db60ea9 100644 (file)
@@ -503,6 +503,7 @@ sv_gameplayfix_gravityunaffectedbyticrate 1
 sv_gameplayfix_nogravityonground 1
 
 set sv_q3compat_changehitbox 0 "use Q3 player hitbox dimensions and camera height on Q3 maps (maps with an entry in a .arena or .defi file)"
+set sv_q3compat_jumppads 1 "calculate the jump pad trajectory starting at the center of the push trigger instead of the player origin, \"0\" = never, \"1\" = on Q3 maps, \"2\" = on all maps"
 
 set g_movement_highspeed 1 "multiplier scale for movement speed (applies to sv_maxspeed and sv_maxairspeed, also applies to air acceleration when g_movement_highspeed_q3_compat is set to 0)"
 set g_movement_highspeed_q3_compat 0 "apply speed modifiers to air movement in a more Q3-compatible way (only apply speed buffs and g_movement_highspeed to max air speed, not to acceleration)"