]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge remote-tracking branch 'origin/master' into morosophos/server-current4
authorNick S <nick@teichisma.info>
Thu, 29 Jun 2023 21:46:42 +0000 (00:46 +0300)
committerNick S <nick@teichisma.info>
Thu, 29 Jun 2023 21:46:42 +0000 (00:46 +0300)
38 files changed:
.gitlab-ci.yml
_hud_common.cfg
bal-wep-mario.cfg
bal-wep-nexuiz25.cfg
bal-wep-samual.cfg
bal-wep-xdf.cfg
bal-wep-xonotic.cfg
qcsrc/client/hud/panel/scoreboard.qc
qcsrc/client/hud/panel/strafehud.qc
qcsrc/client/hud/panel/strafehud.qh
qcsrc/common/command/generic.qc
qcsrc/common/constants.qh
qcsrc/common/gamemodes/gamemode/ctf/sv_ctf.qc
qcsrc/common/gamemodes/gamemode/cts/sv_cts.qc
qcsrc/common/mapobjects/func/door.qc
qcsrc/common/mapobjects/func/door.qh
qcsrc/common/mapobjects/func/plat.qc
qcsrc/common/mapobjects/platforms.qc
qcsrc/common/mapobjects/target/_mod.inc
qcsrc/common/mapobjects/target/_mod.qh
qcsrc/common/mapobjects/target/speed.qc [new file with mode: 0644]
qcsrc/common/mapobjects/target/speed.qh [new file with mode: 0644]
qcsrc/common/mapobjects/trigger/jumppads.qc
qcsrc/common/mapobjects/trigger/jumppads.qh
qcsrc/common/scores.qh
qcsrc/common/stats.qh
qcsrc/common/weapons/weapon/crylink.qh
qcsrc/server/_mod.inc
qcsrc/server/_mod.qh
qcsrc/server/command/sv_cmd.qc
qcsrc/server/compat/quake3.qc
qcsrc/server/damage.qc
qcsrc/server/main.qc
qcsrc/server/race.qc
qcsrc/server/race.qh
qcsrc/server/strafe.qc [new file with mode: 0644]
qcsrc/server/strafe.qh [new file with mode: 0644]
xonotic-server.cfg

index 5637179f1953643a4cbb29999eaa708d710038b1..5af7d56d147d1570d3be94d65115d859ea1e3522 100644 (file)
@@ -75,7 +75,7 @@ test_sv_game:
     - wget -nv -O data/maps/stormkeep.waypoints https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints
     - wget -nv -O data/maps/stormkeep.waypoints.cache https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints.cache
 
-    - EXPECT=57325fe74835910e451ba42d31de8f34
+    - EXPECT=0697ef57c3dca0ff122074b29495c5da
     - HASH=$(${ENGINE} +exec serverbench.cfg
       | tee /dev/stderr
       | grep '^:'
index 95f1e9758b0f10c48e20d3100e3adc23673e1172..418ab97774d0cb270b1bfb267c464b76fe1a7e86 100644 (file)
@@ -166,7 +166,7 @@ seta hud_panel_scoreboard_itemstats_showdelay_minpos 0.75 "delay displaying the
 
 seta _hud_panel_strafehud_demo "0" "strafehud changes angle during configure"
 seta hud_panel_strafehud_mode "0" "strafehud mode which controls whether the strafehud is centered at \"0\" = view angle, \"1\" = velocity angle"
-seta hud_panel_strafehud_range "90" "the angle range up to 360 degrees displayed on the strafehud, \"0\" = dynamic (chooses the minimum range required to still see the whole area needed for accelerating)"
+seta hud_panel_strafehud_range "90" "the angle range up to 360 degrees displayed on the strafehud, \"-1\" = current fov, \"0\" = dynamic (chooses the minimum range required to still see the whole area needed for accelerating)"
 seta hud_panel_strafehud_style "2" "\"0\" = no styling, \"1\" = progress bar style for the strafe bar, \"2\" = gradient for the strafe bar"
 seta hud_panel_strafehud_unit_show "1" "show units"
 seta hud_panel_strafehud_uncapped "0" "set to \"1\" to remove some safety restrictions, useful to set thinner indicator lines down to 1px or for trying out higher values for some performance degrading operations (warning: elements may turn invisible if too thin, other configurations may crash your game or look horribly ugly)"
@@ -219,6 +219,22 @@ seta hud_panel_strafehud_timeout_ground "0.1" "time (in seconds) after take off
 seta hud_panel_strafehud_timeout_turn "0.1" "time (in seconds) after releasing the strafe keys before changing mode (visually more consistent hud while switching between left/right strafe turning)"
 seta hud_panel_strafehud_antiflicker_angle "0.01" "how many degrees from 0° to 180° the hud ignores if it could cause visual disturbances otherwise (and to counter rounding errors)"
 seta hud_panel_strafehud_fps_update "0.5" "update interval (in seconds) of the frametime to calculate the optimal angle, smaller values may cause flickering"
+seta hud_panel_strafehud_sonar "0" "set to \"1\" to enable the strafe sonar"
+seta hud_panel_strafehud_sonar_audio "misc/talk" "audio to play for sonar"
+seta hud_panel_strafehud_sonar_start "0.5" "how optimal from 0 to 1 your strafing angle has to be for the strafe sonar to activate"
+seta hud_panel_strafehud_sonar_interval_start "0.333333" "strafe sonar sound interval in seconds"
+seta hud_panel_strafehud_sonar_interval_range "-0.222222" "dynamic sound interval range in seconds of the strafe sonar as you approach the optimal angle"
+seta hud_panel_strafehud_sonar_interval_exponent "1" "exponent of the dynamic sound interval range of the strafe sonar"
+seta hud_panel_strafehud_sonar_volume_start "0.333333" "sound volume of the strafe sonar"
+seta hud_panel_strafehud_sonar_volume_range "0.666666" "dynamic volume range of the strafe sonar as you approach the optimal angle"
+seta hud_panel_strafehud_sonar_volume_exponent "1" "exponent of the dynamic volume range of the strafe sonar"
+seta hud_panel_strafehud_sonar_pitch_start "0.9" "playback speed of the strafe sonar"
+seta hud_panel_strafehud_sonar_pitch_range "0.1" "dynamic playback speed range of the strafe sonar as you approach the optimal angle"
+seta hud_panel_strafehud_sonar_pitch_exponent "1" "exponent of the dynamic playback speed range of the strafe sonar"
+seta hud_panel_strafehud_vangle "0" "set to \"1\" to enable the vertical angle indicator"
+seta hud_panel_strafehud_vangle_color "0.75 0.75 0.75" "color of the vertical angle text"
+seta hud_panel_strafehud_vangle_size "1" "size of the vertical angle text (relative to the panel height)"
+seta hud_panel_strafehud_projection "0" "strafehud projection mode, \"0\" = linear, \"1\" = perspective, \"2\" = panoramic"
 
 // hud panel aliases
 alias quickmenu "cl_cmd hud quickmenu ${* ?}"
index 5cb22ec3c50aa4fa74073a31a31d3b2c529a407d..70c805199ecb69abddaf24717d19885b1e889c50 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 5
 set g_balance_crylink_secondary_speed 4000
 set g_balance_crylink_secondary_spread 0.08
 set g_balance_crylink_secondary_spreadtype 0
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0.2
 set g_balance_crylink_switchdelay_raise 0.2
 set g_balance_crylink_weaponreplace ""
index 17f5b31cab3b29b313e70f27606e6cf42bd7e082..e91d40b42e531892bbea48070e810c5f48dd98a9 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 7
 set g_balance_crylink_secondary_speed 7000
 set g_balance_crylink_secondary_spread 0.08
 set g_balance_crylink_secondary_spreadtype 0
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0.15
 set g_balance_crylink_switchdelay_raise 0.15
 set g_balance_crylink_weaponreplace ""
index 2799a2496b47fb4debea904aa6b051b95c59a621..e7e94f1e6ab3392e357e7469faa32c5d47d918a3 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 5
 set g_balance_crylink_secondary_speed 3000
 set g_balance_crylink_secondary_spread 0.01
 set g_balance_crylink_secondary_spreadtype 1
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0.2
 set g_balance_crylink_switchdelay_raise 0.2
 set g_balance_crylink_weaponreplace ""
index 09466acce81bcb3949c256133dce2a9675c89acc..ac85665d49fd9b3179c662d5ed2ed81fa4fd93dc 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 1
 set g_balance_crylink_secondary_speed 2000
 set g_balance_crylink_secondary_spread 0
 set g_balance_crylink_secondary_spreadtype 1
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0
 set g_balance_crylink_switchdelay_raise 0
 set g_balance_crylink_weaponreplace ""
index aab42015ca59c53da26e72ef4dce99531d90f597..fc0fc16a5d16ae5b349993c491cb784a6ee59d8c 100644 (file)
@@ -287,6 +287,7 @@ set g_balance_crylink_secondary_shots 5
 set g_balance_crylink_secondary_speed 3000
 set g_balance_crylink_secondary_spread 0.01
 set g_balance_crylink_secondary_spreadtype 1
+set g_balance_crylink_swap_attacks 0
 set g_balance_crylink_switchdelay_drop 0.2
 set g_balance_crylink_switchdelay_raise 0.2
 set g_balance_crylink_weaponreplace ""
index 35227ffab524c63bb077dfb624b4005f19ed4032..f3b1091bde9a1721dff195148af74fbdf09ca94c 100644 (file)
@@ -169,6 +169,10 @@ string Label_getInfo(string label, int mode)
                SCO_LABEL(_("SCO^rounds won"),    "rounds", "             ", _("Number of rounds won"));
                SCO_LABEL(_("SCO^rounds played"), "rounds_pl", "          ", _("Number of rounds played"));
                SCO_LABEL(_("SCO^score"),         "score", "              ", _("Total score"));
+               SCO_LABEL(_("SCO^average speed"), "avgspeed", "           ", _("Average speed (CTS)"));
+               SCO_LABEL(_("SCO^top speed"),     "topspeed", "           ", _("Top speed (CTS)"));
+               SCO_LABEL(_("SCO^start speed"),   "startspeed", "         ", _("Start speed (CTS)"));
+               SCO_LABEL(_("SCO^strafe"),        "strafe", "             ", _("Strafe efficiency (CTS)"));
                SCO_LABEL(_("SCO^suicides"),      "suicides", "           ", _("Number of suicides"));
                SCO_LABEL(_("SCO^sum"),           "sum", "                ", _("Number of kills minus deaths"));
                SCO_LABEL(_("SCO^survivals"),     "survivals", "          ", _("Number of survivals"));
@@ -752,7 +756,7 @@ void Cmd_Scoreboard_Help()
 " +ctf/pickups +ctf/fckills +ctf/returns +ctf/caps +ons/takes +ons/caps" \
 " +lms/lives +lms/rank" \
 " +kh/kckills +kh/losses +kh/caps" \
-" ?+rc/laps ?+rc/time +rc,cts/fastest" \
+" ?+rc/laps ?+rc/time ?+cts/strafe ?+cts/startspeed ?+cts/avgspeed ?+cts/topspeed +rc,cts/fastest" \
 " +as/objectives +nb/faults +nb/goals" \
 " +ka,tka/pickups +ka,tka/bckills +ka,tka/bctime +ft/revivals" \
 " +dom/ticks +dom/takes" \
@@ -1147,6 +1151,23 @@ string Scoreboard_GetField(entity pl, PlayerScoreField field, bool per_round)
                                return sprintf("%.2f k", pl.(scores(field)) / (1000 * rounds_played));
                        return sprintf("%.1f k", pl.(scores(field)) / 1000);
 
+               case SP_CTS_STRAFE:
+               {
+                       float strafe_efficiency = pl.(scores(field)) / 1000;
+                       if(strafe_efficiency < -1) return "";
+                       sbt_field_rgb = '1 1 1' - (strafe_efficiency > 0 ? '1 0 1' : '0 1 1') * fabs(strafe_efficiency);
+                       return sprintf("%.1f%%", strafe_efficiency * 100);
+               }
+
+               case SP_CTS_STARTSPEED:
+               case SP_CTS_AVGSPEED:
+               case SP_CTS_TOPSPEED:
+               {
+                       float speed = pl.(scores(field)) * GetSpeedUnitFactor(autocvar_hud_speed_unit);
+                       if(speed < 0) return "";
+                       return sprintf("%d%s", speed, GetSpeedUnit(autocvar_hud_speed_unit));
+               }
+
                default: case SP_SCORE:
                        tmp = pl.(scores(field));
                        f = scores_flags(field);
index 075dfb3711bed22b957f3723c44e65ae06865f56..f6c23b682a94b8e48b29d89e04d3d533f51d7b5d 100644 (file)
@@ -191,6 +191,7 @@ void HUD_StrafeHUD()
                float  range_minangle;
                float  text_offset_top               = 0;
                float  text_offset_bottom            = 0;
+               float  hfov                          = getproperty(VF_FOVX);
 
                // real_* variables which are always positive with no wishangle offset
                float real_bestangle;
@@ -358,11 +359,26 @@ void HUD_StrafeHUD()
                        else
                                hudangle = range_minangle; // use minimum angle required if dynamically setting hud angle
                }
+               else if(autocvar_hud_panel_strafehud_range < 0)
+               {
+                       hudangle = hfov;
+               }
                else
                {
                        hudangle = bound(0, fabs(autocvar_hud_panel_strafehud_range), 360); // limit HUD range to 360 degrees, higher values don't make sense
                }
 
