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 ""
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 ""
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 ""
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 ""
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 ""
case "revivals": if (!mode) return CTX(_("SCO^revivals")); else LOG_HELP(strcat("^3", "revivals", " ^7", _("Number of revivals")));
case "rounds": if (!mode) return CTX(_("SCO^rounds won")); else LOG_HELP(strcat("^3", "rounds", " ^7", _("Number of rounds won")));
case "score": if (!mode) return CTX(_("SCO^score")); else LOG_HELP(strcat("^3", "score", " ^7", _("Total score")));
+ case "avgspeed": if (!mode) return CTX(_("SCO^average speed"));else LOG_HELP(strcat("^3", "avgspeed", " ^7", _("Average speed (CTS)")));
+ case "topspeed": if (!mode) return CTX(_("SCO^top speed")); else LOG_HELP(strcat("^3", "topspeed", " ^7", _("Top speed (CTS)")));
+ case "startspeed": if (!mode) return CTX(_("SCO^start speed")); else LOG_HELP(strcat("^3", "startspeed", " ^7", _("Start speed (CTS)")));
+ case "strafe": if (!mode) return CTX(_("SCO^strafe")); else LOG_HELP(strcat("^3", "strafe", " ^7", _("Strafe efficiency (CTS)")));
case "suicides": if (!mode) return CTX(_("SCO^suicides")); else LOG_HELP(strcat("^3", "suicides", " ^7", _("Number of suicides")));
case "sum": if (!mode) return CTX(_("SCO^sum")); else LOG_HELP(strcat("^3", "sum", " ^7", _("Number of kills minus deaths")));
case "takes": if (!mode) return CTX(_("SCO^takes")); else LOG_HELP(strcat("^3", "takes", " ^7", _("Number of domination points taken (Domination)")));
" +ctf/pickups +ctf/fckills +ctf/returns +ctf/caps +ons/takes +ons/caps" \
" +lms/lives +lms/rank" \
" +kh/kckills +kh/losses +kh/caps" \
-" ?+rc/laps ?+rc/time +rc,cts/fastest" \
+" ?+rc/laps ?+rc/time ?+cts/strafe ?+cts/startspeed ?+cts/avgspeed ?+cts/topspeed +rc,cts/fastest" \
" +as/objectives +nb/faults +nb/goals" \
" +ka/pickups +ka/bckills +ka/bctime +ft/revivals" \
" +dom/ticks +dom/takes" \
case SP_DMG: case SP_DMGTAKEN:
return sprintf("%.1f k", pl.(scores(field)) / 1000);
+ case SP_CTS_STRAFE:
+ {
+ float strafe_efficiency = pl.(scores(field)) / 1000;
+ if(strafe_efficiency < -1) return "";
+ sbt_field_rgb = '1 1 1' - (strafe_efficiency > 0 ? '1 0 1' : '0 1 1') * fabs(strafe_efficiency);
+ return sprintf("%.1f%%", strafe_efficiency * 100);
+ }
+
+ case SP_CTS_STARTSPEED:
+ case SP_CTS_AVGSPEED:
+ case SP_CTS_TOPSPEED:
+ {
+ float speed = pl.(scores(field)) * GetSpeedUnitFactor(autocvar_hud_panel_physics_speed_unit);
+ if(speed < 0) return "";
+ return sprintf("%d%s", speed, GetSpeedUnit(autocvar_hud_panel_physics_speed_unit));
+ }
+
default: case SP_SCORE:
tmp = pl.(scores(field));
f = scores_flags(field);
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)
const int SPECIES_RESERVED = 15;
#ifdef GAMEQC
-const int RANKINGS_CNT = 99;
+const int RANKINGS_CNT = 256;
///////////////////////////
// keys pressed
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)
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);
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)
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);
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);
}
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),
#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>
#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>
--- /dev/null
+#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
--- /dev/null
+#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)
if(vdist(player.velocity, >, e.speed))
player.velocity = normalize(player.velocity) * max(0, e.speed);
- if(STAT(TELEPORT_MAXSPEED, player))
- if(vdist(player.velocity, >, STAT(TELEPORT_MAXSPEED, player)))
- player.velocity = normalize(player.velocity) * max(0, STAT(TELEPORT_MAXSPEED, player));
+ if(!(teleporter.classname == "trigger_teleport" && teleporter.spawnflags & TELEPORT_KEEP_SPEED) &&
+ !(teleporter.classname == "target_teleporter" && teleporter.spawnflags & TELEPORTER_KEEP_SPEED))
+ if(STAT(TELEPORT_MAXSPEED, player))
+ if(vdist(player.velocity, >, STAT(TELEPORT_MAXSPEED, player)))
+ player.velocity = normalize(player.velocity) * max(0, STAT(TELEPORT_MAXSPEED, player));
locout = e.origin + '0 0 1' * (1 - player.mins.z - 24);
#endif
REGISTER_NET_LINKED(ENT_CLIENT_TRIGGER_PUSH)
+REGISTER_NET_LINKED(ENT_CLIENT_TRIGGER_PUSH_VELOCITY)
REGISTER_NET_LINKED(ENT_CLIENT_TARGET_PUSH)
/*
return sdir * vs + '0 0 1' * vz;
}
-bool jumppad_push(entity this, entity targ)
+vector trigger_push_velocity_calculatevelocity(entity this, vector org, entity tgt, float speed, float count, entity pushed_entity, bool already_pushed)
+{
+ bool is_playerdir_xy = boolean(this.spawnflags & PUSH_VELOCITY_PLAYERDIR_XY);
+ bool is_add_xy = boolean(this.spawnflags & PUSH_VELOCITY_ADD_XY);
+ bool is_playerdir_z = boolean(this.spawnflags & PUSH_VELOCITY_PLAYERDIR_Z);
+ bool is_add_z = boolean(this.spawnflags & PUSH_VELOCITY_ADD_Z);
+ bool is_bidirectional_xy = boolean(this.spawnflags & PUSH_VELOCITY_BIDIRECTIONAL_XY);
+ bool is_bidirectional_z = boolean(this.spawnflags & PUSH_VELOCITY_BIDIRECTIONAL_Z);
+ bool is_clamp_negative_adds = boolean(this.spawnflags & PUSH_VELOCITY_CLAMP_NEGATIVE_ADDS);
+
+ vector sdir = normalize(vec2(pushed_entity.velocity));
+ float zdir = pushed_entity.velocity.z;
+ if(zdir != 0) zdir = copysign(1, zdir);
+
+ vector vs_tgt = '0 0 0';
+ float vz_tgt = 0;
+ if (!is_playerdir_xy || !is_playerdir_z)
+ {
+ vector vel_tgt = trigger_push_calculatevelocity(org, tgt, 0, pushed_entity);
+ vs_tgt = vec2(vel_tgt);
+ vz_tgt = vel_tgt.z;
+
+ // bidirectional jump pads do not play nicely with xonotic's jump pad targets
+ if (is_bidirectional_xy)
+ {
+ if (normalize(vs_tgt) * sdir < 0)
+ {
+ vs_tgt *= -1;
+ }
+ }
+
+ if (is_bidirectional_z)
+ {
+ if (signbit(vz_tgt) != signbit(zdir))
+ {
+ vz_tgt *= -1;
+ }
+ }
+ }
+
+ vector vs;
+ if (is_playerdir_xy)
+ {
+ vs = sdir * speed;
+ }
+ else
+ {
+ vs = vs_tgt;
+ }
+
+ float vz;
+ if (is_playerdir_z)
+ {
+ vz = zdir * count;
+ }
+ else
+ {
+ vz = vz_tgt;
+ }
+
+ if (is_add_xy)
+ {
+ vector vs_add = vec2(pushed_entity.velocity);
+ if (already_pushed)
+ {
+ vs = vs_add;
+ }
+ else
+ {
+ vs += vs_add;
+
+ if (is_clamp_negative_adds)
+ {
+ if ((normalize(vs) * sdir) < 0)
+ {
+ vs = '0 0 0';
+ }
+ }
+ }
+ }
+
+ if (is_add_z)
+ {
+ float vz_add = pushed_entity.velocity.z;
+ if (already_pushed)
+ {
+ vz = vz_add;
+ }
+ else
+ {
+ vz += vz_add;
+
+ if (is_clamp_negative_adds)
+ {
+ if (signbit(vz) != signbit(zdir))
+ {
+ vz = 0;
+ }
+ }
+ }
+ }
+
+ return vs + '0 0 1' * vz;
+}
+
+bool jumppad_push(entity this, entity targ, bool is_velocity_pad)
{
if (!isPushable(targ))
return false;
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
+ }
+ }
if(this.enemy)
{
- targ.velocity = trigger_push_calculatevelocity(org, this.enemy, this.height, targ);
+ if(!is_velocity_pad)
+ {
+ targ.velocity = trigger_push_calculatevelocity(org, this.enemy, this.height, targ);
+ }
+ else
+ {
+ targ.velocity = trigger_push_velocity_calculatevelocity(this, org, this.enemy, this.speed, this.count, targ, already_pushed);
+ }
}
else if(this.target && this.target != "")
{
else
RandomSelection_AddEnt(e, 1, 1);
}
- targ.velocity = trigger_push_calculatevelocity(org, RandomSelection_chosen_ent, this.height, targ);
+ if(!is_velocity_pad)
+ {
+ targ.velocity = trigger_push_calculatevelocity(org, RandomSelection_chosen_ent, this.height, targ);
+ }
+ else
+ {
+ targ.velocity = trigger_push_velocity_calculatevelocity(this, org, RandomSelection_chosen_ent, this.speed, this.count, targ, already_pushed);
+ }
}
else
{
- targ.velocity = this.movedir;
+ if(!is_velocity_pad)
+ {
+ targ.velocity = this.movedir;
+ }
+ else
+ {
+#ifdef SVQC
+ objerror (this, "Jumppad with no target");
+#endif
+ return false;
+ }
}
- UNSET_ONGROUND(targ);
+ if(!is_velocity_pad) UNSET_ONGROUND(targ);
#ifdef CSQC
if (targ.flags & FL_PROJECTILE)
// prevent sound spam when a player hits the jumppad more than once
// or when a dead player gets stuck in the jumppad for some reason
- if(this.pushltime < time && !(IS_DEAD(targ) && targ.velocity == '0 0 0'))
+ if(!already_pushed && this.pushltime < time && !(IS_DEAD(targ) && targ.velocity == '0 0 0'))
{
// flash when activated
Send_Effect(EFFECT_JUMPPAD, targ.origin, targ.velocity, 1);
EXACTTRIGGER_TOUCH(this, toucher);
- noref bool success = jumppad_push(this, toucher);
+ noref bool success = jumppad_push(this, toucher, false);
#ifdef SVQC
if (success && (this.spawnflags & PUSH_ONCE))
#endif
}
+void trigger_push_velocity_touch(entity this, entity toucher)
+{
+ if (this.active == ACTIVE_NOT)
+ return;
+
+ if(this.team && DIFF_TEAM(this, toucher))
+ return;
+
+ EXACTTRIGGER_TOUCH(this, toucher);
+
+ jumppad_push(this, toucher, true);
+}
+
#ifdef SVQC
void trigger_push_link(entity this);
void trigger_push_updatelink(entity this);
return true;
}
+float trigger_push_velocity_send(entity this, entity to, float sf)
+{
+ WriteHeader(MSG_ENTITY, ENT_CLIENT_TRIGGER_PUSH_VELOCITY);
+
+ WriteByte(MSG_ENTITY, this.team);
+ WriteInt24_t(MSG_ENTITY, this.spawnflags);
+ WriteByte(MSG_ENTITY, this.active);
+ WriteCoord(MSG_ENTITY, this.speed);
+ WriteCoord(MSG_ENTITY, this.count);
+
+ trigger_common_write(this, true);
+
+ return true;
+}
+
void trigger_push_updatelink(entity this)
{
this.SendFlags |= SF_TRIGGER_INIT;
trigger_link(this, trigger_push_send);
}
+void trigger_push_velocity_link(entity this)
+{
+ trigger_link(this, trigger_push_velocity_send);
+}
+
/*
* ENTITY PARAMETERS:
*
InitializeEntity(this, trigger_push_findtarget, INITPRIO_FINDTARGET);
}
+/*
+ * ENTITY PARAMETERS:
+ *
+ * target: this points to the target_position to which the player will jump.
+ * speed: XY speed for player-directional velocity pads - either sets or adds to the player's horizontal velocity.
+ * count: Z speed for player-directional velocity pads - either sets or adds to the player's vertical velocity.
+ */
+spawnfunc(trigger_push_velocity)
+{
+ trigger_init(this);
+
+ this.active = ACTIVE_ACTIVE;
+ this.use = trigger_push_use;
+ settouch(this, trigger_push_velocity_touch);
+
+ // normal push setup
+ if (!this.noise)
+ this.noise = "misc/jumppad.wav";
+ precache_sound (this.noise);
+
+ trigger_push_velocity_link(this); // link it now
+}
+
bool target_push_send(entity this, entity to, float sf)
{
if(trigger.classname == "trigger_push" || trigger == this)
return; // WTF, why is this a thing
- jumppad_push(this, actor);
+ jumppad_push(this, actor, false);
}
void target_push_link(entity this)
return true;
}
+NET_HANDLE(ENT_CLIENT_TRIGGER_PUSH_VELOCITY, bool isnew)
+{
+ int mytm = ReadByte(); if(mytm) { this.team = mytm - 1; }
+ this.spawnflags = ReadInt24_t();
+ this.active = ReadByte();
+ this.speed = ReadCoord();
+ this.count = ReadCoord();
+
+ trigger_common_read(this, true);
+
+ this.entremove = trigger_remove_generic;
+ this.solid = SOLID_TRIGGER;
+ settouch(this, trigger_push_velocity_touch);
+ this.move_time = time;
+
+ return true;
+}
+
void target_push_remove(entity this)
{
// strfree(this.classname);
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)
IntrusiveList g_jumppads;
STATIC_INIT(g_jumppads) { g_jumppads = IL_NEW(); }
.bool istypefrag;
.float height;
+.entity last_pushed;
+
const int NUM_JUMPPADSUSED = 3;
.float jumppadcount;
.entity jumppadsused[NUM_JUMPPADSUSED];
void trigger_push_findtarget(entity this);
/*
- * ENTITY PARAMETERS:
+ * ENTITY PARAMETERS trigger_push:
*
* target: target of jump
* height: the absolute value is the height of the highest point of the jump
* values to target a point on the ceiling.
* movedir: if target is not set, this * speed * 10 is the velocity to be reached.
*/
+
+/*
+ * ENTITY PARAMETERS trigger_push_velocity:
+ *
+ * target: this points to the target_position to which the player will jump.
+ * speed: XY speed for player-directional velocity pads - either sets or adds to the player's horizontal velocity.
+ * count: Z speed for player-directional velocity pads - either sets or adds to the player's vertical velocity.
+ */
#ifdef SVQC
spawnfunc(trigger_push);
+spawnfunc(trigger_push_velocity);
spawnfunc(target_push);
spawnfunc(info_notnull);
if(IS_TURRET(player))
return false;
+
+ if(this.classname == "trigger_teleport" && this.spawnflags & TELEPORT_SPECTATOR && !IS_SPEC(player))
+ return false;
#elif defined(CSQC)
if(!IS_PLAYER(player))
return false;
#pragma once
+
+
+// q3df compat spawnflags, they may also be useful in xonotic
+
+// trigger_teleport
+#define TELEPORT_SPECTATOR BIT(0) // exists in q3 but is only documented in q3df
+#define TELEPORT_KEEP_SPEED BIT(1)
+
+// target_teleporter
+#define TELEPORTER_KEEP_SPEED BIT(0)
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);
#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
string to_execute_next_frame;
void execute_next_frame()
{
+#ifdef SVQC
+ IL_EACH(g_moveables, it.last_pushed,
+ {
+ if(WarpZoneLib_ExactTrigger_Touch(it.last_pushed, it))
+ {
+ it.last_pushed = NULL;
+ }
+ });
+#elif defined(CSQC)
+ if(csqcplayer.last_pushed && WarpZoneLib_ExactTrigger_Touch(csqcplayer.last_pushed, csqcplayer))
+ {
+ csqcplayer.last_pushed = NULL;
+ }
+#endif
+
if(to_execute_next_frame)
{
localcmd("\n", to_execute_next_frame, "\n");
thiswep.wr_reload(thiswep, actor, weaponentity);
}
- if(fire & 1)
+ // attack swapping is useful for emulating BFG behavior in XDF
+ int primary_fire = autocvar_g_balance_crylink_swap_attacks ? fire & 2 : fire & 1;
+ int secondary_fire = autocvar_g_balance_crylink_swap_attacks ? fire & 1 : fire & 2;
+
+ if(primary_fire)
{
if(actor.(weaponentity).crylink_waitrelease != 1)
if(weapon_prepareattack(thiswep, actor, weaponentity, false, WEP_CVAR_PRI(crylink, refire)))
}
}
- if((fire & 2) && autocvar_g_balance_crylink_secondary)
+ if((secondary_fire) && autocvar_g_balance_crylink_secondary)
{
if(actor.(weaponentity).crylink_waitrelease != 2)
if(weapon_prepareattack(thiswep, actor, weaponentity, true, WEP_CVAR_SEC(crylink, refire)))
}
}
- if((actor.(weaponentity).crylink_waitrelease == 1 && !(fire & 1)) || (actor.(weaponentity).crylink_waitrelease == 2 && !(fire & 2)))
+ if((actor.(weaponentity).crylink_waitrelease == 1 && !(primary_fire)) || (actor.(weaponentity).crylink_waitrelease == 2 && !(secondary_fire)))
{
if(!actor.(weaponentity).crylink_lastgroup || time > actor.(weaponentity).crylink_lastgroup.teleport_time)
{
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) \
#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>
#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>
#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)
}
}
+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)
}
}
+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)
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); }
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)
{
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);
}
#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));
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;
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;
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;
{
// 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;
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
// 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);
}
{
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);
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)
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);
#pragma once
bool autocvar_g_allow_checkpoints;
+string autocvar_sv_checkpoint_house_url;
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;
.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);
--- /dev/null
+#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;
+}
--- /dev/null
+#pragma once
+
+.float strafe_efficiency_sum;
+.float strafe_efficiency_time;
+.float strafe_efficiency_best;
+
+float calculate_strafe_efficiency(entity, vector, float);
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)"
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"