#include "strafehud.qh"
#include <client/autocvars.qh>
-#include <client/miscfunctions.qh>
+#include <client/draw.qh>
+#include <client/hud/panel/racetimer.qh>
+#include <client/resources.qh>
#include <common/animdecide.qh>
#include <common/ent_cs.qh>
#include <common/mapinfo.qh>
float state_strafekeys_time = 0;
bool turn = false;
float turnangle;
+float turnspeed;
bool fwd = true;
bool state_fwd = true;
bool state_fwd_prev = true;
float state_fwd_time = 0;
+float starttime = 0;
+float startspeed = -1;
+float jumptime = 0;
+float jumpheight = -1;
+float jumpheight_persistent = -1;
+float jumpheight_prev = 0;
+float jumpspeed_prev = 0;
+bool jumprestart = true;
// provide basic panel cvars to old clients
// TODO remove them after a future release (0.8.2+)
{
if(!autocvar_hud_panel_strafehud ||
(spectatee_status == -1 && (autocvar_hud_panel_strafehud == 1 || autocvar_hud_panel_strafehud == 3)) ||
- (autocvar_hud_panel_strafehud == 3 && !(ISGAMETYPE(RACE) || ISGAMETYPE(CTS)))) return;
+ (autocvar_hud_panel_strafehud == 3 && !MUTATOR_CALLHOOK(HUD_StrafeHUD_showoptional))) return;
}
HUD_Panel_LoadCvars();
bool onground = islocal ? IS_ONGROUND(strafeplayer) : !(strafeplayer.anim_implicit_state & ANIMIMPLICITSTATE_INAIR);
bool strafekeys;
bool swimming = strafeplayer.waterlevel >= WATERLEVEL_SWIMMING;
+ bool spectating = entcs_GetSpecState(strafeplayer.sv_entnum) == ENTCS_SPEC_PURE;
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_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 = view_angles.y + 180;
+ float movespeed;
+ 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);
// HUD
int mode = autocvar_hud_panel_strafehud_mode >= 0 && autocvar_hud_panel_strafehud_mode <= 1 ? autocvar_hud_panel_strafehud_mode : 0;
+ float speed_conversion_factor = GetSpeedUnitFactor(autocvar_hud_panel_strafehud_unit);
+ float length_conversion_factor = GetLengthUnitFactor(autocvar_hud_panel_strafehud_unit);
+ string speed_unit = GetSpeedUnit(autocvar_hud_panel_strafehud_unit);
+ string length_unit = GetLengthUnit(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 accelzone_width;
float overturn_offset;
float overturn_width;
+ float slickdetector_height;
vector direction_size_vertical = '0 0 0';
vector direction_size_horizontal = '0 0 0';
float range_minangle;
}
}
- 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
}
state_strafekeys = strafekeys;
- if((keys & KEY_FORWARD) || (keys & KEY_BACKWARD) || swimming || autocvar__hud_configure)
+ if((!strafekeys && vlen(vec2(movement)) > 0) || swimming || autocvar__hud_configure)
{
turn = false;
}
{
turn = true; // CPMA turning
turnangle = wishangle;
+
+ // calculate the maximum air strafe speed
+ if(PHYS_MAXAIRSPEED(strafeplayer) == 0){
+ maxspeed = 0;
+ }
+ else if(PHYS_MAXAIRSTRAFESPEED(strafeplayer) == 0 || PHYS_MAXAIRSPEED(strafeplayer) <= PHYS_MAXAIRSTRAFESPEED(strafeplayer)){
+ maxspeed = PHYS_MAXAIRSPEED(strafeplayer);
+ }
+ else{
+ maxspeed = PHYS_MAXAIRSPEED(strafeplayer) * pow(fabs(PHYS_MAXAIRSTRAFESPEED(strafeplayer) / PHYS_MAXAIRSPEED(strafeplayer)), 1 - (90 - fabs(wishangle)) / 45); // no modifiers here because they don't affect air strafing
+ }
+
+ turnspeed = vlen(vec2(movement));
+ if(turnspeed == 0) turnspeed = maxspeed;
+ else turnspeed = min(turnspeed, maxspeed);
}
}
else if((time - state_strafekeys_time) >= autocvar_hud_panel_strafehud_timeout_turn) // timeout for jumping with strafe keys only
turn = false;
}
}
- if(turn)
+ if(turn && (onground || !strafekeys)) // retain last state until strafe turning times out
{
- maxspeed = PHYS_MAXAIRSTRAFESPEED(strafeplayer); // no modifiers here because they don't affect air strafing
wishangle = turnangle;
+ movespeed = turnspeed;
+ }
+ else{
+ movespeed = vlen(vec2(movement));
+ if(movespeed == 0) movespeed = maxspeed;
+ else movespeed = min(movespeed, maxspeed);
}
- minspeed = autocvar_hud_panel_strafehud_switch_minspeed < 0 ? maxspeed + antiflicker_speed : autocvar_hud_panel_strafehud_switch_minspeed;
+ minspeed = autocvar_hud_panel_strafehud_switch_minspeed < 0 ? movespeed + antiflicker_speed : autocvar_hud_panel_strafehud_switch_minspeed;
// get current strafing angle ranging from -180° to +180°
if(!autocvar__hud_configure)
// 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)
{
}
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
+ if((time - state_fwd_time) >= autocvar_hud_panel_strafehud_timeout_direction || speed < movespeed || ((fabs(wishangle) == 90) && mode == 0)) // timeout when changing between forwards and backwards movement
{
fwd = state_fwd;
}
}
else // simulate turning for HUD setup
{
+ fwd = true;
if(autocvar__hud_panel_strafehud_demo && ((time - demo_time) >= .025))
{
demo_time = time;
}
// best angle to strafe at
- bestangle = (speed > maxspeed ? acos(maxspeed / speed) : 0) * RAD2DEG * (direction < 0 ? -1 : 1);
+ bestangle = (speed > movespeed ? acos(movespeed / speed) : 0) * RAD2DEG * (direction < 0 ? -1 : 1);
odd_bestangle = -bestangle - wishangle;
bestangle -= wishangle;
}
}
- if(speed < (maxspeed + antiflicker_speed) && !immobile)
+ // 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
+ {
+ bool slickdetected = false;
+ if(PHYS_FRICTION(strafeplayer) != 0)
+ {
+ float slicksteps = 90 / pow(2, bound(0, autocvar_hud_panel_strafehud_slickdetector_granularity, 4));
+
+ slickdetected = IS_ONSLICK(strafeplayer); // 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 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
+ {
+ slickoffset.x = slickoffset.y = 0;
+ traceline(strafeplayer.origin, strafeplayer.origin + slickoffset, MOVE_WORLDONLY, NULL);
+ if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_SLICK) slickdetected = true;
+ }
+ }
+ }
+ else
+ {
+ slickdetected = true;
+ }
+
+ // if a traceline hit a slick surface
+ if(slickdetected)
+ {
+ vector slickdetector_size = panel_size;
+ slickdetector_size.y = slickdetector_height;
+ // top horizontal line
+ drawfill(panel_pos - eY * slickdetector_size.y, slickdetector_size, autocvar_hud_panel_strafehud_slickdetector_color, autocvar_hud_panel_strafehud_slickdetector_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+ // bottom horizontal line
+ drawfill(panel_pos + eY * panel_size.y, slickdetector_size, autocvar_hud_panel_strafehud_slickdetector_color, autocvar_hud_panel_strafehud_slickdetector_alpha * panel_fg_alpha, DRAWFLAG_NORMAL);
+ }
+ }
+
+ draw_beginBoldFont();
+ // show speed when crossing the start trigger
+ if(autocvar_hud_panel_strafehud_startspeed_fade > 0)
+ {
+ float text_alpha = 0;
+ 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)
+ {
+ 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);
+ 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);
+ }
+ }
+ else
+ {
+ starttime = 0;
+ startspeed = -1;
+ }
+
+ // experimental: show height achieved by a single jump (doesn't work in low gravity and may not be 100% accurate)
+ if(autocvar_hud_panel_strafehud_jumpheight_fade > 0)
+ {
+ float text_alpha = 0;
+ float jumpheight_min = max(autocvar_hud_panel_strafehud_jumpheight_min, 0);
+ float jumpheight_current = strafeplayer.origin.z;
+ float jumpspeed_current = strafeplayer.velocity.z;
+ if(jumpspeed_prev <= jumpspeed_current || jumpheight_prev > jumpheight_current || IS_ONGROUND(strafeplayer) || swimming || IS_DEAD(strafeplayer) || spectating)
+ {
+ // tries to catch kill and spectate but those are not reliable, should just hook to kill/spectate/teleport and reset jump height there
+ jumprestart = true;
+ }
+ else
+ {
+ if(jumpheight < 0 || jumprestart)
+ {
+ jumprestart = false;
+ jumpheight = 0;
+ }
+ else
+ {
+ jumpheight += jumpheight_current - jumpheight_prev;
+ }
+ if((jumpheight * length_conversion_factor) > jumpheight_min && jumpheight > jumpheight_persistent)
+ {
+ jumptime = time;
+ jumpheight_persistent = jumpheight;
+ }
+ }
+ jumpheight_prev = jumpheight_current;
+ jumpspeed_prev = jumpspeed_current;
+ if(jumpheight_persistent > 0)
+ {
+ text_alpha = cos(((time - jumptime) / autocvar_hud_panel_strafehud_jumpheight_fade) * 90 * DEG2RAD); // fade non-linear like the physics panel does
+ if((time - jumptime) > autocvar_hud_panel_strafehud_jumpheight_fade)
+ {
+ jumpheight_persistent = -1;
+ }
+ }
+ if(jumpheight_persistent > 0 && text_alpha > 0 && autocvar_hud_panel_strafehud_jumpheight_size > 0)
+ {
+ vector jumpheight_size = panel_size;
+ jumpheight_size.y = panel_size.y * min(autocvar_hud_panel_strafehud_jumpheight_size, 5);
+ drawstring_aspect(panel_pos - eY * jumpheight_size.y, strcat(ftos_decimals(jumpheight_persistent * length_conversion_factor, 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);
+ }
+ }
+ else
+ {
+ jumpheight_prev = jumpspeed_prev = 0;
+ jumpheight = jumpheight_persistent = -1;
+ }
+ draw_endBoldFont();
+
+ if(speed < (movespeed + antiflicker_speed) && !immobile)
{
bestangle_anywhere = true; // moving forward should suffice to gain speed
}
if(fabs(moveangle) > 90)
{
strafe_ratio = -((fabs(moveangle) - 90) / 90);
- if(strafe_ratio < -1) strafe_ratio = -1 - strafe_ratio%1;
+ if(strafe_ratio < -1) strafe_ratio = -2 - strafe_ratio;
}
else
{
float mirror_offset, mirror_width;
vector size = panel_size;
vector mirror_size = panel_size;
+ int gradient_start;
+ float gradient_offset, gradient_mirror_offset;
+ float overflow_width = 0, overflow_mirror_width = 0;
float original_width = width;
- float hiddencolor_width;
- if(alpha <= 0 || width <= 0) return;
+ if(alpha <= 0 && type != 2 || width <= 0) return;
if(type == 2 && gradientType == 0) type = 0;
mirror_width = min(offset + width - panel_size.x - hidden_width, width);
mirror_offset = max(offset - panel_size.x - hidden_width, 0);
}
+
+ if(width < 0) width = 0;
if((offset + width) > panel_size.x)
{
+ overflow_width = (offset + width) - panel_size.x;
width = panel_size.x - offset;
}
if(mirror_offset < 0)
mirror_width += mirror_offset;
mirror_offset = 0;
}
+
+ if(mirror_width < 0) 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;
}
- 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;
width += mirror_width;
mirror_width = width - mirror_width;
width -= mirror_width;
+
+ overflow_width += overflow_mirror_width;
+ overflow_mirror_width = overflow_width - overflow_mirror_width;
+ overflow_width -= overflow_mirror_width;
}
size.x = width;
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);
+ // determine whether the gradient starts in the mirrored or the non-mirrored area
+ 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, mirror_size, original_width, mirror_offset, alpha, gradient_mirror_offset, gradientType);
+ StrafeHUD_drawGradient(color, autocvar_hud_panel_strafehud_bar_neutral_color, size, original_width, offset, alpha, gradient_offset, gradientType);
}
}
void StrafeHUD_drawGradient(vector color1, vector color2, vector size, float original_width, float offset, float alpha, float gradientOffset, int gradientType)
{
- float ratio, alpha_ratio;
+ float color_ratio, alpha1, alpha2;
vector gradient_size = size;
- float color_ratio1 = autocvar_hud_panel_strafehud_bar_neutral_alpha == 0 ? 1 : min(min(alpha, 1)/min(autocvar_hud_panel_strafehud_bar_neutral_alpha, 1), 1);
- float color_ratio2 = alpha == 0 ? 1 : min(min(autocvar_hud_panel_strafehud_bar_neutral_alpha, 1)/min(alpha, 1), 1);
- vector gradient_color1 = gradientType == 2 ? StrafeHUD_mixColors(color2, color1, color_ratio1) : StrafeHUD_mixColors(color1, color2, color_ratio2);
- vector gradient_color2 = gradientType == 2 ? StrafeHUD_mixColors(color1, color2, color_ratio2) : StrafeHUD_mixColors(color2, color1, color_ratio1);
+ 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);
if(ratio > 1) ratio = 2 - ratio;
- alpha_ratio = ratio;
- if(gradientType != 2) alpha_ratio = 1 - alpha_ratio;
- drawfill(panel_pos + eX * (offset + i), gradient_size, StrafeHUD_mixColors(gradient_color1, gradient_color2, ratio), alpha - (alpha - autocvar_hud_panel_strafehud_bar_neutral_alpha) * alpha_ratio, DRAWFLAG_NORMAL);
+ if(gradientType != 2) 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);
}
}
+
+// 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;
+ }
+}
+
+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"));
+ }
+}