+               // limit strafe-meter angle to values suitable for the current projection mode
+               switch(autocvar_hud_panel_strafehud_projection)
+               {
+                       case STRAFEHUD_PROJECTION_PERSPECTIVE:
+                               hudangle = min(hudangle, 170);
+                               break;
+                       case STRAFEHUD_PROJECTION_PANORAMIC:
+                               hudangle = min(hudangle, 350);
+                               break;
+               }
+
                // detect air strafe turning
                if((!strafekeys && vlen(vec2(movement)) > 0) || onground || autocvar__hud_configure)
                {
@@ -692,28 +708,32 @@ void HUD_StrafeHUD()
                                accelzone_left_offset, accelzone_width, hidden_width,
                                autocvar_hud_panel_strafehud_bar_accel_color,
                                autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha,
-                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_LEFT);
+                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_LEFT,
+                               true, hudangle);
 
                        if(autocvar_hud_panel_strafehud_bar_preaccel)
                                HUD_Panel_DrawStrafeHUD(
                                        preaccelzone_left_offset, preaccelzone_width, hidden_width,
                                        autocvar_hud_panel_strafehud_bar_accel_color,
                                        autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha,
-                                       autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_RIGHT);
+                                       autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_RIGHT,
+                                       true, hudangle);
 
                        // draw right acceleration zone
                        HUD_Panel_DrawStrafeHUD(
                                accelzone_right_offset, accelzone_width, hidden_width,
                                autocvar_hud_panel_strafehud_bar_accel_color,
                                autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha,
-                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_RIGHT);
+                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_RIGHT,
+                               true, hudangle);
 
                        if(autocvar_hud_panel_strafehud_bar_preaccel)
                                HUD_Panel_DrawStrafeHUD(
                                        preaccelzone_right_offset, preaccelzone_width, hidden_width,
                                        autocvar_hud_panel_strafehud_bar_accel_color,
                                        autocvar_hud_panel_strafehud_bar_accel_alpha * panel_fg_alpha,
-                                       autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_LEFT);
+                                       autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_LEFT,
+                                       true, hudangle);
 
                        // draw overturn zone
                        //   this is technically incorrect
@@ -723,14 +743,16 @@ void HUD_StrafeHUD()
                                overturn_offset, overturn_width, hidden_width,
                                autocvar_hud_panel_strafehud_bar_overturn_color,
                                autocvar_hud_panel_strafehud_bar_overturn_alpha * panel_fg_alpha,
-                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_BOTH);
+                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_BOTH,
+                               true, hudangle);
 
                        // draw neutral zone
                        HUD_Panel_DrawStrafeHUD(
                                neutral_offset, neutral_width, hidden_width,
                                autocvar_hud_panel_strafehud_bar_neutral_color,
                                autocvar_hud_panel_strafehud_bar_neutral_alpha * panel_fg_alpha,
-                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_NONE);
+                               autocvar_hud_panel_strafehud_style, STRAFEHUD_GRADIENT_NONE,
+                               true, hudangle);
 
                        // only draw indicators if minspeed is reached
                        if(autocvar_hud_panel_strafehud_switch && speed >= minspeed && bestangle_width > 0 && autocvar_hud_panel_strafehud_switch_alpha > 0)
@@ -739,6 +761,9 @@ void HUD_StrafeHUD()
                                float offset = !odd_angles ? bestangle_offset : odd_bestangle_offset;
                                float switch_offset = !odd_angles ? switch_bestangle_offset : switch_odd_bestangle_offset;
 
+                               offset = StrafeHUD_projectOffset(offset, hudangle);
+                               switch_offset = StrafeHUD_projectOffset(switch_offset, hudangle);
+
                                // remove switch indicator width from offset
                                if(direction == STRAFEHUD_DIRECTION_LEFT)
                                {
@@ -759,14 +784,16 @@ void HUD_StrafeHUD()
                                        switch_offset, bestangle_width, hidden_width,
                                        autocvar_hud_panel_strafehud_switch_color,
                                        autocvar_hud_panel_strafehud_switch_alpha * panel_fg_alpha,
-                                       STRAFEHUD_STYLE_DRAWFILL, STRAFEHUD_GRADIENT_NONE);
+                                       STRAFEHUD_STYLE_DRAWFILL, STRAFEHUD_GRADIENT_NONE,
+                                       false, hudangle);
 
                                if(direction == STRAFEHUD_DIRECTION_NONE)
                                        HUD_Panel_DrawStrafeHUD(
                                                offset, bestangle_width, hidden_width,
                                                autocvar_hud_panel_strafehud_switch_color,
                                                autocvar_hud_panel_strafehud_switch_alpha * panel_fg_alpha,
-                                               STRAFEHUD_STYLE_DRAWFILL, STRAFEHUD_GRADIENT_NONE);
+                                               STRAFEHUD_STYLE_DRAWFILL, STRAFEHUD_GRADIENT_NONE,
+                                               false, hudangle);
                        }
                }
 
@@ -872,6 +899,27 @@ void HUD_StrafeHUD()
                                DRAWFLAG_NORMAL);
                }
 
+               string newsound = autocvar_hud_panel_strafehud_sonar_audio;
+               static string cursound = string_null;
+               static string sonarsound = string_null;
+               if(newsound == "")
+               {
+                       cursound = sonarsound = string_null;
+               }
+               else if(newsound != cursound)
+               {
+                       strfree(cursound);
+                       cursound = strzone(newsound);
+
+                       strfree(sonarsound);
+                       sonarsound = _Sound_fixpath(newsound);
+                       if(sonarsound)
+                       {
+                               sonarsound = strzone(sonarsound);
+                               precache_sound(sonarsound);
+                       }
+               }
+
                // draw the actual strafe angle
                if(!immobile)
                {
@@ -901,6 +949,33 @@ void HUD_StrafeHUD()
 
                        if(autocvar_hud_panel_strafehud_style == STRAFEHUD_STYLE_GRADIENT)
                                currentangle_color = StrafeHUD_mixColors(autocvar_hud_panel_strafehud_angle_neutral_color, currentangle_color, fabs(strafe_ratio));
+
+                       // reuse strafe ratio for strafe sonar
+                       static float sonar_time = 0;
+
+                       float sonar_start = bound(0, autocvar_hud_panel_strafehud_sonar_start, 1);
+                       float sonar_ratio = strafe_ratio - sonar_start;
+                       if(sonar_start != 1)
+                               sonar_ratio /= 1 - sonar_start;
+                       else
+                               sonar_ratio = 1;
+
+                       float sonar_interval = max(0, autocvar_hud_panel_strafehud_sonar_interval_start);
+                       sonar_interval += autocvar_hud_panel_strafehud_sonar_interval_range * sonar_ratio ** max(1, autocvar_hud_panel_strafehud_sonar_interval_exponent);
+                       bool sonar_ready = (sonar_time == 0) || ((time - sonar_time) >= sonar_interval);
+                       if(autocvar_hud_panel_strafehud_sonar && sonar_ready && (strafe_ratio >= sonar_start))
+                       {
+                               sonar_time = time;
+
+                               float sonar_volume = bound(0, autocvar_hud_panel_strafehud_sonar_volume_start, 1);
+                               sonar_volume += autocvar_hud_panel_strafehud_sonar_volume_range * sonar_ratio ** max(1, autocvar_hud_panel_strafehud_sonar_volume_exponent);
+
+                               float sonar_pitch = max(0, autocvar_hud_panel_strafehud_sonar_pitch_start);
+                               sonar_pitch += autocvar_hud_panel_strafehud_sonar_pitch_range * sonar_ratio ** max(1, autocvar_hud_panel_strafehud_sonar_pitch_exponent);
+
+                               if(sonarsound && (sonar_volume > 0))
+                                       sound7(csqcplayer, CH_INFO, sonarsound, bound(0, sonar_volume, 1) * VOL_BASE, ATTN_NONE, max(0.000001, sonar_pitch * 100), 0);
+                       }
                }
 
                if(mode == STRAFEHUD_MODE_VIEW_CENTERED || straight_overturn)
@@ -911,6 +986,9 @@ void HUD_StrafeHUD()
                if(autocvar_hud_panel_strafehud_bestangle && direction != STRAFEHUD_DIRECTION_NONE)
                        ghost_offset = bound(0, (odd_angles ? odd_bestangle_offset : bestangle_offset), panel_size.x);
 
+               currentangle_offset = StrafeHUD_projectOffset(currentangle_offset, hudangle);
+               ghost_offset = StrafeHUD_projectOffset(ghost_offset, hudangle);
+
                switch(autocvar_hud_panel_strafehud_angle_style)
                {
                        case STRAFEHUD_INDICATOR_SOLID:
@@ -1006,6 +1084,24 @@ void HUD_StrafeHUD()
                text_offset_top = max(angle_offset_top, text_offset_top);
                text_offset_bottom = max(angle_offset_bottom, text_offset_bottom);
 
+               // vertical angle for weapon jumps
+               {
+                       if(autocvar_hud_panel_strafehud_vangle)
+                       {
+                               float vangle = -PHYS_INPUT_ANGLES(strafeplayer).x;
+                               float vangle_height = autocvar_hud_panel_strafehud_vangle_size * panel_size.y;
+                               string vangle_text = strcat(ftos_decimals(vangle, 2), "°");
+
+                               bool was_drawn = StrafeHUD_drawTextIndicator(
+                                       vangle_text, vangle_height,
+                                       autocvar_hud_panel_strafehud_vangle_color, 1,
+                                       time, text_offset_bottom, STRAFEHUD_TEXT_BOTTOM);
+
+                               if(was_drawn)
+                                       text_offset_bottom += vangle_height;
+                       }
+               }
+
                draw_beginBoldFont();
 
                // show speed when crossing the start trigger
@@ -1087,8 +1183,33 @@ void HUD_StrafeHUD()
        hud_lasttime = time;
 }
 
+float StrafeHUD_projectOffset(float offset, float range)
+{
+       range *= DEG2RAD / 2;
+       float angle = (offset - (panel_size.x / 2)) / (panel_size.x / 2) * range;
+       switch(autocvar_hud_panel_strafehud_projection)
+       {
+               default:
+               case STRAFEHUD_PROJECTION_LINEAR:
+                       return offset;
+               case STRAFEHUD_PROJECTION_PERSPECTIVE:
+                       offset = tan(angle) / tan(range);
+                       break;
+               case STRAFEHUD_PROJECTION_PANORAMIC:
+                       offset = tan(angle / 2) / tan(range / 2);
+                       break;
+       }
+       offset = offset * (panel_size.x / 2) + (panel_size.x / 2);
+       return offset;
+}
+
+float StrafeHUD_projectWidth(float offset, float width, float range)
+{
+       return StrafeHUD_projectOffset(offset + width, range) - StrafeHUD_projectOffset(offset, range);
+}
+
 // functions to make hud elements align perfectly in the hud area
-void HUD_Panel_DrawStrafeHUD(float offset, float width, float hidden_width, vector color, float alpha, int type, int gradientType)
+void HUD_Panel_DrawStrafeHUD(float offset, float width, float hidden_width, vector color, float alpha, int type, int gradientType, bool doProject, float range)
 {
        float mirror_offset, mirror_width;
        vector size = panel_size;
@@ -1123,6 +1244,14 @@ void HUD_Panel_DrawStrafeHUD(float offset, float width, float hidden_width, vect
        }
        size.x = width;
 
+       vector original_size = size;
+       float original_offset = offset;
+       if(doProject)
+       {
+               if(size.x > 0) size.x = StrafeHUD_projectWidth(offset, size.x, range);
+               offset = StrafeHUD_projectOffset(offset, range);
+       }
+
        if(mirror_offset < 0)
        {
                mirror_width += mirror_offset;
@@ -1137,6 +1266,14 @@ void HUD_Panel_DrawStrafeHUD(float offset, float width, float hidden_width, vect
        }
        mirror_size.x = mirror_width;
 
+       vector original_mirror_size = mirror_size;
+       float original_mirror_offset = mirror_offset;
+       if(doProject)
+       {
+               if(mirror_size.x > 0) mirror_size.x = StrafeHUD_projectWidth(mirror_offset, mirror_size.x, range);
+               mirror_offset = StrafeHUD_projectOffset(mirror_offset, range);
+       }
+
        switch(type)
        {
                default:
@@ -1191,13 +1328,13 @@ void HUD_Panel_DrawStrafeHUD(float offset, float width, float hidden_width, vect
 
                        StrafeHUD_drawGradient(
                                color, autocvar_hud_panel_strafehud_bar_neutral_color,
-                               mirror_size, original_width, mirror_offset,
-                               alpha, gradient_mirror_offset, gradientType);
+                               original_mirror_size, original_width, original_mirror_offset,
+                               alpha, gradient_mirror_offset, gradientType, doProject, range);
 
                        StrafeHUD_drawGradient(
                                color, autocvar_hud_panel_strafehud_bar_neutral_color,
-                               size, original_width, offset,
-                               alpha, gradient_offset, gradientType);
+                               original_size, original_width, original_offset,
+                               alpha, gradient_offset, gradientType, doProject, range);
        }
 }
 
@@ -1212,7 +1349,7 @@ vector StrafeHUD_mixColors(vector color1, vector color2, float ratio)
        return mixedColor;
 }
 
