]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/client/hud/panel/strafehud.qc
strafehud: add strafe efficiency indicator
[xonotic/xonotic-data.pk3dir.git] / qcsrc / client / hud / panel / strafehud.qc
1 // Author: Juhu
2
3 #include "strafehud.qh"
4
5 #include <client/draw.qh>
6 #include <lib/csqcmodel/cl_player.qh>
7 #include <common/physics/player.qh>
8 #include <common/physics/movetypes/movetypes.qh>
9
10 // non-essential
11 #include <client/view.qh> // for v_flipped state
12
13 // non-local players
14 #include <common/animdecide.qh> // anim_implicit_state
15 #include <common/ent_cs.qh> // CSQCModel_server2csqc()
16
17 // start speed
18 #include <client/hud/panel/racetimer.qh> // checkpoint information (race_*)
19
20 // jump height
21 #include <lib/csqcmodel/common.qh> // for IS_PLAYER() macro
22 #include <common/resources/cl_resources.qh> // IS_DEAD() macro
23
24 // StrafeHUD (#25)
25
26 void HUD_StrafeHUD_Export(int fh)
27 {
28         // allow saving cvars that aesthetically change the panel into hud skin files
29 }
30
31 float GeomLerp(float a, float _lerp, float b); // declare GeomLerp here since there's no header file for it
32
33 void HUD_StrafeHUD()
34 {
35         static float hud_lasttime = 0;
36         entity strafeplayer;
37         bool islocal;
38
39         // generic hud routines
40         if(!autocvar__hud_configure)
41         {
42                 if(!autocvar_hud_panel_strafehud ||
43                    (spectatee_status == -1 && (autocvar_hud_panel_strafehud == 1 || autocvar_hud_panel_strafehud == 3)) ||
44                    (autocvar_hud_panel_strafehud == 3 && !MUTATOR_CALLHOOK(HUD_StrafeHUD_showoptional))) { hud_lasttime = time; return; }
45         }
46
47         HUD_Panel_LoadCvars();
48
49         if(autocvar_hud_panel_strafehud_dynamichud)
50                 HUD_Scale_Enable();
51         else
52                 HUD_Scale_Disable();
53
54         HUD_Panel_DrawBg();
55
56         if(panel_bg_padding)
57         {
58                 panel_pos  += '1 1 0' * panel_bg_padding;
59                 panel_size -= '2 2 0' * panel_bg_padding;
60         }
61
62         // find out whether the local csqcmodel entity is valid
63         if(spectatee_status > 0 || isdemo())
64         {
65                 islocal = false;
66                 strafeplayer = CSQCModel_server2csqc(player_localentnum - 1);
67         }
68         else
69         {
70                 islocal = true;
71                 strafeplayer = csqcplayer;
72         }
73
74         // draw strafehud
75         if(csqcplayer && strafeplayer)
76         {
77                 float strafe_waterlevel;
78
79                 // check the player waterlevel without affecting the player entity, this way we can fetch waterlevel even if client prediction is disabled
80                 {
81                         // store old values
82                         void old_contentstransition(int, int) = strafeplayer.contentstransition;
83                         float old_watertype = strafeplayer.watertype;
84                         float old_waterlevel = strafeplayer.waterlevel;
85
86                         strafeplayer.contentstransition = func_null; // unset the contentstransition function if present
87                         _Movetype_CheckWater(strafeplayer);
88                         strafe_waterlevel = strafeplayer.waterlevel; // store the player waterlevel
89
90                         // restore old values
91                         strafeplayer.contentstransition = old_contentstransition;
92                         strafeplayer.watertype = old_watertype;
93                         strafeplayer.waterlevel = old_waterlevel;
94                 }
95
96                 int keys = STAT(PRESSED_KEYS);
97                 // try to ignore if track_canjump is enabled, doesn't work in spectator mode if spectated player uses +jetpack or cl_movement_track_canjump
98                 bool jumpheld = false;
99                 if(islocal)
100                 {
101                         if((PHYS_INPUT_BUTTON_JUMP(strafeplayer) || PHYS_INPUT_BUTTON_JETPACK(strafeplayer)) && !PHYS_CL_TRACK_CANJUMP(strafeplayer))
102                                 jumpheld = true;
103                 }
104                 else
105                 {
106                         if((keys & KEY_JUMP) && !PHYS_TRACK_CANJUMP(strafeplayer))
107                                 jumpheld = true;
108                 }
109
110                 // persistent
111                 static float onground_lasttime       = 0;
112                 static bool  onslick_last            = false;
113                 static float turn_lasttime           = 0;
114                 static bool  turn                    = false;
115                 static float turnangle;
116                 static float dt_update               = 0;
117                 static int   dt_time                 = 0;
118                 static float dt_sum                  = 0;
119                 static float dt                      = 0;
120
121                 // physics
122                 // doesn't get changed by ground timeout and isn't affected by jump input
123                 bool   real_onground                 = islocal ? IS_ONGROUND(strafeplayer) : !(strafeplayer.anim_implicit_state & ANIMIMPLICITSTATE_INAIR);
124                 // doesn't get changed by ground timeout
125                 bool   real_onslick                  = false;
126                 // if jump is held assume we are in air, avoids flickering of the hud when hitting the ground
127                 bool   onground                      = real_onground && !jumpheld;
128                 bool   onslick                       = real_onslick;
129                 bool   onground_expired;
130                 bool   strafekeys;
131                 // the hud will not work well while swimming
132                 bool   swimming                      = strafe_waterlevel >= WATERLEVEL_SWIMMING;
133                 // use local csqcmodel entity for this even when spectating, flickers too much otherwise
134                 float  speed                         = !autocvar__hud_configure ? vlen(vec2(csqcplayer.velocity)) : 1337;
135                 // only the local csqcplayer entity contains this information even when spectating
136                 float  maxspeed_mod                  = IS_DUCKED(csqcplayer) ? .5 : 1;
137                 float  maxspeed_phys                 = onground ? PHYS_MAXSPEED(strafeplayer) : PHYS_MAXAIRSPEED(strafeplayer);
138                 float  maxspeed                      = !autocvar__hud_configure ? maxspeed_phys * maxspeed_mod : 320;
139                 float  movespeed;
140                 float  bestspeed;
141                 float  maxaccel_phys                 = onground ? PHYS_ACCELERATE(strafeplayer) : PHYS_AIRACCELERATE(strafeplayer);
142                 float  maxaccel                      = !autocvar__hud_configure ? maxaccel_phys : 1;
143                 // change the range from 0° - 360° to -180° - 180° to match how view_angle represents angles
144                 float  vel_angle                     = vectoangles(strafeplayer.velocity).y - (vectoangles(strafeplayer.velocity).y > 180 ? 360 : 0);
145                 float  view_angle                    = PHYS_INPUT_ANGLES(strafeplayer).y;
146                 float  angle;
147                 vector movement                      = PHYS_INPUT_MOVEVALUES(strafeplayer);
148                 bool   fwd;
149                 int    keys_fwd;
150                 float  wishangle;
151                 int    direction;
152
153                 // HUD
154                 int    mode;
155                 float  speed_conversion_factor       = GetSpeedUnitFactor(autocvar_hud_speed_unit);
156                 float  length_conversion_factor      = GetLengthUnitFactor(autocvar_hud_speed_unit);
157                 // use more decimals when displaying km or miles
158                 int    length_decimals               = autocvar_hud_speed_unit >= 3 && autocvar_hud_speed_unit <= 5 ? 6 : 2;
159                 float  antiflicker_angle             = bound(0, autocvar_hud_panel_strafehud_antiflicker_angle, 180);
160                 float  minspeed;
161                 float  shift_offset                  = 0;
162                 bool   straight_overturn             = false;
163                 bool   immobile                      = speed <= 0;
164                 float  hudangle;
165                 float  hidden_width;
166                 float  neutral_offset;
167                 float  neutral_width;
168                 vector currentangle_color            = autocvar_hud_panel_strafehud_angle_neutral_color;
169                 float  currentangle_offset;
170                 vector currentangle_size;
171                 float  bestangle;
172                 float  prebestangle;
173                 float  odd_bestangle;
174                 float  bestangle_offset;
175                 float  switch_bestangle_offset;
176                 bool   odd_angles                    = false;
177                 float  odd_bestangle_offset          = 0;
178                 float  switch_odd_bestangle_offset   = 0;
179                 float  bestangle_width;
180                 float  accelzone_left_offset;
181                 float  accelzone_right_offset;
182                 float  accelzone_width;
183                 float  preaccelzone_left_offset;
184                 float  preaccelzone_right_offset;
185                 float  preaccelzone_width;
186                 float  overturn_offset;
187                 float  overturn_width;
188                 float  slickdetector_height;
189                 vector direction_size_vertical;
190                 vector direction_size_horizontal;
191                 float  range_minangle;
192                 float  text_offset_top               = 0;
193                 float  text_offset_bottom            = 0;
194                 float  hfov                          = getproperty(VF_FOVX);
195
196                 // real_* variables which are always positive with no wishangle offset
197                 float real_bestangle;
198                 float real_prebestangle;
199
200                 if(autocvar_hud_panel_strafehud_mode >= 0 && autocvar_hud_panel_strafehud_mode <= 1)
201                         mode = autocvar_hud_panel_strafehud_mode;
202                 else
203                         mode = STRAFEHUD_MODE_VIEW_CENTERED;
204
205                 // there's only one size cvar for the arrows, they will always have a 45° angle to ensure proper rendering without antialiasing
206                 float arrow_size = max(panel_size.y * min(autocvar_hud_panel_strafehud_angle_arrow_size, 10), 0);
207
208                 if(onground)
209                 {
210                         if(PHYS_FRICTION(strafeplayer) == 0)
211                         {
212                                 onslick = true;
213                         }
214                         else // don't use IS_ONSLICK(), it only works for the local player and only if client prediction is enabled
215                         {
216                                 trace_dphitq3surfaceflags = 0;
217                                 tracebox(strafeplayer.origin, strafeplayer.mins, strafeplayer.maxs, strafeplayer.origin - '0 0 1', MOVE_NOMONSTERS, strafeplayer);
218                                 onslick = trace_dphitq3surfaceflags & Q3SURFACEFLAG_SLICK;
219                         }
220                         real_onslick = onslick;
221
222                         onground_lasttime = time;
223                         onslick_last = onslick;
224                 }
225                 else if(jumpheld || swimming)
226                 {
227                         onground_lasttime = 0;
228                 }
229
230                 if(onground_lasttime == 0)
231                         onground_expired = true;
232                 else
233                         onground_expired = (time - onground_lasttime) >= autocvar_hud_panel_strafehud_timeout_ground; // timeout for slick ramps
234
235                 if(!onground && !onground_expired) // if ground timeout hasn't expired yet use ground physics
236                 {
237                         onground = true;
238                         onslick = onslick_last;
239
240                         if(!autocvar__hud_configure)
241                         {
242                                 maxspeed = PHYS_MAXSPEED(strafeplayer) * maxspeed_mod;
243                                 maxaccel = PHYS_ACCELERATE(strafeplayer);
244                         }
245                 }
246
247                 movespeed = vlen(vec2(movement));
248                 if(movespeed == 0)
249                         movespeed = maxspeed;
250                 else
251                         movespeed = min(movespeed, maxspeed);
252
253                 if(!autocvar_hud_panel_strafehud_uncapped)
254                         arrow_size = max(arrow_size, 1);
255
256                 // determine frametime
257                 if((csqcplayer_status == CSQCPLAYERSTATUS_PREDICTED) && (input_timelength > 0))
258                 {
259                         float dt_client = input_timelength;
260
261                         if(dt_client > .05) // server splits frames longer than 50 ms into two moves
262                                 dt_client /= 2; // doesn't ensure frames are smaller than 50 ms, just splits large frames in half, matches server behaviour
263
264                         // calculate average frametime
265                         dt_sum += dt_client * dt_client;
266                         dt_time += dt_client;
267
268                         if(((time - dt_update) > autocvar_hud_panel_strafehud_fps_update) || (dt_update == 0))
269                         {
270                                 dt = dt_sum / dt_time;
271                                 dt_update = time;
272                                 dt_time = dt_sum = 0;
273                         }
274                 }
275                 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
276                 {
277                         dt = ticrate;
278                         dt_update = dt_time = dt_sum = 0;
279                 }
280
281                 // determine whether the player is pressing forwards or backwards keys
282                 if(islocal) // if entity is local player
283                 {
284                         if(movement.x > 0)
285                                 keys_fwd = STRAFEHUD_KEYS_FORWARD;
286                         else if(movement.x < 0)
287                                 keys_fwd = STRAFEHUD_KEYS_BACKWARD;
288                         else
289                                 keys_fwd = STRAFEHUD_KEYS_NONE;
290                 }
291                 else // alternatively determine direction by querying pressed keys
292                 {
293                         if((keys & KEY_FORWARD) && !(keys & KEY_BACKWARD))
294                                 keys_fwd = STRAFEHUD_KEYS_FORWARD;
295                         else if(!(keys & KEY_FORWARD) && (keys & KEY_BACKWARD))
296                                 keys_fwd = STRAFEHUD_KEYS_BACKWARD;
297                         else
298                                 keys_fwd = STRAFEHUD_KEYS_NONE;
299                 }
300
301                 // determine player wishdir
302                 if(islocal) // if entity is local player
303                 {
304                         if(movement.x == 0)
305                         {
306                                 if(movement.y < 0)
307                                         wishangle = -90;
308                                 else if(movement.y > 0)
309                                         wishangle = 90;
310                                 else
311                                         wishangle = 0;
312                         }
313                         else
314                         {
315                                 if(movement.y == 0)
316                                 {
317                                         wishangle = 0;
318                                 }
319                                 else
320                                 {
321                                         wishangle = RAD2DEG * atan2(movement.y, movement.x);
322                                         // wrap the wish angle if it exceeds ±90°
323                                         if(fabs(wishangle) > 90)
324                                         {
325                                                 if(wishangle < 0)
326                                                         wishangle += 180;
327                                                 else
328                                                         wishangle -= 180;
329
330                                                 wishangle *= -1;
331                                         }
332                                 }
333                         }
334                 }
335                 else // alternatively calculate wishdir by querying pressed keys
336                 {
337                         if(keys & KEY_FORWARD || keys & KEY_BACKWARD)
338                                 wishangle = 45;
339                         else
340                                 wishangle = 90;
341                         if(keys & KEY_LEFT)
342                                 wishangle *= -1;
343                         else if(!(keys & KEY_RIGHT))
344                                 wishangle = 0; // wraps at 180°
345                 }
346
347                 strafekeys = fabs(wishangle) > 45;
348
349                 // determine minimum required angle to display full strafe range
350                 range_minangle = fabs(wishangle) % 90; // maximum range is 90 degree
351                 if(range_minangle > 45) range_minangle = 45 - fabs(wishangle) % 45; // minimum angle range is 45
352                 range_minangle = 90 - range_minangle; // calculate value which is never >90 or <45
353                 range_minangle *= 2; // multiply to accommodate for both sides of the hud
354
355                 if(autocvar_hud_panel_strafehud_range == 0)
356                 {
357                         if(autocvar__hud_configure)
358                                 hudangle = 90;
359                         else
360                                 hudangle = range_minangle; // use minimum angle required if dynamically setting hud angle
361                 }
362                 else if(autocvar_hud_panel_strafehud_range < 0)
363                 {
364                         hudangle = hfov;
365                 }
366                 else
367                 {
368                         hudangle = bound(0, fabs(autocvar_hud_panel_strafehud_range), 360); // limit HUD range to 360 degrees, higher values don't make sense
369                 }
370
371                 // limit strafe-meter angle to values suitable for the current projection mode
372                 switch(autocvar_hud_panel_strafehud_projection)
373                 {
374                         case STRAFEHUD_PROJECTION_PERSPECTIVE:
375                                 hudangle = min(hudangle, 170);
376                                 break;
377                         case STRAFEHUD_PROJECTION_PANORAMIC:
378                                 hudangle = min(hudangle, 350);
379                                 break;
380                 }
381
382                 // detect air strafe turning
383                 if((!strafekeys && vlen(vec2(movement)) > 0) || onground || autocvar__hud_configure)
384                 {
385                         turn = false;
386                 }
387                 else // air strafe only
388                 {
389                         bool turn_expired = (time - turn_lasttime) >= autocvar_hud_panel_strafehud_timeout_turn; // timeout for jumping with strafe keys only
390
391                         if(strafekeys)
392                                 turn = true;
393                         else if(turn_expired)
394                                 turn = false;
395
396                         if(turn) // CPMA turning
397                         {
398                                 if(strafekeys)
399                                 {
400                                         turn_lasttime = time;
401                                         turnangle = wishangle;
402                                 }
403                                 else // retain last state until strafe turning times out
404                                 {
405                                         wishangle = turnangle;
406                                 }
407
408                                 // calculate the maximum air strafe speed and acceleration
409                                 float strafity = 1 - (90 - fabs(wishangle)) / 45;
410
411                                 if(PHYS_MAXAIRSTRAFESPEED(strafeplayer) != 0)
412                                         maxspeed = min(maxspeed, GeomLerp(PHYS_MAXAIRSPEED(strafeplayer), strafity, PHYS_MAXAIRSTRAFESPEED(strafeplayer)));
413
414                                 movespeed = min(movespeed, maxspeed);
415
416                                 if(PHYS_AIRSTRAFEACCELERATE(strafeplayer) != 0)
417                                         maxaccel = GeomLerp(PHYS_AIRACCELERATE(strafeplayer), strafity, PHYS_AIRSTRAFEACCELERATE(strafeplayer));
418                         }
419                 }
420
421                 maxaccel *= dt * movespeed;
422                 bestspeed = max(movespeed - maxaccel, 0); // target speed to gain maximum acceleration
423
424                 float frictionspeed; // speed lost from friction
425                 float strafespeed; // speed minus friction
426
427                 if((speed > 0) && onground)
428                 {
429                         float strafefriction = onslick ? PHYS_FRICTION_SLICK(strafeplayer) : PHYS_FRICTION(strafeplayer);
430
431                         frictionspeed = speed * dt * strafefriction * max(PHYS_STOPSPEED(strafeplayer) / speed, 1);
432                         strafespeed = max(speed - frictionspeed, 0);
433                 }
434                 else
435                 {
436                         frictionspeed = 0;
437                         strafespeed = speed;
438                 }
439
440                 minspeed = autocvar_hud_panel_strafehud_switch_minspeed;
441                 if(minspeed < 0)
442                         minspeed = bestspeed + frictionspeed;
443
444                 // get current strafing angle ranging from -180° to +180°
445                 if(!autocvar__hud_configure)
446                 {
447                         if(speed > 0)
448                         {
449                                 // calculate view angle relative to the players current velocity direction
450                                 angle = vel_angle - view_angle;
451
452                                 // if the angle goes above 180° or below -180° wrap it to the opposite side since we want the interior angle
453                                 if(angle > 180)
454                                         angle -= 360;
455                                 else if(angle < -180)
456                                         angle += 360;
457
458                                 // determine whether the player is strafing forwards or backwards
459                                 // if the player isn't strafe turning use forwards/backwards keys to determine direction
460                                 if(fabs(wishangle) != 90)
461                                 {
462                                         if(keys_fwd == STRAFEHUD_KEYS_FORWARD)
463                                                 fwd = true;
464                                         else if(keys_fwd == STRAFEHUD_KEYS_BACKWARD)
465                                                 fwd = false;
466                                         else
467                                                 fwd = fabs(angle) <= 90;
468                                 }
469                                 // otherwise determine by examining the strafe angle
470                                 else
471                                 {
472                                         if(wishangle < 0) // detect direction using wishangle since the direction is not yet set
473                                                 fwd = angle <= -wishangle;
474                                         else
475                                                 fwd = angle >= -wishangle;
476                                 }
477
478                                 // shift the strafe angle by 180° when strafing backwards
479                                 if(!fwd)
480                                 {
481                                         if(angle < 0)
482                                                 angle += 180;
483                                         else
484                                                 angle -= 180;
485                                 }
486
487                                 // don't make the angle indicator switch side too much at ±180° if anti flicker is turned on
488                                 if(angle > (180 - antiflicker_angle) || angle < (-180 + antiflicker_angle))
489                                         straight_overturn = true;
490                         }
491                         else
492                         {
493                                 angle = 0;
494                                 fwd = true;
495                         }
496                 }
497                 else // simulate turning for HUD setup
498                 {
499                         const float demo_maxangle = 55; // maximum angle before changing direction
500                         const float demo_turnspeed = 40; // turning speed in degrees per second
501
502                         static float demo_position = -37 / demo_maxangle; // current positioning value between -1 and +1
503
504                         if(autocvar__hud_panel_strafehud_demo)
505                         {
506                                 float demo_dt = time - hud_lasttime;
507                                 float demo_step = (demo_turnspeed / demo_maxangle) * demo_dt;
508                                 demo_position = ((demo_position + demo_step) % 4 + 4) % 4;
509                         }
510
511                         // triangle wave function
512                         if(demo_position > 3)
513                                 angle = -1 + (demo_position - 3);
514                         else if(demo_position > 1)
515                                 angle = +1 - (demo_position - 1);
516                         else
517                                 angle = demo_position;
518                         angle *= demo_maxangle;
519
520                         fwd = true;
521                         wishangle = 45;
522                         if(angle < 0)
523                                 wishangle *= -1;
524                 }
525
526                 // invert the wish angle when strafing backwards
527                 if(!fwd)
528                         wishangle *= -1;
529
530                 // flip angles if v_flipped is enabled
531                 if(autocvar_v_flipped)
532                 {
533                         angle *= -1;
534                         wishangle *= -1;
535                 }
536
537                 // determine whether the player is strafing left or right
538                 if(wishangle > 0)
539                 {
540                         direction = STRAFEHUD_DIRECTION_RIGHT;
541                 }
542                 else if(wishangle < 0)
543                 {
544                         direction = STRAFEHUD_DIRECTION_LEFT;
545                 }
546                 else
547                 {
548                         if(angle > antiflicker_angle && angle < (180 - antiflicker_angle))
549                                 direction = STRAFEHUD_DIRECTION_RIGHT;
550                         else if(angle < -antiflicker_angle && angle > (-180 + antiflicker_angle))
551                                 direction = STRAFEHUD_DIRECTION_LEFT;
552                         else
553                                 direction = STRAFEHUD_DIRECTION_NONE;
554                 }
555
556                 // best angle to strafe at
557                 // in case of ground friction we may decelerate if the acceleration is smaller than the speed loss from friction
558                 real_bestangle = bestangle = (strafespeed > bestspeed ? acos(bestspeed / strafespeed) * RAD2DEG : 0);
559                 real_prebestangle = prebestangle = (strafespeed > movespeed ? acos(movespeed / strafespeed) * RAD2DEG : 0);
560                 if(direction == STRAFEHUD_DIRECTION_LEFT) // the angle becomes negative in case we strafe left
561                 {
562                         bestangle *= -1;
563                         prebestangle *= -1;
564                 }
565                 odd_bestangle = -bestangle - wishangle;
566                 bestangle -= wishangle;
567                 prebestangle -= wishangle;
568
569                 // various offsets and size calculations of hud indicator elements
570                 // how much is hidden by the current hud angle
571                 hidden_width = (360 - hudangle) / hudangle * panel_size.x;
572
573                 // current angle
574                 currentangle_size.x = autocvar_hud_panel_strafehud_angle_width;
575                 currentangle_size.y = autocvar_hud_panel_strafehud_angle_height;
576                 currentangle_size.z = 0;
577                 if(!autocvar_hud_panel_strafehud_uncapped)
578                 {
579                         currentangle_size.x = min(currentangle_size.x, 10);
580                         currentangle_size.y = min(currentangle_size.y, 10);
581                 }
582                 currentangle_size.x *= panel_size.x;
583                 currentangle_size.y *= panel_size.y;
584                 if(!autocvar_hud_panel_strafehud_uncapped)
585                 {
586                         currentangle_size.x = max(currentangle_size.x, 1);
587                         currentangle_size.y = max(currentangle_size.y, 1);
588                 }
589                 else
590                 {
591                         currentangle_size.y = max(currentangle_size.y, 0);
592                 }
593                 if(mode == STRAFEHUD_MODE_VIEW_CENTERED)
594                         currentangle_offset = angle / hudangle * panel_size.x;
595                 else
596
597                         currentangle_offset = bound(-hudangle / 2, angle, hudangle / 2) / hudangle * panel_size.x + panel_size.x / 2;
598
599                 // best strafe acceleration angle
600                 bestangle_offset        =  bestangle / hudangle * panel_size.x + panel_size.x / 2;
601                 switch_bestangle_offset = -bestangle / hudangle * panel_size.x + panel_size.x / 2;
602                 bestangle_width = panel_size.x * autocvar_hud_panel_strafehud_switch_width;
603                 if(!autocvar_hud_panel_strafehud_uncapped)
604                         bestangle_width = max(bestangle_width, 1);
605
606                 if((angle > -wishangle && direction == STRAFEHUD_DIRECTION_LEFT) || (angle < -wishangle && direction == STRAFEHUD_DIRECTION_RIGHT))
607                 {
608                         odd_angles = true;
609                         odd_bestangle_offset = odd_bestangle / hudangle * panel_size.x + panel_size.x / 2;
610                         switch_odd_bestangle_offset = (odd_bestangle + bestangle * 2) / hudangle * panel_size.x + panel_size.x / 2;
611                 }
612                 // direction indicator
613                 direction_size_vertical.x = autocvar_hud_panel_strafehud_direction_width;
614                 if(!autocvar_hud_panel_strafehud_uncapped)
615                         direction_size_vertical.x = min(direction_size_vertical.x, 1);
616                 direction_size_vertical.x *= panel_size.y;
617                 if(!autocvar_hud_panel_strafehud_uncapped)
618                         direction_size_vertical.x = max(direction_size_vertical.x, 1);
619                 direction_size_vertical.y = panel_size.y + direction_size_vertical.x * 2;
620                 direction_size_vertical.z = 0;
621                 direction_size_horizontal.x = panel_size.x * min(autocvar_hud_panel_strafehud_direction_length, .5);
622                 direction_size_horizontal.y = direction_size_vertical.x;
623                 direction_size_horizontal.z = 0;
624
625                 // the neutral zone fills the whole strafe bar
626                 if(immobile)
627                 {
628                         // draw neutral zone
629                         if(panel_size.x > 0 && panel_size.y > 0 && autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha > 0)
630                         {
631                                 switch(autocvar_hud_panel_strafehud_style)
632                                 {
633                                         default:
634                                         case STRAFEHUD_STYLE_DRAWFILL:
635                                                 drawfill(
636                                                         panel_pos, panel_size,
637                                                         autocvar_hud_panel_strafehud_bar_neutral_color,
638                                                         autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha,
639                                                         DRAWFLAG_NORMAL);
640                                                 break;
641
642                                         case STRAFEHUD_STYLE_PROGRESSBAR:
643                                                 HUD_Panel_DrawProgressBar(
644                                                         panel_pos, panel_size, "progressbar", 1, 0, 0,
645                                                         autocvar_hud_panel_strafehud_bar_neutral_color,
646                                                         autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha,
647                                                         DRAWFLAG_NORMAL);
648                                 }
649                         }
650                 }
651                 else
652                 {
653                         // calculate various zones of the strafe-o-meter
654                         if(autocvar_hud_panel_strafehud_bar_preaccel)
655                                 preaccelzone_width = (fabs(bestangle - prebestangle)) / hudangle * panel_size.x;
656                         else
657                                 preaccelzone_width = 0;
658                         accelzone_width = (90 - fabs(bestangle + wishangle)) / hudangle * panel_size.x;
659                         overturn_width = 180 / hudangle * panel_size.x;
660                         neutral_width = 360 / hudangle * panel_size.x - accelzone_width * 2 - preaccelzone_width * 2 - overturn_width;
661
662                         {
663                                 float current_offset = 0;
664                                 preaccelzone_right_offset = current_offset;
665                                 current_offset += preaccelzone_width;
666
667                                 accelzone_right_offset = current_offset;
668                                 current_offset += accelzone_width;
669
670                                 overturn_offset = current_offset;
671                                 current_offset += overturn_width;
672
673                                 accelzone_left_offset = current_offset;
674                                 current_offset += accelzone_width;
675
676                                 preaccelzone_left_offset = current_offset;
677                                 current_offset += preaccelzone_width;
678
679                                 // the wrapping code may struggle if we always append it on the right side
680                                 neutral_offset = direction == STRAFEHUD_DIRECTION_LEFT ? current_offset : -neutral_width;
681                         }
682
683                         // shift hud if operating in view angle centered mode
684                         if(mode == STRAFEHUD_MODE_VIEW_CENTERED)
685                         {
686                                 shift_offset = -currentangle_offset;
687                                 bestangle_offset += shift_offset;
688                                 switch_bestangle_offset += shift_offset;
689                                 odd_bestangle_offset += shift_offset;
690                                 switch_odd_bestangle_offset += shift_offset;
691                         }
692                         if(direction == STRAFEHUD_DIRECTION_LEFT)
693                                 shift_offset += -360 / hudangle * panel_size.x;
694
695                         // calculate how far off-center the strafe zones currently are
696                         shift_offset += (panel_size.x + neutral_width) / 2 - wishangle / hudangle * panel_size.x;
697
698                         // shift strafe zones into correct place
699                         neutral_offset += shift_offset;
700                         accelzone_left_offset += shift_offset;
701                         accelzone_right_offset += shift_offset;
702                         preaccelzone_left_offset += shift_offset;
703                         preaccelzone_right_offset += shift_offset;
704                         overturn_offset += shift_offset;
705
706                         // draw left acceleration zone
707                         HUD_Panel_DrawStrafeHUD(
708                                 accelzone_left_offset, accelzone_width, hidden_width,
709                                 autocvar_hud_panel_strafehud_bar_accel_color,
710                                 autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha,
711                                 autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_LEFT,
712                                 true, hudangle);
713
714                         if(autocvar_hud_panel_strafehud_bar_preaccel)
715                                 HUD_Panel_DrawStrafeHUD(
716                                         preaccelzone_left_offset, preaccelzone_width, hidden_width,
717                                         autocvar_hud_panel_strafehud_bar_accel_color,
718                                         autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha,
719                                         autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_RIGHT,
720                                         true, hudangle);
721
722                         // draw right acceleration zone
723                         HUD_Panel_DrawStrafeHUD(
724                                 accelzone_right_offset, accelzone_width, hidden_width,
725                                 autocvar_hud_panel_strafehud_bar_accel_color,
726                                 autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha,
727                                 autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_RIGHT,
728                                 true, hudangle);
729
730                         if(autocvar_hud_panel_strafehud_bar_preaccel)
731                                 HUD_Panel_DrawStrafeHUD(
732                                         preaccelzone_right_offset, preaccelzone_width, hidden_width,
733                                         autocvar_hud_panel_strafehud_bar_accel_color,
734                                         autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha,
735                                         autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_LEFT,
736                                         true, hudangle);
737
738                         // draw overturn zone
739                         //   this is technically incorrect
740                         //   acceleration decreases at 90 degrees but speed loss happens a little bit after 90 degrees,
741                         //   however due to sv_airstopaccelerate that's hard to calculate
742                         HUD_Panel_DrawStrafeHUD(
743                                 overturn_offset, overturn_width, hidden_width,
744                                 autocvar_hud_panel_strafehud_bar_overturn_color,
745                                 autocvar_hud_panel_strafehud_bar_overturn_alpha * panel_fg_alpha,
746                                 autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_BOTH,
747                                 true, hudangle);
748
749                         // draw neutral zone
750                         HUD_Panel_DrawStrafeHUD(
751                                 neutral_offset, neutral_width, hidden_width,
752                                 autocvar_hud_panel_strafehud_bar_neutral_color,
753                                 autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha,
754                                 autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_NONE,
755                                 true, hudangle);
756
757                         // only draw indicators if minspeed is reached
758                         if(autocvar_hud_panel_strafehud_switch && speed >= minspeed && bestangle_width > 0 && autocvar_hud_panel_strafehud_switch_alpha > 0)
759                         {
760                                 // draw the switch indicator(s)
761                                 float offset = !odd_angles ? bestangle_offset : odd_bestangle_offset;
762                                 float switch_offset = !odd_angles ? switch_bestangle_offset : switch_odd_bestangle_offset;
763
764                                 offset = StrafeHUD_projectOffset(offset, hudangle, false);
765                                 switch_offset = StrafeHUD_projectOffset(switch_offset, hudangle, false);
766
767                                 // remove switch indicator width from offset
768                                 if(direction == STRAFEHUD_DIRECTION_LEFT)
769                                 {
770                                         if(!odd_angles)
771                                                 offset -= bestangle_width;
772                                         else
773                                                 switch_offset -= bestangle_width;
774                                 }
775                                 else
776                                 {
777                                         if(!odd_angles)
778                                                 switch_offset -= bestangle_width;
779                                         else
780                                                 offset -= bestangle_width;
781                                 }
782
783                                 HUD_Panel_DrawStrafeHUD(
784                                         switch_offset, bestangle_width, hidden_width,
785                                         autocvar_hud_panel_strafehud_switch_color,
786                                         autocvar_hud_panel_strafehud_switch_alpha * panel_fg_alpha,
787                                         STRAFEHUD_STYLE_DRAWFILL, STRAFEHUD_GRADIENT_NONE,
788                                         false, hudangle);
789
790                                 if(direction == STRAFEHUD_DIRECTION_NONE)
791                                         HUD_Panel_DrawStrafeHUD(
792                                                 offset, bestangle_width, hidden_width,
793                                                 autocvar_hud_panel_strafehud_switch_color,
794                                                 autocvar_hud_panel_strafehud_switch_alpha * panel_fg_alpha,
795                                                 STRAFEHUD_STYLE_DRAWFILL, STRAFEHUD_GRADIENT_NONE,
796                                                 false, hudangle);
797                         }
798                 }
799
800                 // slick detector
801                 slickdetector_height = max(autocvar_hud_panel_strafehud_slickdetector_height, 0);
802                 if(!autocvar_hud_panel_strafehud_uncapped)
803                         slickdetector_height = min(slickdetector_height, 1);
804                 slickdetector_height *= panel_size.y;
805                 if(autocvar_hud_panel_strafehud_slickdetector &&
806                    autocvar_hud_panel_strafehud_slickdetector_range > 0 &&
807                    autocvar_hud_panel_strafehud_slickdetector_alpha > 0 &&
808                    slickdetector_height > 0 &&
809                    panel_size.x > 0)
810                 {
811                         float slicksteps = max(autocvar_hud_panel_strafehud_slickdetector_granularity, 0);
812                         bool slickdetected = false;
813
814                         if(!autocvar_hud_panel_strafehud_uncapped)
815                                 slicksteps = min(slicksteps, 4);
816                         slicksteps = 90 / 2 ** slicksteps;
817
818                         slickdetected = real_onslick; // don't need to traceline if already touching slick
819
820                         // traceline into every direction
821                         trace_dphitq3surfaceflags = 0;
822                         vector traceorigin = strafeplayer.origin + eZ * strafeplayer.mins.z;
823                         for(float i = 0; i < 90 && !slickdetected; i += slicksteps)
824                         {
825                                 vector slickoffset;
826                                 float slickrotate;
827                                 slickoffset.z = -cos(i * DEG2RAD) * autocvar_hud_panel_strafehud_slickdetector_range;
828                                 slickrotate = sin(i * DEG2RAD) * autocvar_hud_panel_strafehud_slickdetector_range;
829
830                                 for(float j = 0; j < 360 && !slickdetected; j += slicksteps)
831                                 {
832                                         slickoffset.x = sin(j * DEG2RAD) * slickrotate;
833                                         slickoffset.y = cos(j * DEG2RAD) * slickrotate;
834
835                                         traceline(traceorigin, traceorigin + slickoffset, MOVE_NOMONSTERS, strafeplayer);
836                                         if((PHYS_FRICTION(strafeplayer) == 0 && trace_fraction < 1) || trace_dphitq3surfaceflags & Q3SURFACEFLAG_SLICK)
837                                                 slickdetected = true;
838                                         if(i == 0)
839                                                 break;
840                                 }
841                         }
842
843                         // if a traceline hit a slick surface
844                         if(slickdetected)
845                         {
846                                 vector slickdetector_size = panel_size;
847                                 slickdetector_size.y = slickdetector_height;
848
849                                 // top horizontal line
850                                 drawfill(
851                                         panel_pos - eY * slickdetector_size.y, slickdetector_size,
852                                         autocvar_hud_panel_strafehud_slickdetector_color,
853                                         autocvar_hud_panel_strafehud_slickdetector_alpha * panel_fg_alpha,
854                                         DRAWFLAG_NORMAL);
855
856                                 // bottom horizontal line
857                                 drawfill(
858                                         panel_pos + eY * panel_size.y,
859                                         slickdetector_size, autocvar_hud_panel_strafehud_slickdetector_color,
860                                         autocvar_hud_panel_strafehud_slickdetector_alpha * panel_fg_alpha,
861                                         DRAWFLAG_NORMAL);
862                         }
863
864                         text_offset_top = text_offset_bottom = slickdetector_height;
865                 }
866
867                 if(autocvar_hud_panel_strafehud_direction &&
868                    direction != STRAFEHUD_DIRECTION_NONE &&
869                    direction_size_vertical.x > 0 &&
870                    autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha > 0)
871                 {
872                         bool indicator_direction = direction == STRAFEHUD_DIRECTION_LEFT;
873                         // invert left/right when strafing backwards or when strafing towards the opposite side indicated by the direction variable
874                         // if both conditions are true then it's inverted twice hence not inverted at all
875                         if(!fwd != odd_angles)
876                                 indicator_direction = !indicator_direction;
877
878                         // draw the direction indicator caps at the sides of the hud
879                         // vertical line
880                         if(direction_size_vertical.y > 0)
881                                 drawfill(
882                                         panel_pos - eY * direction_size_horizontal.y + eX * (indicator_direction ? -direction_size_vertical.x : panel_size.x),
883                                         direction_size_vertical, autocvar_hud_panel_strafehud_direction_color,
884                                         autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha,
885                                         DRAWFLAG_NORMAL);
886
887                         // top horizontal line
888                         drawfill(
889                                 panel_pos + eX * (indicator_direction ? 0 : panel_size.x - direction_size_horizontal.x) - eY * direction_size_horizontal.y,
890                                 direction_size_horizontal, autocvar_hud_panel_strafehud_direction_color,
891                                 autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha,
892                                 DRAWFLAG_NORMAL);
893
894                         // bottom horizontal line
895                         drawfill(
896                                 panel_pos + eX * (indicator_direction ? 0 : panel_size.x - direction_size_horizontal.x) + eY * panel_size.y,
897                                 direction_size_horizontal, autocvar_hud_panel_strafehud_direction_color,
898                                 autocvar_hud_panel_strafehud_direction_alpha * panel_fg_alpha,
899                                 DRAWFLAG_NORMAL);
900                 }
901
902                 string newsound = autocvar_hud_panel_strafehud_sonar_audio;
903                 static string cursound = string_null;
904                 static string sonarsound = string_null;
905                 if(newsound == "")
906                 {
907                         cursound = sonarsound = string_null;
908                 }
909                 else if(newsound != cursound)
910                 {
911                         strfree(cursound);
912                         cursound = strzone(newsound);
913
914                         strfree(sonarsound);
915                         sonarsound = _Sound_fixpath(newsound);
916                         if(sonarsound)
917                         {
918                                 sonarsound = strzone(sonarsound);
919                                 precache_sound(sonarsound);
920                         }
921                 }
922
923                 // draw the actual strafe angle
924                 float strafe_ratio = 0;
925                 if(!immobile)
926                 {
927                         float moveangle = fabs(angle + wishangle);
928
929                         // player is overturning
930                         if(moveangle >= 90)
931                         {
932                                 currentangle_color = autocvar_hud_panel_strafehud_angle_overturn_color;
933                                 strafe_ratio = (moveangle - 90) / 90;
934                                 if(strafe_ratio > 1) strafe_ratio = 2 - strafe_ratio;
935                                 strafe_ratio *= -1;
936                         }
937                         // player gains speed by strafing
938                         else if(moveangle >= real_bestangle)
939                         {
940                                 currentangle_color = autocvar_hud_panel_strafehud_angle_accel_color;
941                                 strafe_ratio = (90 - moveangle) / (90 - real_bestangle);
942                         }
943                         else if(moveangle >= real_prebestangle)
944                         {
945                                 if(autocvar_hud_panel_strafehud_bar_preaccel)
946                                         currentangle_color = autocvar_hud_panel_strafehud_angle_accel_color;
947                                 strafe_ratio = (moveangle - real_prebestangle) / (real_bestangle - real_prebestangle);
948                         }
949
950                         if(autocvar_hud_panel_strafehud_style == STRAFEHUD_STYLE_GRADIENT)
951                                 currentangle_color = StrafeHUD_mixColors(autocvar_hud_panel_strafehud_angle_neutral_color, currentangle_color, fabs(strafe_ratio));
952
953                         // reuse strafe ratio for strafe sonar
954                         static float sonar_time = 0;
955
956                         float sonar_start = bound(0, autocvar_hud_panel_strafehud_sonar_start, 1);
957                         float sonar_ratio = strafe_ratio - sonar_start;
958                         if(sonar_start != 1)
959                                 sonar_ratio /= 1 - sonar_start;
960                         else
961                                 sonar_ratio = 1;
962
963                         float sonar_interval = max(0, autocvar_hud_panel_strafehud_sonar_interval_start);
964                         sonar_interval += autocvar_hud_panel_strafehud_sonar_interval_range * sonar_ratio ** max(1, autocvar_hud_panel_strafehud_sonar_interval_exponent);
965                         bool sonar_ready = (sonar_time == 0) || ((time - sonar_time) >= sonar_interval);
966                         if(autocvar_hud_panel_strafehud_sonar && sonar_ready && (strafe_ratio >= sonar_start))
967                         {
968                                 sonar_time = time;
969
970                                 float sonar_volume = bound(0, autocvar_hud_panel_strafehud_sonar_volume_start, 1);
971                                 sonar_volume += autocvar_hud_panel_strafehud_sonar_volume_range * sonar_ratio ** max(1, autocvar_hud_panel_strafehud_sonar_volume_exponent);
972
973                                 float sonar_pitch = max(0, autocvar_hud_panel_strafehud_sonar_pitch_start);
974                                 sonar_pitch += autocvar_hud_panel_strafehud_sonar_pitch_range * sonar_ratio ** max(1, autocvar_hud_panel_strafehud_sonar_pitch_exponent);
975
976                                 if(sonarsound && (sonar_volume > 0))
977                                         sound7(csqcplayer, CH_INFO, sonarsound, bound(0, sonar_volume, 1) * VOL_BASE, ATTN_NONE, max(0.000001, sonar_pitch * 100), 0);
978                         }
979                 }
980
981                 if(mode == STRAFEHUD_MODE_VIEW_CENTERED || straight_overturn)
982                         currentangle_offset = panel_size.x / 2;
983
984                 float angleheight_offset = currentangle_size.y;
985                 float ghost_offset = 0;
986                 if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE)
987                         ghost_offset = bound(0, (odd_angles ? odd_bestangle_offset : bestangle_offset), panel_size.x);
988
989                 currentangle_offset = StrafeHUD_projectOffset(currentangle_offset, hudangle, false);
990                 ghost_offset = StrafeHUD_projectOffset(ghost_offset, hudangle, false);
991
992                 switch(autocvar_hud_panel_strafehud_angle_style)
993                 {
994                         case STRAFEHUD_INDICATOR_SOLID:
995                                 if(currentangle_size.x > 0 && currentangle_size.y > 0)
996                                 {
997                                         if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE)
998                                                 drawfill(
999                                                         panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2) + eX * (ghost_offset - currentangle_size.x / 2),
1000                                                         currentangle_size, autocvar_hud_panel_strafehud_bestangle_color,
1001                                                         autocvar_hud_panel_strafehud_bestangle_alpha * panel_fg_alpha,
1002                                                         DRAWFLAG_NORMAL);
1003                                         drawfill(
1004                                                 panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2) + eX * (currentangle_offset - currentangle_size.x / 2),
1005                                                 currentangle_size, currentangle_color,
1006                                                 autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha,
1007                                                 DRAWFLAG_NORMAL);
1008                                 }
1009                                 break;
1010                         case STRAFEHUD_INDICATOR_DASHED:
1011                                 if(currentangle_size.x > 0 && currentangle_size.y > 0)
1012                                 {
1013                                         vector line_size = currentangle_size;
1014                                         line_size.y = currentangle_size.y / (bound(2, autocvar_hud_panel_strafehud_angle_dashes, currentangle_size.y) * 2 - 1);
1015                                         for(float i = 0; i < currentangle_size.y; i += line_size.y * 2)
1016                                         {
1017                                                 if(i + line_size.y * 2 >= currentangle_size.y)
1018                                                         line_size.y = currentangle_size.y - i;
1019                                                 if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE)
1020                                                         drawfill(
1021                                                                 panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2 - i) + eX * (ghost_offset - line_size.x / 2),
1022                                                                 line_size, autocvar_hud_panel_strafehud_bestangle_color,
1023                                                                 autocvar_hud_panel_strafehud_bestangle_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
1024                                                 drawfill(
1025                                                         panel_pos - eY * ((currentangle_size.y - panel_size.y) / 2 - i) + eX * (currentangle_offset - line_size.x / 2),
1026                                                         line_size, currentangle_color,
1027                                                         autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
1028                                         }
1029                                 }
1030                                 break;
1031                         case STRAFEHUD_INDICATOR_NONE:
1032                         default:
1033                                 // don't offset text and arrows if the angle indicator line isn't drawn
1034                                 angleheight_offset = panel_size.y;
1035                                 currentangle_size = '0 0 0';
1036                 }
1037
1038                 float angle_offset_top = 0, angle_offset_bottom = 0;
1039
1040                 // offset text if any angle indicator is drawn
1041                 if((autocvar_hud_panel_strafehud_angle_alpha > 0) ||
1042                    (autocvar_hud_panel_strafehud_bestangle && autocvar_hud_panel_strafehud_bestangle_alpha > 0))
1043                 {
1044                         // offset text by amount the angle indicator extrudes from the strafehud bar
1045                         angle_offset_top = angle_offset_bottom = (angleheight_offset - panel_size.y) / 2;
1046                 }
1047
1048                 if(autocvar_hud_panel_strafehud_angle_arrow > 0)
1049                 {
1050                         if(arrow_size > 0)
1051                         {
1052                                 if(autocvar_hud_panel_strafehud_angle_arrow == 1 || autocvar_hud_panel_strafehud_angle_arrow >= 3)
1053                                 {
1054                                         if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE)
1055                                                 StrafeHUD_drawStrafeArrow(
1056                                                         panel_pos + eY * ((panel_size.y - angleheight_offset) / 2) + eX * ghost_offset,
1057                                                         arrow_size, autocvar_hud_panel_strafehud_bestangle_color,
1058                                                         autocvar_hud_panel_strafehud_bestangle_alpha * panel_fg_alpha, true, currentangle_size.x);
1059                                         StrafeHUD_drawStrafeArrow(
1060                                                 panel_pos + eY * ((panel_size.y - angleheight_offset) / 2) + eX * currentangle_offset,
1061                                                 arrow_size, currentangle_color,
1062                                                 autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha, true, currentangle_size.x);
1063
1064                                         angle_offset_top += arrow_size; // further offset the top text offset if the top arrow is drawn
1065                                 }
1066                                 if(autocvar_hud_panel_strafehud_angle_arrow >= 2)
1067                                 {
1068                                         if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE)
1069                                                 StrafeHUD_drawStrafeArrow(
1070                                                         panel_pos + eY * ((panel_size.y - angleheight_offset) / 2 + angleheight_offset) + eX * ghost_offset,
1071                                                         arrow_size, autocvar_hud_panel_strafehud_bestangle_color,
1072                                                         autocvar_hud_panel_strafehud_bestangle_alpha * panel_fg_alpha, false, currentangle_size.x);
1073                                         StrafeHUD_drawStrafeArrow(
1074                                                 panel_pos + eY * ((panel_size.y - angleheight_offset) / 2 + angleheight_offset) + eX * currentangle_offset,
1075                                                 arrow_size, currentangle_color,
1076                                                 autocvar_hud_panel_strafehud_angle_alpha * panel_fg_alpha, false, currentangle_size.x);
1077
1078                                         angle_offset_bottom += arrow_size; // further offset the bottom text offset if the bottom arrow is drawn
1079                                 }
1080                         }
1081                 }
1082
1083                 // make sure text doesn't draw inside the strafehud bar
1084                 text_offset_top = max(angle_offset_top, text_offset_top);
1085                 text_offset_bottom = max(angle_offset_bottom, text_offset_bottom);
1086
1087                 // vertical angle for weapon jumps
1088                 {
1089                         if(autocvar_hud_panel_strafehud_vangle)
1090                         {
1091                                 float vangle = -PHYS_INPUT_ANGLES(strafeplayer).x;
1092                                 float vangle_height = autocvar_hud_panel_strafehud_vangle_size * panel_size.y;
1093                                 string vangle_text = strcat(ftos_decimals(vangle, 2), "°");
1094
1095                                 bool was_drawn = StrafeHUD_drawTextIndicator(
1096                                         vangle_text, vangle_height,
1097                                         autocvar_hud_panel_strafehud_vangle_color, 1,
1098                                         time, text_offset_bottom, STRAFEHUD_TEXT_BOTTOM);
1099
1100                                 if(was_drawn)
1101                                         text_offset_bottom += vangle_height;
1102                         }
1103                 }
1104
1105                 draw_beginBoldFont();
1106
1107                 // show speed when crossing the start trigger
1108                 {
1109                         static float startspeed = 0, starttime = 0; // displayed value and timestamp for fade out
1110
1111                         // check if the start trigger was hit (will also trigger if the finish trigger was hit if those have the same ID)
1112                         if((race_nextcheckpoint == 1) || (race_checkpoint == 254 && race_nextcheckpoint == 255))
1113                         {
1114                                 if((race_checkpointtime > 0) && (starttime != race_checkpointtime))
1115                                 {
1116                                         starttime = race_checkpointtime;
1117                                         startspeed = speed;
1118                                 }
1119                         }
1120
1121                         if(autocvar_hud_panel_strafehud_startspeed)
1122                         {
1123                                 float startspeed_height = autocvar_hud_panel_strafehud_startspeed_size * panel_size.y;
1124                                 string startspeed_text = ftos_decimals(startspeed * speed_conversion_factor, 2);
1125                                 if(autocvar_hud_panel_strafehud_unit_show)
1126                                         startspeed_text = strcat(startspeed_text, GetSpeedUnit(autocvar_hud_speed_unit));
1127
1128                                 bool was_drawn = StrafeHUD_drawTextIndicator(
1129                                         startspeed_text, startspeed_height,
1130                                         autocvar_hud_panel_strafehud_startspeed_color,
1131                                         autocvar_hud_panel_strafehud_startspeed_fade,
1132                                         starttime, text_offset_bottom, STRAFEHUD_TEXT_BOTTOM);
1133
1134                                 if(was_drawn)
1135                                         text_offset_bottom += startspeed_height;
1136                         }
1137                 }
1138
1139                 // strafe efficiency
1140                 {
1141                         if(autocvar_hud_panel_strafehud_strafeefficiency)
1142                         {
1143                                 float strafeeff_height = autocvar_hud_panel_strafehud_strafeefficiency_size * panel_size.y;
1144                                 string strafeeff_text = strcat(ftos_decimals(strafe_ratio * 100, 2), "%");
1145                             vector strafeeff_color = '1 1 1' - (strafe_ratio > 0 ? '1 0 1' : '0 1 1') * fabs(strafe_ratio);
1146
1147                                 bool was_drawn = StrafeHUD_drawTextIndicator(
1148                                         strafeeff_text, strafeeff_height,
1149                                         strafeeff_color, 1,
1150                                         time, text_offset_top, STRAFEHUD_TEXT_TOP);
1151
1152                                 if(was_drawn)
1153                                         text_offset_top += strafeeff_height;
1154                         }
1155                 }
1156
1157                 // show height achieved by a single jump
1158                 // FIXME: checking z position differences is unreliable (warpzones, teleporter, kill, etc) but using velocity to calculate jump height would be
1159                 //        inaccurate in hud code (possibly different tick rate than physics, doesn't run when hud isn't drawn, rounding errors)
1160                 {
1161                         static float height_min = 0, height_max = 0; // ground and peak of jump z coordinates
1162                         static float jumpheight = 0, jumptime = 0;   // displayed value and timestamp for fade out
1163
1164                         // tries to catch kill and spectate but those are not reliable
1165                         if((strafeplayer.velocity.z <= 0) || real_onground || swimming || IS_DEAD(strafeplayer) || !IS_PLAYER(strafeplayer))
1166                         {
1167                                 height_min = height_max = strafeplayer.origin.z;
1168                         }
1169                         else if(strafeplayer.origin.z > height_max)
1170                         {
1171                                 height_max = strafeplayer.origin.z;
1172                                 float jumpheight_new = height_max - height_min;
1173
1174                                 if((jumpheight_new * length_conversion_factor) > max(autocvar_hud_panel_strafehud_jumpheight_min, 0))
1175                                 {
1176                                         jumpheight = jumpheight_new;
1177                                         jumptime = time;
1178                                 }
1179                         }
1180
1181                         if(autocvar_hud_panel_strafehud_jumpheight)
1182                         {
1183                                 float jumpheight_height = autocvar_hud_panel_strafehud_jumpheight_size * panel_size.y;
1184                                 string jumpheight_text = ftos_decimals(jumpheight * length_conversion_factor, length_decimals);
1185                                 if(autocvar_hud_panel_strafehud_unit_show)
1186                                         jumpheight_text = strcat(jumpheight_text, GetLengthUnit(autocvar_hud_speed_unit));
1187
1188                                 bool was_drawn = StrafeHUD_drawTextIndicator(
1189                                         jumpheight_text, jumpheight_height,
1190                                         autocvar_hud_panel_strafehud_jumpheight_color,
1191                                         autocvar_hud_panel_strafehud_jumpheight_fade,
1192                                         jumptime, text_offset_top, STRAFEHUD_TEXT_TOP);
1193
1194                                 if(was_drawn)
1195                                         text_offset_top += jumpheight_height;
1196                         }
1197                 }
1198
1199                 draw_endBoldFont();
1200         }
1201         hud_lasttime = time;
1202 }
1203
1204 float StrafeHUD_projectOffset(float offset, float range, bool reverse)
1205 {
1206         range *= DEG2RAD / 2;
1207         float angle = (offset - (panel_size.x / 2)) / (panel_size.x / 2);
1208         switch(autocvar_hud_panel_strafehud_projection)
1209         {
1210                 default:
1211                 case STRAFEHUD_PROJECTION_LINEAR:
1212                         return offset;
1213                 case STRAFEHUD_PROJECTION_PERSPECTIVE:
1214                         if(!reverse)
1215                         {
1216                                 angle *= range;
1217                                 angle = tan(angle) / tan(range);
1218                         }
1219                         else
1220                         {
1221                                 angle = atan(angle * tan(range));
1222                                 angle /= range;
1223                         }
1224                         break;
1225                 case STRAFEHUD_PROJECTION_PANORAMIC:
1226                         if(!reverse)
1227                         {
1228                                 angle *= range;
1229                                 angle = tan(angle / 2) / tan(range / 2);
1230                         }
1231                         else
1232                         {
1233                                 angle = atan(angle * tan(range / 2)) * 2;
1234                                 angle /= range;
1235                         }
1236                         break;
1237         }
1238         offset = angle * (panel_size.x / 2) + (panel_size.x / 2);
1239         return offset;
1240 }
1241
1242 float StrafeHUD_projectWidth(float offset, float width, float range)
1243 {
1244         return StrafeHUD_projectOffset(offset + width, range, false) - StrafeHUD_projectOffset(offset, range, false);
1245 }
1246
1247 // functions to make hud elements align perfectly in the hud area
1248 void HUD_Panel_DrawStrafeHUD(float offset, float width, float hidden_width, vector color, float alpha, int type, int gradientType, bool doProject, float range)
1249 {
1250         float mirror_offset, mirror_width;
1251         vector size = panel_size;
1252         vector mirror_size = panel_size;
1253         float overflow_width = 0, overflow_mirror_width = 0;
1254         float original_width = width; // required for gradient
1255
1256         if(type == STRAFEHUD_STYLE_GRADIENT && gradientType == STRAFEHUD_GRADIENT_NONE)
1257                 type = STRAFEHUD_STYLE_DRAWFILL;
1258
1259         if(alpha <= 0 && type != STRAFEHUD_STYLE_GRADIENT || width <= 0)
1260                 return;
1261
1262         if(offset < 0)
1263         {
1264                 mirror_width = min(fabs(offset), width);
1265                 mirror_offset = panel_size.x + hidden_width - fabs(offset);
1266                 width += offset;
1267                 offset = 0;
1268         }
1269         else
1270         {
1271                 mirror_width = min(offset + width - panel_size.x - hidden_width, width);
1272                 mirror_offset = max(offset - panel_size.x - hidden_width, 0);
1273         }
1274
1275         width = max(width, 0);
1276         if((offset + width) > panel_size.x)
1277         {
1278                 overflow_width = (offset + width) - panel_size.x;
1279                 width = panel_size.x - offset;
1280         }
1281         size.x = width;
1282
1283         float original_offset = offset;
1284         if(doProject)
1285         {
1286                 if(size.x > 0) size.x = StrafeHUD_projectWidth(offset, size.x, range);
1287                 offset = StrafeHUD_projectOffset(offset, range, false);
1288         }
1289
1290         if(mirror_offset < 0)
1291         {
1292                 mirror_width += mirror_offset;
1293                 mirror_offset = 0;
1294         }
1295
1296         mirror_width = max(mirror_width, 0);
1297         if((mirror_offset + mirror_width) > panel_size.x)
1298         {
1299                 overflow_mirror_width = (mirror_offset + mirror_width) - panel_size.x;
1300                 mirror_width = panel_size.x - mirror_offset;
1301         }
1302         mirror_size.x = mirror_width;
1303
1304         float original_mirror_offset = mirror_offset;
1305         if(doProject)
1306         {
1307                 if(mirror_size.x > 0) mirror_size.x = StrafeHUD_projectWidth(mirror_offset, mirror_size.x, range);
1308                 mirror_offset = StrafeHUD_projectOffset(mirror_offset, range, false);
1309         }
1310
1311         switch(type)
1312         {
1313                 default:
1314                 case STRAFEHUD_STYLE_DRAWFILL: // no styling (drawfill)
1315                         if(mirror_size.x > 0 && mirror_size.y > 0)
1316                                 drawfill(panel_pos + eX * mirror_offset, mirror_size, color, alpha, DRAWFLAG_NORMAL);
1317                         if(size.x > 0 && size.y > 0)
1318                                 drawfill(panel_pos + eX * offset, size, color, alpha, DRAWFLAG_NORMAL);
1319                         break;
1320
1321                 case STRAFEHUD_STYLE_PROGRESSBAR: // progress bar style
1322                         if(mirror_size.x > 0 && mirror_size.y > 0)
1323                                 HUD_Panel_DrawProgressBar(
1324                                         panel_pos + eX * mirror_offset,
1325                                         mirror_size, "progressbar",
1326                                         1, 0, 0, color, alpha, DRAWFLAG_NORMAL);
1327                         if(size.x > 0 && size.y > 0)
1328                                 HUD_Panel_DrawProgressBar(
1329                                         panel_pos + eX * offset,
1330                                         size, "progressbar",
1331                                         1, 0, 0, color, alpha, DRAWFLAG_NORMAL);
1332                         break;
1333
1334                 case STRAFEHUD_STYLE_GRADIENT: // gradient style (types: 1 = left, 2 = right, 3 = both)
1335                         // determine whether the gradient starts in the mirrored or the non-mirrored area
1336                         int gradient_start;
1337                         float gradient_offset, gradient_mirror_offset;
1338
1339                         if(offset == 0 && mirror_offset == 0)
1340                                 gradient_start = width > mirror_width ? 2 : 1;
1341                         else if(offset == 0)
1342                                 gradient_start = 2;
1343                         else if(mirror_offset == 0)
1344                                 gradient_start = 1;
1345                         else
1346                                 gradient_start = 0;
1347
1348                         switch(gradient_start)
1349                         {
1350                                 default:
1351                                 case 0: // no offset required
1352                                         gradient_offset = gradient_mirror_offset = 0;
1353                                         break;
1354                                 case 1: // offset starts in non-mirrored area, mirrored area requires offset
1355                                         gradient_offset = 0;
1356                                         gradient_mirror_offset = original_width - (mirror_width + overflow_mirror_width);
1357                                         break;
1358                                 case 2: // offset starts in mirrored area, non-mirrored area requires offset
1359                                         gradient_offset = original_width - (width + overflow_width);
1360                                         gradient_mirror_offset = 0;
1361                         }
1362
1363                         StrafeHUD_drawGradient(
1364                                 color, autocvar_hud_panel_strafehud_bar_neutral_color,
1365                                 mirror_size, original_width, mirror_offset, original_mirror_offset,
1366                                 alpha, gradient_mirror_offset, gradientType, doProject, range);
1367
1368                         StrafeHUD_drawGradient(
1369                                 color, autocvar_hud_panel_strafehud_bar_neutral_color,
1370                                 size, original_width, offset, original_offset,
1371                                 alpha, gradient_offset, gradientType, doProject, range);
1372         }
1373 }
1374
1375 vector StrafeHUD_mixColors(vector color1, vector color2, float ratio)
1376 {
1377         vector mixedColor;
1378         if(ratio <= 0) return color1;
1379         if(ratio >= 1) return color2;
1380         mixedColor.x = color1.x + (color2.x - color1.x) * ratio;
1381         mixedColor.y = color1.y + (color2.y - color1.y) * ratio;
1382         mixedColor.z = color1.z + (color2.z - color1.z) * ratio;
1383         return mixedColor;
1384 }
1385
1386 void StrafeHUD_drawGradient(vector color1, vector color2, vector size, float original_width, float offset, float original_offset, float alpha, float gradientOffset, int gradientType, bool doProject, float range)
1387 {
1388         float color_ratio, alpha1, alpha2;
1389         vector segment_size = size;
1390         alpha1 = bound(0, alpha, 1);
1391         alpha2 = bound(0, autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha, 1);
1392         if((alpha1 + alpha2) == 0) return;
1393         color_ratio = alpha1 / (alpha1 + alpha2);
1394         for(int i = 0; i < size.x; ++i)
1395         {
1396                 float ratio, ratio_offset, alpha_ratio, combine_ratio1, combine_ratio2, segment_offset;
1397                 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
1398                 segment_offset = offset + i;
1399                 ratio_offset = segment_offset + segment_size.x / 2;
1400                 if(doProject)
1401                         ratio_offset = StrafeHUD_projectOffset(ratio_offset, range, true);
1402                 ratio_offset += gradientOffset;
1403                 ratio = (ratio_offset - original_offset) / original_width * (gradientType == STRAFEHUD_GRADIENT_BOTH ? 2 : 1);
1404                 if(ratio > 1) ratio = 2 - ratio;
1405                 if(gradientType != STRAFEHUD_GRADIENT_RIGHT) ratio = 1 - ratio;
1406                 alpha_ratio = alpha1 - (alpha1 - alpha2) * ratio;
1407                 combine_ratio1 = ratio * (1 - color_ratio);
1408                 combine_ratio2 = (1 - ratio) * color_ratio;
1409                 ratio = (combine_ratio1 + combine_ratio2) == 0 ? 1 : combine_ratio1 / (combine_ratio1 + combine_ratio2);
1410
1411                 if(alpha_ratio > 0)
1412                         drawfill(
1413                                 panel_pos + eX * segment_offset,
1414                                 segment_size,
1415                                 StrafeHUD_mixColors(color1, color2, ratio),
1416                                 alpha_ratio,
1417                                 DRAWFLAG_NORMAL);
1418         }
1419 }
1420
1421 // draw the strafe arrows (inspired by drawspritearrow() in common/mutators/mutator/waypoints/waypointsprites.qc)
1422 void StrafeHUD_drawStrafeArrow(vector origin, float size, vector color, float alpha, bool flipped, float connection_width)
1423 {
1424         origin = HUD_Shift(origin);
1425         float width = HUD_ScaleX(size * 2 + connection_width);
1426         float height = HUD_ScaleY(size);
1427         if(flipped) origin.y -= size;
1428         R_BeginPolygon("", DRAWFLAG_NORMAL, true);
1429         if(connection_width > 0)
1430         {
1431                 R_PolygonVertex(origin + (connection_width / 2 * eX) + (flipped ? height * eY : '0 0 0'), '0 0 0', color, alpha);
1432                 R_PolygonVertex(origin - (connection_width / 2 * eX) + (flipped ? height * eY : '0 0 0'), '0 0 0', color, alpha);
1433         }
1434         else
1435         {
1436                 R_PolygonVertex(origin + (flipped ? height * eY : '0 0 0'), '0 0 0', color, alpha);
1437         }
1438         R_PolygonVertex(origin + (flipped ? '0 0 0' : height * eY) - (width / 2) * eX, '0 0 0', color, alpha);
1439         R_PolygonVertex(origin + (flipped ? '0 0 0' : height * eY) + (width / 2) * eX, '0 0 0', color, alpha);
1440         R_EndPolygon();
1441 }
1442
1443 // draw a fading text indicator above or below the strafe meter, return true if something was displayed
1444 bool StrafeHUD_drawTextIndicator(string text, float height, vector color, float fadetime, float lasttime, float offset, int position)
1445 {
1446         if((height <= 0) || (lasttime <= 0) || (fadetime <= 0) || ((time - lasttime) >= fadetime))
1447                 return false;
1448
1449         float alpha = cos(((time - lasttime) / fadetime) * 90 * DEG2RAD); // fade non-linear like the physics panel does
1450         vector size = panel_size;
1451         size.y = height;
1452
1453         switch(position)
1454         {
1455                 case STRAFEHUD_TEXT_TOP:
1456                         offset += size.y;
1457                         offset *= -1;
1458                         break;
1459                 case STRAFEHUD_TEXT_BOTTOM:
1460                         offset += panel_size.y;
1461                         break;
1462         }
1463
1464         drawstring_aspect(panel_pos + eY * offset, text, size, color, alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
1465         return true;
1466 }
1467
1468 // length unit conversion (km and miles are only included to match the GetSpeedUnit* functions)
1469 float GetLengthUnitFactor(int length_unit)
1470 {
1471         switch(length_unit)
1472         {
1473                 default:
1474                 case 1: return 1.0;
1475                 case 2: return 0.0254;
1476                 case 3: return 0.0254 * 0.001;
1477                 case 4: return 0.0254 * 0.001 * 0.6213711922;
1478                 case 5: return 0.0254 * 0.001 * 0.5399568035;
1479         }
1480 }
1481
1482 string GetLengthUnit(int length_unit)
1483 {
1484         switch(length_unit)
1485         {
1486                 // translator-friendly strings without the initial space
1487                 default:
1488                 case 1: return strcat(" ", _("qu"));
1489                 case 2: return strcat(" ", _("m"));
1490                 case 3: return strcat(" ", _("km"));
1491                 case 4: return strcat(" ", _("mi"));
1492                 case 5: return strcat(" ", _("nmi"));
1493         }
1494 }