-void StrafeHUD_drawGradient(vector color1, vector color2, vector size, float original_width, float offset, float alpha, float gradientOffset, int gradientType)
+void StrafeHUD_drawGradient(vector color1, vector color2, vector size, float original_width, float offset, float alpha, float gradientOffset, int gradientType, bool doProject, float range)
 {
        float color_ratio, alpha1, alpha2;
        vector segment_size = size;
@@ -1222,8 +1359,14 @@ void StrafeHUD_drawGradient(vector color1, vector color2, vector size, float ori
        color_ratio = alpha1 / (alpha1 + alpha2);
        for(int i = 0; i < size.x; ++i)
        {
-               float ratio, alpha_ratio, combine_ratio1, combine_ratio2;
+               float ratio, alpha_ratio, combine_ratio1, combine_ratio2, segment_offset;
                segment_size.x = min(size.x - i, 1); // each gradient segment is 1 unit wide except if there is less than 1 unit of gradient remaining
+               segment_offset = offset + i;
+               if(doProject)
+               {
+                       segment_size.x = StrafeHUD_projectWidth(segment_offset, segment_size.x, range);
+                       segment_offset = StrafeHUD_projectOffset(segment_offset, range);
+               }
                ratio = (i + segment_size.x / 2 + gradientOffset) / original_width * (gradientType == STRAFEHUD_GRADIENT_BOTH ? 2 : 1);
                if(ratio > 1) ratio = 2 - ratio;
                if(gradientType != STRAFEHUD_GRADIENT_RIGHT) ratio = 1 - ratio;
@@ -1234,7 +1377,7 @@ void StrafeHUD_drawGradient(vector color1, vector color2, vector size, float ori
 
                if(alpha_ratio > 0)
                        drawfill(
-                               panel_pos + eX * (offset + i),
+                               panel_pos + eX * segment_offset,
                                segment_size,
                                StrafeHUD_mixColors(color1, color2, ratio),
                                alpha_ratio,
index 834c5a5e73945a8c623c1b14c3ecd71628df1214..894515fce69f3ed1881358b14dcaf7c537902043 100644 (file)
@@ -1,71 +1,89 @@
 #pragma once
 #include "../panel.qh"
 
-int autocvar_hud_panel_strafehud = 3;
-bool autocvar__hud_panel_strafehud_demo = false;
-bool autocvar_hud_panel_strafehud_dynamichud = true;
-int autocvar_hud_panel_strafehud_mode = 0;
-float autocvar_hud_panel_strafehud_range = 90;
-int autocvar_hud_panel_strafehud_style = 2;
-bool autocvar_hud_panel_strafehud_unit_show = true;
-bool autocvar_hud_panel_strafehud_uncapped = false;
-bool autocvar_hud_panel_strafehud_bar_preaccel = true;
-vector autocvar_hud_panel_strafehud_bar_neutral_color = '1 1 1';
-float autocvar_hud_panel_strafehud_bar_neutral_alpha = 0.1;
-vector autocvar_hud_panel_strafehud_bar_accel_color = '0 1 0';
-float autocvar_hud_panel_strafehud_bar_accel_alpha = 0.5;
-vector autocvar_hud_panel_strafehud_bar_overturn_color = '1 0 1';
-float autocvar_hud_panel_strafehud_bar_overturn_alpha = 0.5;
-int autocvar_hud_panel_strafehud_angle_style = 0;
-int autocvar_hud_panel_strafehud_angle_dashes = 4;
-float autocvar_hud_panel_strafehud_angle_alpha = 0.8;
-float autocvar_hud_panel_strafehud_angle_height = 1;
-float autocvar_hud_panel_strafehud_angle_width = 0.001;
-vector autocvar_hud_panel_strafehud_angle_neutral_color = '1 1 0';
-vector autocvar_hud_panel_strafehud_angle_accel_color = '0 1 1';
-vector autocvar_hud_panel_strafehud_angle_overturn_color = '1 0 1';
-int autocvar_hud_panel_strafehud_angle_arrow = 1;
-float autocvar_hud_panel_strafehud_angle_arrow_size = 0.5;
-bool autocvar_hud_panel_strafehud_bestangle = true;
-vector autocvar_hud_panel_strafehud_bestangle_color = '1 1 1';
-float autocvar_hud_panel_strafehud_bestangle_alpha = 0.5;
-bool autocvar_hud_panel_strafehud_switch = true;
-float autocvar_hud_panel_strafehud_switch_minspeed = -1;
-vector autocvar_hud_panel_strafehud_switch_color = '1 1 0';
-float autocvar_hud_panel_strafehud_switch_alpha = 1;
-float autocvar_hud_panel_strafehud_switch_width = 0.003;
-bool autocvar_hud_panel_strafehud_direction = false;
-vector autocvar_hud_panel_strafehud_direction_color = '0 0.5 1';
-float autocvar_hud_panel_strafehud_direction_alpha = 1;
-float autocvar_hud_panel_strafehud_direction_width = 0.25;
-float autocvar_hud_panel_strafehud_direction_length = 0.02;
-bool autocvar_hud_panel_strafehud_slickdetector = true;
-float autocvar_hud_panel_strafehud_slickdetector_range = 200;
-int autocvar_hud_panel_strafehud_slickdetector_granularity = 1;
-vector autocvar_hud_panel_strafehud_slickdetector_color = '0 1 1';
-float autocvar_hud_panel_strafehud_slickdetector_alpha = 0.5;
-float autocvar_hud_panel_strafehud_slickdetector_height = 0.125;
-bool autocvar_hud_panel_strafehud_startspeed = true;
-float autocvar_hud_panel_strafehud_startspeed_fade = 4;
-vector autocvar_hud_panel_strafehud_startspeed_color = '1 0.75 0';
-float autocvar_hud_panel_strafehud_startspeed_size = 1.5;
-bool autocvar_hud_panel_strafehud_jumpheight = false;
-float autocvar_hud_panel_strafehud_jumpheight_fade = 4;
-float autocvar_hud_panel_strafehud_jumpheight_min = 50;
-vector autocvar_hud_panel_strafehud_jumpheight_color = '0 1 0.75';
-float autocvar_hud_panel_strafehud_jumpheight_size = 1.5;
-float autocvar_hud_panel_strafehud_timeout_ground = 0.1;
-float autocvar_hud_panel_strafehud_timeout_turn = 0.1;
-float autocvar_hud_panel_strafehud_antiflicker_angle = 0.01;
-float autocvar_hud_panel_strafehud_fps_update = 0.5;
+AUTOCVAR_SAVE(hud_panel_strafehud, int, 3, "enable this panel, 1 = show if not observing, 2 = show always, 3 = show only in race/cts if not observing");
+AUTOCVAR_SAVE(_hud_panel_strafehud_demo, bool, false, "strafehud changes angle during configure");
+AUTOCVAR_SAVE(hud_panel_strafehud_dynamichud, bool, true, "apply the dynamic hud effects to this panel");
+AUTOCVAR_SAVE(hud_panel_strafehud_mode, int, 0, "strafehud mode which controls whether the strafehud is centered at \"0\" = view angle, \"1\" = velocity angle");
+AUTOCVAR_SAVE(hud_panel_strafehud_range, float, 90, "the angle range up to 360 degrees displayed on the strafehud, \"-1\" = current fov, \"0\" = dynamic (chooses the minimum range required to still see the whole area needed for accelerating)");
+AUTOCVAR_SAVE(hud_panel_strafehud_style, int, 2, "\"0\" = no styling, \"1\" = progress bar style for the strafe bar, \"2\" = gradient for the strafe bar");
+AUTOCVAR_SAVE(hud_panel_strafehud_unit_show, bool, true, "show units");
+AUTOCVAR_SAVE(hud_panel_strafehud_uncapped, bool, false, "set to \"1\" to remove some safety restrictions, useful to set thinner indicator lines down to 1px or for trying out higher values for some performance degrading operations (warning: elements may turn invisible if too thin, other configurations may crash your game or look horribly ugly)");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_preaccel, bool, true, "set to \"1\" to extend the acceleration zone by the strafe meter zone before full acceleration can be achieved");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_neutral_color, vector, '1 1 1', "color of the strafe meter neutral zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_neutral_alpha, float, 0.1, "opacity of the strafe meter neutral zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_accel_color, vector, '0 1 0', "color of the strafe meter acceleration zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_accel_alpha, float, 0.5, "opacity of the strafe meter acceleration zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_overturn_color, vector, '1 0 1', "color of the strafe meter overturn zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_bar_overturn_alpha, float, 0.5, "opacity of the strafe meter overturn zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_style, int, 0, "set the angle indicator style: 0 = none, 1 = solid line, 2 = dashed line");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_dashes, int, 4, "determines the amount of dashes if the angle indicator uses a dashed line");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_alpha, float, 0.8, "opacity of the indicator showing the player's current angle");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_height, float, 1, "height of the indicator showing the player's current angle (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_width, float, 0.001, "width of the indicator showing the player's current angle (relative to the panel width)");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_neutral_color, vector, '1 1 0', "color of the indicator showing the player's current angle if it is within the neutral zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_accel_color, vector, '0 1 1', "color of the indicator showing the player's current angle if it is within the acceleration zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_overturn_color, vector, '1 0 1', "color of the indicator showing the player's current angle if it is within the overturn zone");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_arrow, int, 1, "set the angle indicator's arrow style: 0 = none, 1 = top, 2 = bottom, 3 = both");
+AUTOCVAR_SAVE(hud_panel_strafehud_angle_arrow_size, float, 0.5, "size of the arrow (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_bestangle, bool, true, "set to \"1\" to enable a ghost angle indicator showing the best angle to gain maximum acceleration");
+AUTOCVAR_SAVE(hud_panel_strafehud_bestangle_color, vector, '1 1 1', "color of the indicator showing the best angle to gain maximum acceleration");
+AUTOCVAR_SAVE(hud_panel_strafehud_bestangle_alpha, float, 0.5, "opacity of the indicator showing the best angle to gain maximum acceleration");
+AUTOCVAR_SAVE(hud_panel_strafehud_switch, bool, true, "set to \"1\" to enable the switch indicator showing the angle to move to when switching sides");
+AUTOCVAR_SAVE(hud_panel_strafehud_switch_minspeed, float, -1, "minimum speed in qu/s at which switch indicator(s) which are used to aid changing strafe direction will be shown (set to -1 for dynamic minspeed)");
+AUTOCVAR_SAVE(hud_panel_strafehud_switch_color, vector, '1 1 0', "color of the switch indicator");
+AUTOCVAR_SAVE(hud_panel_strafehud_switch_alpha, float, 1, "opacity of the switch indicator");
+AUTOCVAR_SAVE(hud_panel_strafehud_switch_width, float, 0.003, "width of the strafe angle indicator(s) (relative to the strafe bar width)");
+AUTOCVAR_SAVE(hud_panel_strafehud_direction, bool, false, "set to \"1\" to enable the direction caps to see in which direction you are currently strafing");
+AUTOCVAR_SAVE(hud_panel_strafehud_direction_color, vector, '0 0.5 1', "color of the direction caps which indicate the direction the player is currently strafing towards");
+AUTOCVAR_SAVE(hud_panel_strafehud_direction_alpha, float, 1, "opacity of the direction caps which indicate the direction the player is currently strafing towards");
+AUTOCVAR_SAVE(hud_panel_strafehud_direction_width, float, 0.25, "stroke width of the direction caps which indicate the direction the player is currently strafing towards (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_direction_length, float, 0.02, "length of the horizontal component of the direction caps which indicate the direction the player is currently strafing towards (relative to the panel width)");
+AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector, bool, true, "set to \"1\" to enable the slick detector which notifies you if there is slick near you");
+AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_range, float, 200, "range of the slick detector in qu");
+AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_granularity, int, 1, "value from 0 to 4 which defines how exact the search for slick should be, higher values may yield better results but require more computation");
+AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_color, vector, '0 1 1', "color of the slick detector indicator");
+AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_alpha, float, 0.5, "opacity of the slick detector indicator");
+AUTOCVAR_SAVE(hud_panel_strafehud_slickdetector_height, float, 0.125, "height of the slick detector indicator (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_startspeed, bool, true, "set to \"1\" to enable the start speed indicator which shows you the speed you had while passing the start trigger of a race map");
+AUTOCVAR_SAVE(hud_panel_strafehud_startspeed_fade, float, 4, "fade time (in seconds) of the start speed text");
+AUTOCVAR_SAVE(hud_panel_strafehud_startspeed_color, vector, '1 0.75 0', "color of the start speed text");
+AUTOCVAR_SAVE(hud_panel_strafehud_startspeed_size, float, 1.5, "size of the start speed text (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight, bool, false, "set to \"1\" to enable the jump height indicator which tells you how high you jumped");
+AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight_fade, float, 4, "fade time (in seconds) of the jump height text");
+AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight_min, float, 50, "minimum jump height to display in the selected unit");
+AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight_color, vector, '0 1 0.75', "color of the jump height text");
+AUTOCVAR_SAVE(hud_panel_strafehud_jumpheight_size, float, 1.5, "size of the jump height text (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_timeout_ground, float, 0.1, "time (in seconds) after take off before changing to air strafe physics when not jumping (visually more consistent hud while on slick downwards ramps)");
+AUTOCVAR_SAVE(hud_panel_strafehud_timeout_turn, float, 0.1, "time (in seconds) after releasing the strafe keys before changing mode (visually more consistent hud while switching between left/right strafe turning)");
+AUTOCVAR_SAVE(hud_panel_strafehud_antiflicker_angle, float, 0.01, "how many degrees from 0° to 180° the hud ignores if it could cause visual disturbances otherwise (and to counter rounding errors)");
+AUTOCVAR_SAVE(hud_panel_strafehud_fps_update, float, 0.5, "update interval (in seconds) of the frametime to calculate the optimal angle, smaller values may cause flickering");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar, bool, false, "set to \"1\" to enable the strafe sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_audio, string, "misc/talk", "audio to play for sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_start, float, 0.5, "how optimal from 0 to 1 your strafing angle has to be for the strafe sonar to activate");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_interval_start, float, 0.333333, "strafe sonar sound interval in seconds");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_interval_range, float, -0.222222, "dynamic sound interval range in seconds of the strafe sonar as you approach the optimal angle");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_interval_exponent, float, 1, "exponent of the dynamic sound interval range of the strafe sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_volume_start, float, 0.333333, "sound volume of the strafe sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_volume_range, float, 0.666666, "dynamic volume range of the strafe sonar as you approach the optimal angle");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_volume_exponent, float, 1, "exponent of the dynamic volume range of the strafe sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_pitch_start, float, 0.9, "playback speed of the strafe sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_pitch_range, float, 0.1, "dynamic playback speed range of the strafe sonar as you approach the optimal angle");
+AUTOCVAR_SAVE(hud_panel_strafehud_sonar_pitch_exponent, float, 1, "exponent of the dynamic playback speed range of the strafe sonar");
+AUTOCVAR_SAVE(hud_panel_strafehud_vangle, bool, false, "set to \"1\" to enable the vertical angle indicator");
+AUTOCVAR_SAVE(hud_panel_strafehud_vangle_color, vector, '0.75 0.75 0.75', "color of the vertical angle text");
+AUTOCVAR_SAVE(hud_panel_strafehud_vangle_size, float, 1, "size of the vertical angle text (relative to the panel height)");
+AUTOCVAR_SAVE(hud_panel_strafehud_projection, int, 0, "strafehud projection mode, \"0\" = linear, \"1\" = perspective, \"2\" = panoramic");
 
-void HUD_Panel_DrawStrafeHUD(float, float, float, vector, float, int, int);
+void HUD_Panel_DrawStrafeHUD(float, float, float, vector, float, int, int, bool, float);
 vector StrafeHUD_mixColors(vector, vector, float);
-void StrafeHUD_drawGradient(vector, vector, vector, float, float, float, float, int);
+void StrafeHUD_drawGradient(vector, vector, vector, float, float, float, float, int, bool, float);
 float GetLengthUnitFactor(int);
 string GetLengthUnit(int);
 void StrafeHUD_drawStrafeArrow(vector, float, vector, float, bool, float);
 bool StrafeHUD_drawTextIndicator(string, float, vector, float, float, float, int);
+float StrafeHUD_projectOffset(float, float);
+float StrafeHUD_projectWidth(float, float, float);
 
 const int STRAFEHUD_MODE_VIEW_CENTERED = 0;
 const int STRAFEHUD_MODE_VELOCITY_CENTERED = 1;
@@ -93,3 +111,7 @@ const int STRAFEHUD_INDICATOR_DASHED = 2;
 
 const int STRAFEHUD_TEXT_TOP = 0;
 const int STRAFEHUD_TEXT_BOTTOM = 1;
+
+const int STRAFEHUD_PROJECTION_LINEAR = 0;
+const int STRAFEHUD_PROJECTION_PERSPECTIVE = 1;
+const int STRAFEHUD_PROJECTION_PANORAMIC = 2;
index 3ce25e26d71272da71edde5d46f32ae482c7d879..5a960bdb6fe2aff6995127630365c6b21d20759a 100644 (file)
@@ -270,6 +270,12 @@ void GenericCommand_maplist(int request, int argc)
                                        return;
                                }
 
+                        case "rebuild": // rebuilds maplist to include available maps, useful after doing fs_rescan
+                          {
+                            cvar_set("g_maplist", MapInfo_ListAllowedMaps(MapInfo_CurrentGametype(), MapInfo_RequiredFlags(), MapInfo_ForbiddenFlags()));
+                            return;
+                          }
+
                                case "remove": // scans maplist and only adds back whatever maps were not provided in argv(2)
                                {
                                        if(argc == 3)
index 18e4e6100142b9c4ee73c3fb27ab94ec61481db3..03488da3a6bbfee36472c583c8c65b7027928a33 100644 (file)
@@ -28,7 +28,7 @@ const int SPECIES_ROBOT_SHINY = 5;
 const int SPECIES_RESERVED = 15;
 
 #ifdef GAMEQC
-const int RANKINGS_CNT = 99;
+const int RANKINGS_CNT = 256;
 
 ///////////////////////////
 // keys pressed
index e16ed913829338802823dddce026e7be7f42d989..2b55f283784d869726a3c43ea344e5a326f152fe 100644 (file)
@@ -131,7 +131,7 @@ void ctf_CaptureRecord(entity flag, entity player)
                ctf_captimerecord = cap_time;
                db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/time"), ftos(cap_time));
                db_put(ServerProgsDB, strcat(GetMapname(), "/captimerecord/netname"), player.netname);
-               write_recordmarker(player, flag.ctf_pickuptime, cap_time);
+               write_recordmarker(player, 1, flag.ctf_pickuptime, cap_time);
        }
 
        if(autocvar_g_ctf_leaderboard && !ctf_oneflag)
index 0fbab7ae51f54159e1913b7fe723bd4795b77ed4..defeb686345f41da4ff1825d169d64beb61a0f1d 100644 (file)
@@ -57,6 +57,10 @@ void cts_ScoreRules()
     GameRules_score_enabled(false);
     GameRules_scoring(0, 0, 0, {
         if (g_race_qualifying) {
+            field(SP_CTS_STRAFE, "strafe", 0);
+            field(SP_CTS_STARTSPEED, "startspeed", 0);
+            field(SP_CTS_AVGSPEED, "avgspeed", 0);
+            field(SP_CTS_TOPSPEED, "topspeed", 0);
             field(SP_RACE_FASTEST, "fastest", SFL_SORT_PRIO_PRIMARY | SFL_LOWER_IS_BETTER | SFL_TIME);
         } else {
             field(SP_RACE_LAPS, "laps", SFL_SORT_PRIO_PRIMARY);
@@ -141,6 +145,21 @@ MUTATOR_HOOKFUNCTION(cts, PlayerPhysics)
                                CS(player).movement_y = -M_SQRT1_2 * wishspeed;
                }
        }
+       player.strafe_efficiency_sum += calculate_strafe_efficiency(player, CS(player).movement, dt) * dt;
+       if(player.race_started)
+       {
+               float current_speed = vlen(vec2(player.velocity));
+               if(player.race_topspeed < current_speed)
+               {
+                       player.race_topspeed = current_speed;
+               }
+               player.race_avgspeed_sum += current_speed * dt;
+               player.race_avgspeed_time += dt;
+       }
+       else
+       {
+               player.race_startspeed = player.race_avgspeed_sum = player.race_avgspeed_time = player.race_topspeed = 0;
+       }
 }
 
 MUTATOR_HOOKFUNCTION(cts, reset_map_global)
@@ -153,6 +172,15 @@ MUTATOR_HOOKFUNCTION(cts, reset_map_global)
        PlayerScore_Sort(race_place, 0, true, false);
 
        FOREACH_CLIENT(true, {
+               it.strafe_efficiency_best = -2;
+               it.strafe_efficiency_sum = it.strafe_efficiency_time = 0;
+               PlayerScore_Set(it, SP_CTS_STRAFE, -20000);
+               it.race_startspeed_best = it.race_avgspeed_best = it.race_topspeed_best = -1;
+               it.race_startspeed = it.race_avgspeed_sum = it.race_avgspeed_time = it.race_topspeed = 0;
+               PlayerScore_Set(it, SP_CTS_STARTSPEED, -1);
+               PlayerScore_Set(it, SP_CTS_AVGSPEED, -1);
+               PlayerScore_Set(it, SP_CTS_TOPSPEED, -1);
+
                if(it.race_place)
                {
                        s = GameRules_scoring_add(it, RACE_FASTEST, 0);
@@ -179,6 +207,14 @@ MUTATOR_HOOKFUNCTION(cts, ClientConnect)
 
        race_PreparePlayer(player);
        player.race_checkpoint = -1;
+       player.strafe_efficiency_best = -2;
+       player.strafe_efficiency_sum = player.strafe_efficiency_time = 0;
+       PlayerScore_Set(player, SP_CTS_STRAFE, -20000);
+       player.race_startspeed_best = player.race_avgspeed_best = player.race_topspeed_best = -1;
+       player.race_startspeed = player.race_avgspeed_sum = player.race_avgspeed_time = player.race_topspeed = 0;
+       PlayerScore_Set(player, SP_CTS_STARTSPEED, -1);
+       PlayerScore_Set(player, SP_CTS_AVGSPEED, -1);
+       PlayerScore_Set(player, SP_CTS_TOPSPEED, -1);
 
        race_SendAll(player, false);
 }
@@ -248,6 +284,9 @@ MUTATOR_HOOKFUNCTION(cts, PlayerDies)
        frag_target.respawn_flags |= RESPAWN_FORCE;
        race_AbandonRaceCheck(frag_target);
 
+       frag_target.strafe_efficiency_sum = frag_target.strafe_efficiency_time = 0;
+       frag_target.race_startspeed = frag_target.race_avgspeed_sum = frag_target.race_avgspeed_time = frag_target.race_topspeed = 0;
+
        if(autocvar_g_cts_removeprojectiles)
        {
                IL_EACH(g_projectiles, it.owner == frag_target && (it.flags & FL_PROJECTILE),
index 1316c3283003e6ce5b5a573e47960eebdb9c2020..ac1e0bee49bb6aaf07099ea512c5aadf487aae2e 100644 (file)
@@ -29,6 +29,7 @@ void door_blocked(entity this, entity blocker)
 {
        bool reverse = false;
        if((this.spawnflags & DOOR_CRUSH)
+               && !Q3COMPAT_COMMON
 #ifdef SVQC
                && (blocker.takedamage != DAMAGE_NO)
 #elif defined(CSQC)
@@ -43,46 +44,46 @@ void door_blocked(entity this, entity blocker)
        else
        {
 #ifdef SVQC
-               if((this.dmg) && (blocker.takedamage == DAMAGE_YES))    // Shall we bite?
+               if(this.dmg && blocker.takedamage != DAMAGE_NO)    // Shall we bite?
                        Damage (blocker, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
 #endif
 
-                // don't change direction for dead or dying stuff
-               if(IS_DEAD(blocker)
+               // don't change direction for dead or dying stuff
+               if(!IS_DEAD(blocker)
 #ifdef SVQC
-                       && (blocker.takedamage == DAMAGE_NO)
+                       && blocker.takedamage != DAMAGE_NO
 #endif
+                       && this.wait >= 0
+                       && !(Q3COMPAT_COMMON && this.spawnflags & Q3_DOOR_CRUSHER)
                )
                {
-                       if (this.wait >= 0)
+                       if (this.state == STATE_DOWN)
                        {
-                               if (this.state == STATE_DOWN)
-                               {
-                                       if (this.classname == "door")
-                                               door_go_up(this, NULL, NULL);
-                                       else
-                                               door_rotating_go_up(this, blocker);
-                               }
+                               if (this.classname == "door")
+                                       door_go_up(this, NULL, NULL);
                                else
-                               {
-                                       if (this.classname == "door")
-                                               door_go_down(this);
-                                       else
-                                               door_rotating_go_down(this);
-                               }
-                               reverse = true;
+                                       door_rotating_go_up(this, blocker);
                        }
+                       else
+                       {
+                               if (this.classname == "door")
+                                       door_go_down(this);
+                               else
+                                       door_rotating_go_down(this);
+                       }
+                       reverse = true;
                }
 #ifdef SVQC
                else
                {
                        //gib dying stuff just to make sure
-                       if((this.dmg) && (blocker.takedamage != DAMAGE_NO))    // Shall we bite?
+                       if(this.dmg && blocker.takedamage != DAMAGE_NO && IS_DEAD(blocker))    // Shall we bite?
                                Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
                }
 #endif
        }
-       if (!reverse && this.classname == "door")
+       // if we didn't change direction and are using a non-linear movement controller, we must pause it
+       if (!reverse && this.classname == "door" && this.move_controller)
                SUB_CalcMovePause(this);
 }
 
@@ -212,10 +213,13 @@ bool door_check_keys(entity door, entity player)
        return false;
 }
 
-void door_fire(entity this, entity actor, entity trigger)
+void door_use(entity this, entity actor, entity trigger)
 {
-       if (this.owner != this)
-               objerror (this, "door_fire: this.owner != this");
+       //dprint("door_use (model: ");dprint(this.model);dprint(")\n");
+
+       if (!this.owner)
+               return;
+       this = this.owner;
 
        if (this.spawnflags & DOOR_TOGGLE)
        {
@@ -256,14 +260,6 @@ void door_fire(entity this, entity actor, entity trigger)
        } while ((e != this) && (e != NULL));
 }
 
-void door_use(entity this, entity actor, entity trigger)
-{
-       //dprint("door_use (model: ");dprint(this.model);dprint(")\n");
-
-       if (this.owner)
-               door_fire(this.owner, actor, trigger);
-}
-
 void door_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
 {
        if(this.spawnflags & NOSPLASH)
@@ -369,14 +365,25 @@ void door_trigger_touch(entity this, entity toucher)
 #endif
                        return;
 
-       if (time < this.door_finished)
+       if (this.owner.state == STATE_UP)
                return;
 
        // check if door is locked
        if (!door_check_keys(this, toucher))
                return;
 
-       this.door_finished = time + 1;
+       if (this.owner.state == STATE_TOP)
+       {
+               if (this.owner.nextthink < this.owner.ltime + this.owner.wait)
+               {
+                       entity e = this.owner;
+                       do {
+                               e.nextthink = e.ltime + e.wait;
+                               e = e.enemy;
+                       } while (e != this.owner);
+               }
+               return;
+       }
 
        door_use(this.owner, toucher, NULL);
 }
@@ -408,7 +415,7 @@ LinkDoors
 
 entity LinkDoors_nextent(entity cur, entity near, entity pass)
 {
-       while((cur = find(cur, classname, pass.classname)) && ((cur.spawnflags & DOOR_DONT_LINK) || cur.enemy))
+       while((cur = find(cur, classname, pass.classname)) && ((cur.spawnflags & DOOR_DONT_LINK && !Q3COMPAT_COMMON) || cur.enemy))
        {
        }
        return cur;
@@ -416,6 +423,9 @@ entity LinkDoors_nextent(entity cur, entity near, entity pass)
 
 bool LinkDoors_isconnected(entity e1, entity e2, entity pass)
 {
+       if(Q3COMPAT_COMMON)
+               return e1.team == e2.team;
+
        float DELTA = 4;
        if((e1.absmin_x > e2.absmax_x + DELTA)
        || (e1.absmin_y > e2.absmax_y + DELTA)
@@ -441,7 +451,9 @@ void LinkDoors(entity this)
 
        if (this.enemy)
                return;         // already linked by another door
-       if (this.spawnflags & DOOR_DONT_LINK)
+
+       // Q3 door linking is done for teamed doors only and is not affected by spawnflags or bmodel proximity
+       if ((this.spawnflags & DOOR_DONT_LINK && !Q3COMPAT_COMMON) || (Q3COMPAT_COMMON && !this.team))
        {
                this.owner = this.enemy = this;
 
@@ -544,7 +556,7 @@ SILVER_KEY causes the door to open only if the activator holds a silver key.
 "speed"                movement speed (100 default)
 "wait"         wait before returning (3 default, -1 = never return)
 "lip"          lip remaining at end of move (8 default)
-"dmg"          damage to inflict when blocked (2 default)
+"dmg"          damage to inflict when blocked (0 default)
 "sounds"
 0)     no sound
 1)     stone
@@ -609,9 +621,10 @@ void door_init_startopen(entity this)
        this.pos2 = this.pos1;
        this.pos1 = this.origin;
 
-#ifdef SVQC
-       this.SendFlags |= SF_TRIGGER_UPDATE;
-#endif
+// no longer needed: not using delayed initialisation for door_init_startopen()
+//#ifdef SVQC
+//     this.SendFlags |= SF_TRIGGER_UPDATE;
+//#endif
 }
 
 void door_reset(entity this)
@@ -666,14 +679,28 @@ void door_init_shared(entity this)
 
        if (q3compat)
        {
-               // CPMA adds these fields for overriding the engine sounds
+               // CPMA adds these fields for overriding the Q3 default sounds
                string s = GetField_fullspawndata(this, "sound_start", true);
                string e = GetField_fullspawndata(this, "sound_end", true);
 
                if (s)
                        this.noise2 = strzone(s);
+               else
+               {
+                       // PK3s supporting Q3A sometimes include custom sounds at Q3 default paths
+                       s = "sound/movers/doors/dr1_strt.wav";
+                       if (FindFileInMapPack(s))
+                               this.noise2 = s;
+               }
+
                if (e)
                        this.noise1 = strzone(e);
+               else
+               {
+                       e = "sound/movers/doors/dr1_end.wav";
+                       if (FindFileInMapPack(e))
+                               this.noise1 = e;
+               }
        }
 
        // sound when door stops moving
@@ -693,7 +720,7 @@ void door_init_shared(entity this)
        }
        else if (!this.wait)
        {
-               this.wait = 3;
+               this.wait = q3compat ? 2 : 3;
        }
 
        if (!this.lip)
@@ -738,15 +765,19 @@ spawnfunc(func_door)
        if(this.spawnflags & DOOR_NONSOLID)
                this.solid = SOLID_NOT;
 
-// DOOR_START_OPEN is to allow an entity to be lighted in the closed position
-// but spawn in the open position
-       if (this.spawnflags & DOOR_START_OPEN)
-               InitializeEntity(this, door_init_startopen, INITPRIO_SETLOCATION);
-
        door_init_shared(this);
 
        this.pos1 = this.origin;
-       this.pos2 = this.pos1 + this.movedir*(fabs(this.movedir*this.size) - this.lip);
+       vector absmovedir;
+       absmovedir.x = fabs(this.movedir.x);
+       absmovedir.y = fabs(this.movedir.y);
+       absmovedir.z = fabs(this.movedir.z);
+       this.pos2 = this.pos1 + this.movedir * (absmovedir * this.size - this.lip);
+
+// DOOR_START_OPEN is to allow an entity to be lighted in the closed position
+// but spawn in the open position
+       if (this.spawnflags & DOOR_START_OPEN)
+               door_init_startopen(this);
 
        if(autocvar_sv_doors_always_open)
        {
@@ -760,6 +791,19 @@ spawnfunc(func_door)
                        this.speed = 100;
        }
 
+       if (q3compat)
+       {
+               if (!this.dmg)
+                       this.dmg = 2;
+
+               if (!this.team)
+               {
+                       string t = GetField_fullspawndata(this, "team");
+                       // bones_was_here: same hack as used to support teamed items on Q3 maps
+                       if(t) this.team = crc16(false, t);
+               }
+       }
+
        settouch(this, door_touch);
 
 // LinkDoors can't be done until all of the doors have been spawned, so
index f185f4be8fc96e307ae05ee89078b3174396fb44..ce7025dd5488854e741bb166c9c3e2db8b1a8f2a 100644 (file)
@@ -4,7 +4,7 @@
 bool autocvar_sv_doors_always_open;
 #endif
 
-const int DOOR_START_OPEN = BIT(0);
+const int DOOR_START_OPEN = BIT(0); // has same meaning in Q3: reverse position 1 and 2
 const int DOOR_DONT_LINK = BIT(2);
 const int SPAWNFLAGS_GOLD_KEY = BIT(3); // Quake 1 compat, can only be used with func_door!
 const int SPAWNFLAGS_SILVER_KEY = BIT(4); // Quake 1 compat, can only be used with func_door!
@@ -13,6 +13,7 @@ const int DOOR_TOGGLE = BIT(5);
 const int DOOR_NONSOLID = BIT(10);
 const int DOOR_CRUSH = BIT(11); // can't use CRUSH cause that is the same as DOOR_DONT_LINK
 
+#define Q3_DOOR_CRUSHER BIT(2) // in Q3 this disables the auto reverse so the blocking player takes damage every frame
 
 #ifdef CSQC
 // stuff for preload
index 2376c5956de1adbdc227654e30b819c8d5137b0f..08faae9ebef1a1ca05f9c5b380ac41254d54c7b6 100644 (file)
@@ -7,7 +7,9 @@ void plat_link(entity this);
 void plat_delayedinit(entity this)
 {
        plat_link(this);
-       plat_spawn_inside_trigger(this); // the "start moving" trigger
+       // Q3 uses only a truth check of .targetname to decide whether to spawn a trigger
+       if(!Q3COMPAT_COMMON || this.targetname == "")
+               plat_spawn_inside_trigger(this); // the "start moving" trigger
 }
 
 float plat_send(entity this, entity to, float sf)
@@ -56,7 +58,12 @@ void plat_link(entity this)
 
 spawnfunc(func_plat)
 {
-       if (this.spawnflags & CRUSH)
+       if (q3compat)
+       {
+               this.spawnflags = 0; // Q3 plats have no spawnflags
+               if (!this.dmg) this.dmg = 2;
+       }
+       else if (this.spawnflags & CRUSH)
        {
                this.dmg = 10000;
        }
@@ -91,14 +98,28 @@ spawnfunc(func_plat)
 
        if (q3compat)
        {
-               // CPMA adds these fields for overriding the engine sounds
+               // CPMA adds these fields for overriding the Q3 default sounds
                string s = GetField_fullspawndata(this, "sound_start", true);
                string e = GetField_fullspawndata(this, "sound_end", true);
 
                if (s)
                        this.noise = strzone(s);
+               else
+               {
+                       // PK3s supporting Q3A sometimes include custom sounds at Q3 default paths
+                       s = "sound/movers/plats/pt1_strt.wav";
+                       if (FindFileInMapPack(s))
+                               this.noise = s;
+               }
+
                if (e)
                        this.noise1 = strzone(e);
+               else
+               {
+                       e = "sound/movers/plats/pt1_end.wav";
+                       if (FindFileInMapPack(e))
+                               this.noise1 = e;
+               }
        }
 
        if(this.noise && this.noise != "")
@@ -122,8 +143,8 @@ spawnfunc(func_plat)
 
        setblocked(this, plat_crush);
 
-       if (!this.speed) this.speed = 150;
-       if (!this.lip) this.lip = 16;
+       if (!this.speed) this.speed = q3compat ? 200 : 150;
+       if (!this.lip) this.lip = q3compat ? 8 : 16;
        if (!this.height) this.height = this.size.z - this.lip;
 
        this.pos1 = this.origin;
@@ -186,7 +207,8 @@ NET_HANDLE(ENT_CLIENT_PLAT, bool isnew)
                set_movetype(this, MOVETYPE_PUSH);
                this.move_time = time;
 
-               plat_spawn_inside_trigger(this);
+               if(!Q3COMPAT_COMMON || this.targetname == "")
+                       plat_spawn_inside_trigger(this);
        }
 
        if(sf & SF_TRIGGER_RESET)
index 28b420b20ab4f2ed6c49bde68e5383d365de1b6b..9e1f635b32c9982ab594d10c3036bad243f6e00e 100644 (file)
@@ -173,13 +173,21 @@ void plat_use(entity this, entity actor, entity trigger)
        plat_go_down(this);
 }
 
+void plat_target_use(entity this, entity actor, entity trigger)
+{
+       if (this.state == STATE_TOP)
+               this.nextthink = this.ltime + 1;
+       else if (this.state != STATE_UP)
+               plat_go_up(this);
+}
+
 // WARNING: backwards compatibility because people don't use already existing fields :(
 // TODO: Check if any maps use these fields and remove these fields if it doesn't break maps
 .string sound1, sound2;
 
 void plat_reset(entity this)
 {
-       if(this.targetname && this.targetname != "")
+       if(this.targetname && this.targetname != "" && !Q3COMPAT_COMMON)
        {
                setorigin(this, this.pos1);
                this.state = STATE_UP;
@@ -189,7 +197,7 @@ void plat_reset(entity this)
        {
                setorigin(this, this.pos2);
                this.state = STATE_BOTTOM;
-               this.use = plat_trigger_use;
+               this.use = (this.targetname != "" && Q3COMPAT_COMMON) ? plat_target_use : plat_trigger_use;
        }
 
 #ifdef SVQC
index f9cc536eda728e8e402121af6c5fe33a702cc179..9d97d66101451fa042925a756b13015da9fede91 100644 (file)
@@ -7,4 +7,5 @@
 #include <common/mapobjects/target/spawn.qc>
 #include <common/mapobjects/target/spawnpoint.qc>
 #include <common/mapobjects/target/speaker.qc>
+#include <common/mapobjects/target/speed.qc>
 #include <common/mapobjects/target/voicescript.qc>
index e3f4cede9912314c03bbd2cef0d9a5cc494e20b4..48c57c42d9f12b0090ece1df715229177569591f 100644 (file)
@@ -7,4 +7,5 @@
 #include <common/mapobjects/target/spawn.qh>
 #include <common/mapobjects/target/spawnpoint.qh>
 #include <common/mapobjects/target/speaker.qh>
+#include <common/mapobjects/target/speed.qh>
 #include <common/mapobjects/target/voicescript.qh>
diff --git a/qcsrc/common/mapobjects/target/speed.qc b/qcsrc/common/mapobjects/target/speed.qc
new file mode 100644 (file)
index 0000000..fb7da58
--- /dev/null
@@ -0,0 +1,209 @@
+#include "speed.qh"
+
+#define XYZ_ARRAY(name) float name[3]
+#define ARRAY_AS_VECTOR(a) ((a)[0] * '1 0 0' + (a)[1] * '0 1 0' + (a)[2] * '0 0 1')
+#define VECTOR_TO_ARRAY(a, e) { vector v = (e); (a)[0] = v.x; (a)[1] = v.y; (a)[2] = v.z; }
+#define FOR_XYZ(idx) for(int idx = 0; idx < 3; ++idx)
+vector target_speed_calculatevelocity(entity this, float speed, entity pushed_entity)
+{
+       bool is_percentage = boolean(this.spawnflags & SPEED_PERCENTAGE);
+       bool is_add = boolean(this.spawnflags & SPEED_ADD);
+       bool is_launcher = boolean(this.spawnflags & SPEED_LAUNCHER);
+
+       bool is_positive[3];
+       is_positive[0] = boolean(this.spawnflags & SPEED_POSITIVE_X);
+       is_positive[1] = boolean(this.spawnflags & SPEED_POSITIVE_Y);
+       is_positive[2] = boolean(this.spawnflags & SPEED_POSITIVE_Z);
+
+       bool is_negative[3];
+       is_negative[0] = boolean(this.spawnflags & SPEED_NEGATIVE_X);
+       is_negative[1] = boolean(this.spawnflags & SPEED_NEGATIVE_Y);
+       is_negative[2] = boolean(this.spawnflags & SPEED_NEGATIVE_Z);
+
+       // speed cannot be negative except when subtracting
+       if(!is_add)
+       {
+               speed = max(speed, 0);
+       }
+
+       XYZ_ARRAY(pushvel);
+       VECTOR_TO_ARRAY(pushvel, pushed_entity.velocity);
+
+       FOR_XYZ(i)
+       {
+               // launcher can only be either positive or negative not both
+               if(is_launcher && is_positive[i] && is_negative[i])
+               {
+                       is_positive[i] = is_negative[i] = false;
+               }
+
+               // ignore this direction
+               if(!is_positive[i] && !is_negative[i])
+               {
+                       pushvel[i] = 0;
+               }
+       }
+
+       float oldspeed = vlen(ARRAY_AS_VECTOR(pushvel));
+
+       // the speed field is used to specify the percentage of the current speed
+       if(is_percentage)
+       {
+               speed = oldspeed * speed / 100;
+       }
+
+       float launcherspeed = 0;
+
+       // do this properly when not playing a Q3 map, do not put this in the loop
+       if(!STAT(Q3COMPAT, pushed_entity))
+       {
+               launcherspeed += speed;
+
+               // add the add speed in the same variable
+               // as it goes in the same direction
+               if(is_add) launcherspeed += oldspeed;
+       }
+
+       FOR_XYZ(i)
+       {
+               if(((pushvel[i] != 0) || is_launcher) && (is_positive[i] != is_negative[i]))
+               {
+                       if(is_launcher)
+                       {
+                               // every direction weighs the same amount on launchers
+                               // movedir does not matter
+                               pushvel[i] = 1;
+
+                               // this does not belong inside the loop
+                               // only simulate this bug when playing a Q3 map
+                               if(STAT(Q3COMPAT, pushed_entity))
+                               {
+                                       launcherspeed += speed;
+
+                                       // add the add speed in the same variable
+                                       // as it goes in the same direction
+                                       if(is_add) launcherspeed += oldspeed;
+                               }
+                       }
+
+                       if(is_positive[i])
+                       {
+                               pushvel[i] = copysign(pushvel[i], 1);
+                       }
+                       else if(is_negative[i])
+                       {
+                               pushvel[i] = copysign(pushvel[i], -1);
+                       }
+               }
+       }
+
+       XYZ_ARRAY(oldvel);
+       VECTOR_TO_ARRAY(oldvel, pushed_entity.velocity);
+
+       if(is_launcher)
+       {
+               // launcher will always launch you in the correct direction
+               // even if speed is set to a negative value, fabs() is correct
+               VECTOR_TO_ARRAY(pushvel, normalize(ARRAY_AS_VECTOR(pushvel)) * fabs(launcherspeed));
+       }
+       else
+       {
+               VECTOR_TO_ARRAY(pushvel, normalize(ARRAY_AS_VECTOR(pushvel)) * speed)
+
+               if(is_add)
+               {
+                       VECTOR_TO_ARRAY(pushvel, ARRAY_AS_VECTOR(pushvel) + ARRAY_AS_VECTOR(oldvel));
+               }
+       }
+
+       FOR_XYZ(i)
+       {
+               // preserve unaffected directions
+               if(!is_positive[i] && !is_negative[i])
+               {
+                       pushvel[i] = oldvel[i];
+               }
+       }
+
+       return ARRAY_AS_VECTOR(pushvel);
+}
+#undef XYZ_ARRAY
+#undef ARRAY_AS_VECTOR
+#undef VECTOR_TO_ARRAY
+#undef FOR_XYZ
+
+REGISTER_NET_LINKED(ENT_CLIENT_TARGET_SPEED)
+
+void target_speed_use(entity this, entity actor, entity trigger)
+{
+       if(this.active != ACTIVE_ACTIVE)
+               return;
+
+       actor.velocity = target_speed_calculatevelocity(this, this.speed, actor);
+}
+
+void target_speed_reset(entity this)
+{
+       this.active = ACTIVE_ACTIVE;
+}
+
+#ifdef SVQC
+void target_speed_link(entity this);
+
+/*
+ * ENTITY PARAMETERS:
+ *
+ *   targetname:  Activating trigger points to this.
+ *   speed:       Speed value to set (default: 100).
+ */
+spawnfunc(target_speed)
+{
+       this.active = ACTIVE_ACTIVE;
+       this.setactive = generic_netlinked_setactive;
+       this.use = target_speed_use;
+       this.reset = target_speed_reset;
+
+       // FIXME: zero and unset cannot be disambiguated in xonotic
+       //if (!this.speed)
+       //      this.speed = 100;
+
+       target_speed_link(this);
+}
+
+bool target_speed_send(entity this, entity to, float sf)
+{
+       WriteHeader(MSG_ENTITY, ENT_CLIENT_TARGET_SPEED);
+
+       WriteInt24_t(MSG_ENTITY, this.spawnflags);
+       WriteByte(MSG_ENTITY, this.active);
+       WriteString(MSG_ENTITY, this.targetname);
+       WriteCoord(MSG_ENTITY, this.speed);
+
+       return true;
+}
+
+void target_speed_link(entity this)
+{
+       Net_LinkEntity(this, false, 0, target_speed_send);
+}
+
+#elif defined(CSQC)
+
+void target_speed_remove(entity this)
+{
+       strfree(this.targetname);
+}
+
+NET_HANDLE(ENT_CLIENT_TARGET_SPEED, bool isnew)
+{
+       this.spawnflags = ReadInt24_t();
+       this.active = ReadByte();
+       this.targetname = strzone(ReadString());
+       this.speed = ReadCoord();
+
+       this.use = target_speed_use;
+       this.entremove = target_speed_remove;
+
+       return true;
+}
+#endif
diff --git a/qcsrc/common/mapobjects/target/speed.qh b/qcsrc/common/mapobjects/target/speed.qh
new file mode 100644 (file)
index 0000000..6e85444
--- /dev/null
@@ -0,0 +1,12 @@
+#pragma once
+
+
+#define SPEED_PERCENTAGE BIT(0)
+#define SPEED_ADD        BIT(1)
+#define SPEED_POSITIVE_X BIT(2)
+#define SPEED_NEGATIVE_X BIT(3)
+#define SPEED_POSITIVE_Y BIT(4)
+#define SPEED_NEGATIVE_Y BIT(5)
+#define SPEED_POSITIVE_Z BIT(6)
+#define SPEED_NEGATIVE_Z BIT(7)
+#define SPEED_LAUNCHER   BIT(8)
index f7281e5e4ae2619098dffa22cefb386b9236105f..4dc68505740a7a1decf0dc9a21f62e98c7123db5 100644 (file)
@@ -249,8 +249,23 @@ bool jumppad_push(entity this, entity targ, bool is_velocity_pad)
 
        vector org = targ.origin;
 
-       if(Q3COMPAT_COMMON || this.spawnflags & PUSH_STATIC)
+       if(STAT(Q3COMPAT, targ) || this.spawnflags & PUSH_STATIC)
+       {
                org = (this.absmin + this.absmax) * 0.5;
+       }
+
+       bool already_pushed = false;
+       if(is_velocity_pad) // remember velocity jump pads
+       {
+               if(this == targ.last_pushed || (targ.last_pushed && !STAT(Q3COMPAT, targ))) // if q3compat is active overwrite last stored jump pad, otherwise ignore
+               {
+                       already_pushed = true;
+               }
+               else
+               {
+                       targ.last_pushed = this; // may be briefly out of sync between client and server if client prediction is toggled
+               }
+       }
 
        bool already_pushed = false;
        if(is_velocity_pad) // remember velocity jump pads
index 7146cc52cb057a3d4d12f19c5664017d0aa71a8f..96ab74aaa684e21c3db602c25f887bf2976aec0b 100644 (file)
@@ -3,7 +3,15 @@
 
 const int PUSH_ONCE = BIT(0); // legacy, deactivate with relay instead
 const int PUSH_SILENT = BIT(1); // not used?
-const int PUSH_STATIC = BIT(12); // xonotic-only, Q3 already behaves like this by default
+#define PUSH_STATIC BIT(12) // xonotic-only, Q3 already behaves like this by default
+
+#define PUSH_VELOCITY_PLAYERDIR_XY        BIT(0)
+#define PUSH_VELOCITY_ADD_XY              BIT(1)
+#define PUSH_VELOCITY_PLAYERDIR_Z         BIT(2)
+#define PUSH_VELOCITY_ADD_Z               BIT(3)
+#define PUSH_VELOCITY_BIDIRECTIONAL_XY    BIT(4)
+#define PUSH_VELOCITY_BIDIRECTIONAL_Z     BIT(5)
+#define PUSH_VELOCITY_CLAMP_NEGATIVE_ADDS BIT(6)
 
 #define PUSH_VELOCITY_PLAYERDIR_XY        BIT(0)
 #define PUSH_VELOCITY_ADD_XY              BIT(1)
index 8a01893f1019c423927029d57b004baa8f66b88c..448712f78d7154bd7f7c3ef29f068ab1c4b5bc43 100644 (file)
@@ -26,6 +26,11 @@ REGISTER_SP(RACE_LAPS);
 REGISTER_SP(RACE_TIME);
 REGISTER_SP(RACE_FASTEST);
 
+REGISTER_SP(CTS_STRAFE);
+REGISTER_SP(CTS_STARTSPEED);
+REGISTER_SP(CTS_AVGSPEED);
+REGISTER_SP(CTS_TOPSPEED);
+
 REGISTER_SP(ASSAULT_OBJECTIVES);
 
 REGISTER_SP(CTF_CAPS);
index 96a136cadc0dd3b11a6b5007345d6421afa80deb..98279ed34faf55a3ed2c8f8d2cc71d8fcbe84c8d 100644 (file)
@@ -355,7 +355,11 @@ bool autocvar_sv_slick_applygravity;
 #endif
 REGISTER_STAT(SLICK_APPLYGRAVITY, bool, autocvar_sv_slick_applygravity)
 
+#ifdef SVQC
+int autocvar_sv_q3compat_jumppads;
+#endif
 REGISTER_STAT(Q3COMPAT, int, q3compat)
+REGISTER_STAT(Q3COMPAT_JUMPPADS, int, autocvar_sv_q3compat_jumppads)
 // FIXME: workaround for https://gitlab.com/xonotic/xonotic-data.pk3dir/-/issues/2812
 #ifdef SVQC
        #define Q3COMPAT_COMMON q3compat
index 2329c364e2998f16d425856292da6c6431b181e3..243407a00122e35a79a86f68ca60587da0d96c81 100644 (file)
@@ -49,6 +49,7 @@ CLASS(Crylink, Weapon)
         P(class, prefix, speed, float, BOTH) \
         P(class, prefix, spreadtype, float, SEC) \
         P(class, prefix, spread, float, BOTH) \
+        P(class, prefix, swap_attacks, float, NONE) \
         P(class, prefix, switchdelay_drop, float, NONE) \
         P(class, prefix, switchdelay_raise, float, NONE) \
         P(class, prefix, weaponreplace, string, NONE) \
index c82e892f721815fdeff9c6b46d0260eee26fe86d..c62dbad50db98188ae950d32014101f13df51319 100644 (file)
@@ -25,6 +25,7 @@
 #include <server/scores_rules.qc>
 #include <server/spawnpoints.qc>
 #include <server/steerlib.qc>
+#include <server/strafe.qc>
 #include <server/teamplay.qc>
 #include <server/tests.qc>
 #include <server/world.qc>
index 52574efecc1ac689a2d9a341cc9a10a7d40c6943..cc41db238f1e65908a7a3c68f3a6957251d0e8ae 100644 (file)
@@ -25,6 +25,7 @@
 #include <server/scores_rules.qh>
 #include <server/spawnpoints.qh>
 #include <server/steerlib.qh>
+#include <server/strafe.qh>
 #include <server/teamplay.qh>
 #include <server/tests.qh>
 #include <server/world.qh>
index f05edfcb5a670225986eb18ae6f679645d540c9b..4d61a3962268b62df4e64fa3d108d7a9ee174b34 100644 (file)
@@ -26,6 +26,7 @@
 #include <server/scores_rules.qh>
 #include <server/teamplay.qh>
 #include <server/world.qh>
+#include <lib/misc.qh>
 
 //  used by GameCommand_make_mapinfo()
 void make_mapinfo_Think(entity this)
@@ -1210,6 +1211,38 @@ void GameCommand_nospectators(int request)
        }
 }
 
+void GameCommand_printplayer(int request, int argc)
+{
+       switch (request)
+        {
+               case CMD_REQUEST_COMMAND:
+                {
+                       entity player = GetIndexedEntity(argc, 1);
+                        if (player.playerid)
+                        {
+                               GameLogEcho(strcat(
+                                                   strcat(
+                                                          ":playerinfo:", ftos(player.playerid),
+                                                          ":", ftos(etof(player)),
+                                                          ":", ftos(CS_CVAR(player).cvar_cl_allow_uidtracking),
+                                                          ":", ftos(CS_CVAR(player).cvar_cl_allow_uid2name)),
+                                                   strcat(
+                                                          ":", ftos(CS_CVAR(player).cvar_cl_allow_uidranking),
+                                                          ":", ((IS_REAL_CLIENT(player)) ? GameLog_ProcessIP(player.netaddress) : "bot"),
+                                                          ":", player.crypto_idfp,
+                                                          ":", playername(player.netname, player.team, false))));
+                        }
+                       return;
+                }
+               default:
+                case CMD_REQUEST_USAGE:
+                {
+                       LOG_HELP("Usage:^3 sv_cmd printplayer <player_entity_id>");
+                        return;
+                }
+        }
+}
+
 void GameCommand_printstats(int request)
 {
        switch (request)
@@ -1717,6 +1750,36 @@ void GameCommand_warp(int request, int argc)
        }
 }
 
+void IRCSay(string msgstr)
+{
+       if(msgstr == "")
+               return;
+
+       string prefix;
+       if(substring(msgstr, 0, 3) == "^4*") // actions
+               prefix = "\{3}";
+       else
+               prefix = "\{1}";
+
+       msgstr = strcat(prefix, strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint
+
+       FOREACH_CLIENTSLOT(true,
+       {
+               if(!intermission_running)
+               if((autocvar_g_chat_nospectators == 1) || (autocvar_g_chat_nospectators == 2 && !(warmup_stage || game_stopped)))
+               if(IS_PLAYER(it))
+                       continue;
+               if(IS_REAL_CLIENT(it))
+                       sprint(it, msgstr);
+       });
+}
+
+void GameCommand_ircmsg(int request, int argc, string command)
+{
+       IRCSay(substring(command, strlen(argv(0))+1, strlen(command)));
+        return;
+}
+
 /* use this when creating a new command, making sure to place it in alphabetical order... also,
 ** ADD ALL NEW COMMANDS TO commands.cfg WITH PROPER ALIASES IN THE SAME FASHION!
 void GameCommand_(int request)
@@ -1768,10 +1831,12 @@ SERVER_COMMAND(extendmatchtime, "Increase the timelimit value incrementally") {
 SERVER_COMMAND(gametype, "Simple command to change the active gametype") { GameCommand_gametype(request, arguments); }
 SERVER_COMMAND(gettaginfo, "Get specific information about a weapon model") { GameCommand_gettaginfo(request, arguments); }
 SERVER_COMMAND(gotomap, "Simple command to switch to another map") { GameCommand_gotomap(request, arguments); }
+SERVER_COMMAND(ircmsg, "Utility function to forward chat messages from IRC/discord/whatever") { GameCommand_ircmsg(request, arguments, command); }
 SERVER_COMMAND(lockteams, "Disable the ability for players to switch or enter teams") { GameCommand_lockteams(request); }
 SERVER_COMMAND(make_mapinfo, "Automatically rebuild mapinfo files") { GameCommand_make_mapinfo(request); }
 SERVER_COMMAND(moveplayer, "Change the team/status of a player") { GameCommand_moveplayer(request, arguments); }
 SERVER_COMMAND(nospectators, "Automatically remove spectators from a match") { GameCommand_nospectators(request); }
+SERVER_COMMAND(printplayer, "Print information about a player") { GameCommand_printplayer(request, arguments); }
 SERVER_COMMAND(printstats, "Dump eventlog player stats and other score information") { GameCommand_printstats(request); }
 SERVER_COMMAND(radarmap, "Generate a radar image of the map") { GameCommand_radarmap(request, arguments); }
 SERVER_COMMAND(reducematchtime, "Decrease the timelimit value incrementally") { GameCommand_reducematchtime(request); }
index 9333ebfed50973872e0c0111aba93b4886cde5eb..666598d02545e1afa39d8c1d474e39af5116a846 100644 (file)
@@ -256,12 +256,29 @@ spawnfunc(target_score)
        this.use = score_use;
 }
 
+#define FRAGSFILTER_REMOVER BIT(0)
+#define FRAGSFILTER_RUNONCE BIT(1) // unused
+#define FRAGSFILTER_SILENT  BIT(2)
+#define FRAGSFILTER_RESET   BIT(3)
+
 void fragsfilter_use(entity this, entity actor, entity trigger)
 {
        if(!IS_PLAYER(actor))
                return;
        if(actor.fragsfilter_cnt >= this.frags)
+       {
+               if(this.spawnflags & FRAGSFILTER_RESET)
+                       actor.fragsfilter_cnt = 0;
+               else if(this.spawnflags & FRAGSFILTER_REMOVER)
+                       actor.fragsfilter_cnt -= this.frags;
                SUB_UseTargets(this, actor, trigger);
+       }
+       else if(!(this.spawnflags & FRAGSFILTER_SILENT))
+       {
+               int req_frags = this.frags - actor.fragsfilter_cnt;
+               centerprint(actor, sprintf("%d more frag%s needed", req_frags, req_frags > 1 ? "s" : ""));
+               play2(actor, SND(TALK));
+       }
 }
 spawnfunc(target_fragsFilter)
 {
index 44bce18a823533eeaf494f6b0dc654d5903bec68..74eef6a7c403ea8644c9fa8156905376502c0b94 100644 (file)
@@ -709,6 +709,24 @@ void Damage(entity targ, entity inflictor, entity attacker, float damage, int de
                mirrordamage = M_ARGV(5, float);
                force = M_ARGV(6, vector);
 
+               // Dr. Jaska:
+               // Quake 3 DeFRaG's cpm physic's rocketlauncher has 1.2x horizontal force multiplier
+               // This aims to reproduce it in Quake 3 DeFRaG cpm and XDF physics if damagepush_speedfactor is 0
+               // CPMA cpm physics should be active in .arena maps and Quake 3 DeFRaG cpm physics in .defi maps
+               // It is only intended to be used with 250 base force for devastator which matches Q3 if our
+               // target's damageforcescale is also 2
+               if(force)
+               if(q3compat & Q3COMPAT_DEFI)
+               if(autocvar_g_balance_damagepush_speedfactor == 0)
+               if(attacker.(weaponentity).m_weapon == WEP_DEVASTATOR)
+               if(autocvar_g_balance_devastator_force == 250)
+               if(targ.damageforcescale == 2)
+               if((cvar_string("g_mod_physics") == "CPMA") || (cvar_string("g_mod_physics") == "XDF"))
+               {
+                       force.x *= 1.2;
+                       force.y *= 1.2;
+               }
+
                if(IS_PLAYER(targ) && damage > 0 && attacker)
                {
                        for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
index 8a2da54aa4a721c2312017b0bd30ae52bd72e561..76942c1873fd0b6289440ee6195ca1aca4a60e8d 100644 (file)
@@ -263,7 +263,7 @@ void remove_except_protected(entity e)
 void remove_unsafely(entity e)
 {
     if(e.classname == "spike")
-        error("Removing spikes is forbidden (crylink bug), please report");
+        LOG_WARN("Removing spikes is forbidden (crylink bug), please report");
     builtin_remove(e);
 }
 
@@ -467,6 +467,32 @@ string GetField_fullspawndata(entity e, string f, ...)
        return v;
 }
 
+/*
+=============
+FindFileInMapPack
+
+Returns the first matching VFS file path that exists in the current map's pack.
+Returns string_null if no files match or the map isn't packaged.
+=============
+*/
+string FindFileInMapPack(string pattern)
+{
+       if(!checkextension("DP_QC_FS_SEARCH_PACKFILE"))
+               return string_null;
+
+       string base_pack = whichpack(strcat("maps/", mapname, ".bsp"));
+       if(base_pack == "" || !base_pack) // this map isn't packaged or there was an error
+               return string_null;
+
+       int glob = search_packfile_begin(pattern, true, true, base_pack);
+       if(glob < 0)
+               return string_null;
+
+       string file = search_getfilename(glob, 0);
+       search_end(glob);
+       return file;
+}
+
 void WarpZone_PostInitialize_Callback()
 {
        // create waypoint links for warpzones
index f8af3fc5f716c5ca12eb604c6f32ecd55c64d496..9f95a5a6b6b472544c007ef15e4de961f606c74c 100644 (file)
 #include <server/spawnpoints.qh>
 #include <server/weapons/common.qh>
 #include <server/world.qh>
+#include <server/strafe.qh>
+
 
 .string stored_netname; // TODO: store this information independently of race-based gamemodes
 
+.float race_startspeed;
+.float race_startspeed_best;
+.float race_avgspeed_sum;
+.float race_avgspeed_time;
+.float race_avgspeed_best;
+.float race_topspeed;
+.float race_topspeed_best;
+
 string uid2name(string myuid)
 {
        string s = db_get(ServerProgsDB, strcat("/uid2name/", myuid));
@@ -54,15 +64,23 @@ string uid2name(string myuid)
        return s;
 }
 
-void write_recordmarker(entity pl, float tstart, float dt)
+void write_recordmarker(entity pl, float newpos, float tstart, float dt)
 {
-    GameLogEcho(strcat(":recordset:", ftos(pl.playerid), ":", ftos(dt)));
+    GameLogEcho(strcat(":recordset:", ftos(newpos), ":", ftos(pl.playerid), ":", ftos(etof(pl)), ":", ftos(dt)));
 
     // also write a marker into demo files for demotc-race-record-extractor to find
-    stuffcmd(pl,
-             strcat(
-                 strcat("//", strconv(2, 0, 0, GetGametype()), " RECORD SET ", TIME_ENCODED_TOSTRING(TIME_ENCODE(dt), false)),
-                 " ", ftos(tstart), " ", ftos(dt), "\n"));
+    if (pl.crypto_idfp != "") {
+      stuffcmd(pl,
+               strcat(
+                   strcat("//", strconv(2, 0, 0, GetGametype()), " RECORD SET ", TIME_ENCODED_TOSTRING(TIME_ENCODE(dt), false)),
+                   strcat(" ", ftos(tstart), " ", ftos(dt), " ", ftos(newpos), " "),
+                   strcat(pl.crypto_idfp, "\n")));
+    } else {
+      stuffcmd(pl,
+               strcat(
+                   strcat("//", strconv(2, 0, 0, GetGametype()), " RECORD SET ", TIME_ENCODED_TOSTRING(TIME_ENCODE(dt), false)),
+                   strcat(" ", ftos(tstart), " ", ftos(dt), " ", ftos(newpos), " ANONYMOUS\n")));
+    }
 }
 
 IntrusiveList g_race_targets;
@@ -172,6 +190,7 @@ float race_checkpoint_lastlaps[MAX_CHECKPOINTS];
 entity race_checkpoint_lastplayers[MAX_CHECKPOINTS];
 
 .float race_checkpoint_record[MAX_CHECKPOINTS];
+.float current_checkpoint_record[MAX_CHECKPOINTS];
 
 float race_highest_checkpoint;
 float race_timed_checkpoint;
@@ -219,11 +238,19 @@ void race_SendNextCheckpoint(entity e, float spec) // qualifying only
                return;
 
        int cp = e.race_checkpoint;
-       float recordtime = race_checkpoint_records[cp];
-       float myrecordtime = e.race_checkpoint_record[cp];
-       string recordholder = race_checkpoint_recordholders[cp];
-       if(recordholder == e.netname)
+        float myrecordtime = e.race_checkpoint_record[cp];
+        float recordtime;
+        string recordholder;
+        if (autocvar_g_cts_cptimes_onlyself && (race_CheckpointNetworkID(cp) < 254)) {  // cp 254 - start line, cp 255 - finish line
+          recordtime = myrecordtime;
+          recordholder = "";
+        } else {
+          recordtime = race_checkpoint_records[cp];
+
+         recordholder = race_checkpoint_recordholders[cp];
+         if(recordholder == e.netname)
                recordholder = "";
+        }
 
        if(!IS_REAL_CLIENT(e))
                return;
@@ -366,9 +393,9 @@ void race_setTime(string map, float t, string myuid, string mynetname, entity e,
 {
        // netname only used TEMPORARILY for printing
        int newpos = race_readPos(map, t);
-
+  int i;
        int player_prevpos = 0;
-       for(int i = 1; i <= RANKINGS_CNT; ++i)
+       for(i = 1; i <= RANKINGS_CNT; ++i)
        {
                if(race_readUID(map, i) == myuid)
                        player_prevpos = i;
@@ -394,6 +421,29 @@ void race_setTime(string map, float t, string myuid, string mynetname, entity e,
                return;
        }
 
+       string body = sprintf("RECORDv1\n%s\n%s\n%s\n%s\n%d\n", strftime(false, "%Y-%m-%dT%H:%M:%SZ"), map, e.crypto_idfp, e.netaddress, t);
+        bool first_cp = true;
+        for (i=0; i < MAX_CHECKPOINTS; i++) {
+               if (e.current_checkpoint_record[i] > 0) {
+                        if (first_cp) {
+                            body = strcat(body, sprintf("%d %d", i, TIME_ENCODE(e.current_checkpoint_record[i])));
+                            first_cp = false;
+                        } else {
+                          body = strcat(body, sprintf(";%d %d", i, TIME_ENCODE(e.current_checkpoint_record[i])));
+                        }
+               }
+               }
+       body = strcat(body, sprintf("\n%f\n%f\n%f\n%f\n%s", e.race_topspeed_best, e.race_avgspeed_best, e.race_startspeed_best, e.strafe_efficiency_best, e.netname));
+        float r;
+        float buf = buf_create();
+        bufstr_set(buf, 0, body);
+        r = crypto_uri_postbuf(autocvar_sv_checkpoint_house_url, URI_GET_CURL + curl_uri_get_pos, "text/plain", "&", buf, 0);
+        if (r) {
+            curl_uri_get_pos = (curl_uri_get_pos + 1) % (URI_GET_CURL_END - URI_GET_CURL + 1);
+          }
+        buf_del(buf);
+        write_recordmarker(e, newpos, time - TIME_DECODE(t), TIME_DECODE(t));
+
        // if we didn't hit a return yet, we have a new record!
 
        // if the player does not have a UID we can unfortunately not store the record, as the rankings system relies on UIDs
@@ -417,9 +467,8 @@ void race_setTime(string map, float t, string myuid, string mynetname, entity e,
        // store new ranking
        race_writeTime(GetMapname(), t, myuid);
 
-       if (newpos == 1 && showmessage)
+       if (showmessage)
        {
-               write_recordmarker(e, time - TIME_DECODE(t), TIME_DECODE(t));
                race_send_recordtime(MSG_ALL);
        }
 
@@ -486,7 +535,21 @@ void race_SendTime(entity e, float cp, float t, float tvalid)
        {
                int s = GameRules_scoring_add(e, RACE_FASTEST, 0);
                if(!s || t < s)
+               {
                        GameRules_scoring_add(e, RACE_FASTEST, t - s);
+
+                       e.strafe_efficiency_best = e.strafe_efficiency_sum / e.strafe_efficiency_time;
+                       PlayerScore_Set(e, SP_CTS_STRAFE, floor(e.strafe_efficiency_best * 1000 + .5));
+
+                       e.race_startspeed_best = e.race_startspeed;
+                       PlayerScore_Set(e, SP_CTS_STARTSPEED, floor(e.race_startspeed_best + .5));
+
+                       e.race_avgspeed_best = e.race_avgspeed_sum / e.race_avgspeed_time;
+                       PlayerScore_Set(e, SP_CTS_AVGSPEED, floor(e.race_avgspeed_best + .5));
+
+                       e.race_topspeed_best = e.race_topspeed;
+                       PlayerScore_Set(e, SP_CTS_TOPSPEED, floor(e.race_topspeed_best + .5));
+               }
                if(!g_race_qualifying)
                {
                        s = GameRules_scoring_add(e, RACE_TIME, 0);
@@ -518,12 +581,17 @@ void race_SendTime(entity e, float cp, float t, float tvalid)
 
                if(tvalid)
                {
-                       recordtime = race_checkpoint_records[cp];
-                       float myrecordtime = e.race_checkpoint_record[cp];
-                       recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
-                       if(recordholder == e.netname)
+                        float myrecordtime = e.race_checkpoint_record[cp];
+                        if (autocvar_g_cts_cptimes_onlyself && (race_CheckpointNetworkID(cp) < 254)) {  // cp 254 - start line, cp 255 - finish line
+                          recordtime = myrecordtime;
+                          recordholder = "";
+                        } else {
+                          recordtime = race_checkpoint_records[cp];
+                          recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it
+                          if(recordholder == e.netname)
                                recordholder = "";
 
+                        }
                        if(t != 0)
                        {
                                if(cp == race_timed_checkpoint)
@@ -771,11 +839,17 @@ void checkpoint_passed(entity this, entity player)
 
                if(!this.race_checkpoint) // start line
                {
+                       player.race_startspeed = vlen(vec2(player.velocity));
                        player.race_laptime = time;
                        player.race_movetime = player.race_movetime_frac = player.race_movetime_count = 0;
                        player.race_penalty_accumulator = 0;
                        player.race_lastpenalty = NULL;
-               }
+                 for (int i=0; i < MAX_CHECKPOINTS; i++) {
+                               player.current_checkpoint_record[i] = 0;
+                       }
+               } else {
+                   player.current_checkpoint_record[this.race_checkpoint] = player.race_movetime; 
+       }
 
                if(g_race_qualifying)
                        race_SendNextCheckpoint(player, 0);
index 1e851ee3041e7fd671397c917571292b5a5ef863..fade8f795fa7939351fbf8f5ae65fb1b06ed53c0 100644 (file)
@@ -1,6 +1,7 @@
 #pragma once
 
 bool autocvar_g_allow_checkpoints;
+string autocvar_sv_checkpoint_house_url;
 
 float race_teams;
 
@@ -8,6 +9,7 @@ float race_teams;
 const float ST_RACE_LAPS = 1;
 
 int autocvar_g_cts_send_rankings_cnt = 15;
+bool autocvar_g_cts_cptimes_onlyself = false;
 
 int g_race_qualifying;
 
@@ -34,7 +36,7 @@ float race_completing;
 .entity race_respawn_spotref; // try THIS spawn in case you respawn
 
 // definitions for functions used outside race.qc
-void write_recordmarker(entity pl, float tstart, float dt);
+void write_recordmarker(entity pl, float newpos, float tstart, float dt);
 
 float race_PreviousCheckpoint(float f);
 float race_NextCheckpoint(float f);
diff --git a/qcsrc/server/strafe.qc b/qcsrc/server/strafe.qc
new file mode 100644 (file)
index 0000000..c25b1ef
--- /dev/null
@@ -0,0 +1,188 @@
+#include "strafe.qh"
+
+#include <common/physics/movetypes/movetypes.qh>
+#include <common/physics/player.qh>
+#include <common/stats.qh>
+
+.float race_started;
+
+float calculate_strafe_efficiency(entity strafeplayer, vector movement, float dt)
+{
+    if(!strafeplayer) return 0;
+
+    bool swimming = strafeplayer.waterlevel >= WATERLEVEL_SWIMMING;
+    float speed = vlen(vec2(strafeplayer.velocity));
+
+    if(speed <= 0 || swimming || !strafeplayer.race_started) return 0; // only calculate the efficiency if all conditions are met
+    strafeplayer.strafe_efficiency_time += dt;
+
+    // physics
+    bool   onground                      = IS_ONGROUND(strafeplayer) && !(PHYS_INPUT_BUTTON_JUMP(strafeplayer) || PHYS_INPUT_BUTTON_JETPACK(strafeplayer));
+    bool   onslick                       = IS_ONSLICK(strafeplayer);
+    bool   strafekeys;
+    float  maxspeed_mod                  = IS_DUCKED(strafeplayer) ? .5 : 1;
+    float  maxspeed_phys                 = onground ? PHYS_MAXSPEED(strafeplayer) : PHYS_MAXAIRSPEED(strafeplayer);
+    float  maxspeed                      = maxspeed_phys * maxspeed_mod;
+    float  movespeed;
+    float  bestspeed;
+    float  maxaccel_phys                 = onground ? PHYS_ACCELERATE(strafeplayer) : PHYS_AIRACCELERATE(strafeplayer);
+    float  maxaccel                      = maxaccel_phys;
+    float  vel_angle                     = vectoangles(strafeplayer.velocity).y - (vectoangles(strafeplayer.velocity).y > 180 ? 360 : 0); // change the range from 0° - 360° to -180° - 180° to match how view_angle represents angles
+    float  view_angle                    = PHYS_INPUT_ANGLES(strafeplayer).y;
+    float  angle;
+    int    keys_fwd;
+    float  wishangle;
+    bool   fwd                           = true;
+
+    // determine whether the player is pressing forwards or backwards keys
+    if(movement.x > 0)
+    {
+        keys_fwd = 1;
+    }
+    else if(movement.x < 0)
+    {
+        keys_fwd = -1;
+    }
+    else
+    {
+        keys_fwd = 0;
+    }
+
+    // determine player wishdir
+    if(movement.x == 0)
+    {
+        if(movement.y < 0)
+        {
+            wishangle = -90;
+        }
+        else if(movement.y > 0)
+        {
+            wishangle = 90;
+        }
+        else
+        {
+            wishangle = 0;
+        }
+    }
+    else
+    {
+        if(movement.y == 0)
+        {
+            wishangle = 0;
+        }
+        else
+        {
+            wishangle = RAD2DEG * atan2(movement.y, movement.x);
+            // wrap the wish angle if it exceeds ±90°
+            if(fabs(wishangle) > 90)
+            {
+                if(wishangle < 0) wishangle += 180;
+                else wishangle -= 180;
+                wishangle = -wishangle;
+            }
+        }
+    }
+
+    strafekeys = fabs(wishangle) == 90;
+
+    if(strafekeys && !onground && !swimming)
+    {
+        if(PHYS_MAXAIRSTRAFESPEED(strafeplayer) != 0)
+            maxspeed = min(PHYS_MAXAIRSTRAFESPEED(strafeplayer), PHYS_MAXAIRSPEED(strafeplayer) * maxspeed_mod);
+        if(PHYS_AIRSTRAFEACCELERATE(strafeplayer) != 0)
+            maxaccel = PHYS_AIRSTRAFEACCELERATE(strafeplayer);
+    }
+
+    movespeed = min(vlen(vec2(movement)), maxspeed);
+
+    maxaccel *= dt * movespeed;
+    bestspeed = max(movespeed - maxaccel, 0);
+
+    float strafespeed = speed; // speed minus friction
+
+    if((strafespeed > 0) && onground){
+        float strafefriction = onslick ? PHYS_FRICTION_SLICK(strafeplayer) : PHYS_FRICTION(strafeplayer);
+        float f = 1 - dt * strafefriction * max(PHYS_STOPSPEED(strafeplayer) / strafespeed, 1);
+
+        if(f <= 0)
+            strafespeed = 0;
+        else
+            strafespeed *= f;
+    }
+
+    // get current strafing angle ranging from -180° to +180°
+    // calculate view angle relative to the players current velocity direction
+    angle = vel_angle - view_angle;
+
+    // if the angle goes above 180° or below -180° wrap it to the opposite side since we want the interior angle
+    if (angle > 180) angle -= 360;
+    else if(angle < -180) angle += 360;
+
+    // determine whether the player is strafing forwards or backwards
+    // if the player isn't strafe turning use forwards/backwards keys to determine direction
+    if(!strafekeys)
+    {
+        if(keys_fwd > 0)
+        {
+            fwd = true;
+        }
+        else if(keys_fwd < 0)
+        {
+            fwd = false;
+        }
+        else
+        {
+            fwd = fabs(angle) <= 90;
+        }
+    }
+    // otherwise determine by examining the strafe angle
+    else
+    {
+        if(wishangle < 0) // detect direction since the direction is not yet set
+        {
+            fwd = angle <= -wishangle;
+        }
+        else
+        {
+            fwd = angle >= -wishangle;
+        }
+    }
+
+    // shift the strafe angle by 180° when strafing backwards
+    if(!fwd)
+    {
+        if(angle < 0) angle += 180;
+        else angle -= 180;
+    }
+
+    // invert the wish angle when strafing backwards
+    if(!fwd)
+    {
+        wishangle = -wishangle;
+    }
+
+    // note about accuracy: a few ticks after dying do still have race_started set to true causing minimal interference in the efficiency total
+    float efficiency = 0;
+    float moveangle = fabs(angle + wishangle);
+    float bestangle = (strafespeed > bestspeed ? acos(bestspeed / strafespeed) : 0) * RAD2DEG;
+    float prebestangle = (strafespeed > movespeed ? acos(movespeed / strafespeed) : 0) * RAD2DEG;
+
+    if(fabs(vlen(vec2(movement))) > 0)
+    {
+        if(moveangle >= 90)
+        {
+            efficiency = (moveangle - 90) / 90;
+            if(efficiency > 1) efficiency = 2 - efficiency;
+            efficiency *= -1;
+        }
+        else if(moveangle >= bestangle)
+        {
+            efficiency = (90 - moveangle) / (90 - bestangle);
+        }
+        else if(moveangle >= prebestangle)
+        {
+            efficiency = (moveangle - prebestangle) / (bestangle - prebestangle);
+        }
+    }
+    return efficiency;
+}
diff --git a/qcsrc/server/strafe.qh b/qcsrc/server/strafe.qh
new file mode 100644 (file)
index 0000000..ceecbb5
--- /dev/null
@@ -0,0 +1,7 @@
+#pragma once
+
+.float strafe_efficiency_sum;
+.float strafe_efficiency_time;
+.float strafe_efficiency_best;
+
+float calculate_strafe_efficiency(entity, vector, float);
index 819ef7a50625ba67450a0f2ecafc4bad72ed8e98..e869b1b311f5b3bab048a4c6d3a2bb4db20da08c 100644 (file)
@@ -550,6 +550,7 @@ sv_gameplayfix_gravityunaffectedbyticrate 1
 sv_gameplayfix_nogravityonground 1
 
 set sv_q3compat_changehitbox 0 "use Q3 player hitbox dimensions and camera height on Q3 maps (maps with an entry in a .arena or .defi file)"
+set sv_q3compat_jumppads 1 "calculate the jump pad trajectory starting at the center of the push trigger instead of the player origin, \"0\" = never, \"1\" = on Q3 maps, \"2\" = on all maps"
 
 set g_movement_highspeed 1 "multiplier scale for movement speed (applies to sv_maxspeed and sv_maxairspeed, also applies to air acceleration when g_movement_highspeed_q3_compat is set to 0)"
 set g_movement_highspeed_q3_compat 0 "apply speed modifiers to air movement in a more Q3-compatible way (only apply speed buffs and g_movement_highspeed to max air speed, not to acceleration)"
@@ -669,5 +670,6 @@ sv_disablenotify 1
 
 set sv_quickmenu_file "" "filename of a custom server's quickmenu that will be selectable from the default client's quickmenu. This file must be sent in a pk3 archive and should have an unique name (e.g. quickmenu-servername.txt) to prevent overriding existing quickmenus"
 
+set sv_checkpoint_house_url "http://127.0.0.1:10876/new-record"
 // Avoid perf problems near '0 0 0'; `set` without a description is because only current DP has this engine cvar
 set sv_areagrid_link_SOLID_NOT 0