mirror of
https://github.com/id-Software/quake2-rerelease-dll.git
synced 2025-02-25 12:40:50 +00:00
3832 lines
97 KiB
C++
3832 lines
97 KiB
C++
// Copyright (c) ZeniMax Media Inc.
|
|
// Licensed under the GNU General Public License 2.0.
|
|
#include "../g_local.h"
|
|
#include "../m_player.h"
|
|
|
|
#include <assert.h>
|
|
|
|
enum match_t
|
|
{
|
|
MATCH_NONE,
|
|
MATCH_SETUP,
|
|
MATCH_PREGAME,
|
|
MATCH_GAME,
|
|
MATCH_POST
|
|
};
|
|
|
|
enum elect_t
|
|
{
|
|
ELECT_NONE,
|
|
ELECT_MATCH,
|
|
ELECT_ADMIN,
|
|
ELECT_MAP
|
|
};
|
|
|
|
struct ctfgame_t
|
|
{
|
|
int team1, team2;
|
|
int total1, total2; // these are only set when going into intermission except in teamplay
|
|
gtime_t last_flag_capture;
|
|
int last_capture_team;
|
|
|
|
match_t match; // match state
|
|
gtime_t matchtime; // time for match start/end (depends on state)
|
|
int lasttime; // last time update, explicitly truncated to seconds
|
|
bool countdown; // has audio countdown started?
|
|
|
|
elect_t election; // election type
|
|
edict_t *etarget; // for admin election, who's being elected
|
|
char elevel[32]; // for map election, target level
|
|
int evotes; // votes so far
|
|
int needvotes; // votes needed
|
|
gtime_t electtime; // remaining time until election times out
|
|
char emsg[256]; // election name
|
|
int warnactive; // true if stat string 30 is active
|
|
|
|
ghost_t ghosts[MAX_CLIENTS]; // ghost codes
|
|
};
|
|
|
|
ctfgame_t ctfgame;
|
|
|
|
cvar_t *ctf;
|
|
cvar_t *teamplay;
|
|
cvar_t *g_teamplay_force_join;
|
|
|
|
// [Paril-KEX]
|
|
bool G_TeamplayEnabled()
|
|
{
|
|
return ctf->integer || teamplay->integer;
|
|
}
|
|
|
|
// [Paril-KEX]
|
|
void G_AdjustTeamScore(ctfteam_t team, int32_t offset)
|
|
{
|
|
if (team == CTF_TEAM1)
|
|
ctfgame.total1 += offset;
|
|
else if (team == CTF_TEAM2)
|
|
ctfgame.total2 += offset;
|
|
}
|
|
|
|
cvar_t *competition;
|
|
cvar_t *matchlock;
|
|
cvar_t *electpercentage;
|
|
cvar_t *matchtime;
|
|
cvar_t *matchsetuptime;
|
|
cvar_t *matchstarttime;
|
|
cvar_t *admin_password;
|
|
cvar_t *allow_admin;
|
|
cvar_t *warp_list;
|
|
cvar_t *warn_unbalanced;
|
|
|
|
// Index for various CTF pics, this saves us from calling gi.imageindex
|
|
// all the time and saves a few CPU cycles since we don't have to do
|
|
// a bunch of string compares all the time.
|
|
// These are set in CTFPrecache() called from worldspawn
|
|
int imageindex_i_ctf1;
|
|
int imageindex_i_ctf2;
|
|
int imageindex_i_ctf1d;
|
|
int imageindex_i_ctf2d;
|
|
int imageindex_i_ctf1t;
|
|
int imageindex_i_ctf2t;
|
|
int imageindex_i_ctfj;
|
|
int imageindex_sbfctf1;
|
|
int imageindex_sbfctf2;
|
|
int imageindex_ctfsb1;
|
|
int imageindex_ctfsb2;
|
|
int modelindex_flag1, modelindex_flag2; // [Paril-KEX]
|
|
|
|
constexpr item_id_t tech_ids[] = { IT_TECH_RESISTANCE, IT_TECH_STRENGTH, IT_TECH_HASTE, IT_TECH_REGENERATION };
|
|
|
|
/*--------------------------------------------------------------------------*/
|
|
|
|
#ifndef KEX_Q2_GAME
|
|
/*
|
|
=================
|
|
findradius
|
|
|
|
Returns entities that have origins within a spherical area
|
|
|
|
findradius (origin, radius)
|
|
=================
|
|
*/
|
|
static edict_t *loc_findradius(edict_t *from, const vec3_t &org, float rad)
|
|
{
|
|
vec3_t eorg;
|
|
int j;
|
|
|
|
if (!from)
|
|
from = g_edicts;
|
|
else
|
|
from++;
|
|
for (; from < &g_edicts[globals.num_edicts]; from++)
|
|
{
|
|
if (!from->inuse)
|
|
continue;
|
|
for (j = 0; j < 3; j++)
|
|
eorg[j] = org[j] - (from->s.origin[j] + (from->mins[j] + from->maxs[j]) * 0.5f);
|
|
if (eorg.length() > rad)
|
|
continue;
|
|
return from;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
#endif
|
|
|
|
static void loc_buildboxpoints(vec3_t (&p)[8], const vec3_t &org, const vec3_t &mins, const vec3_t &maxs)
|
|
{
|
|
p[0] = org + mins;
|
|
p[1] = p[0];
|
|
p[1][0] -= mins[0];
|
|
p[2] = p[0];
|
|
p[2][1] -= mins[1];
|
|
p[3] = p[0];
|
|
p[3][0] -= mins[0];
|
|
p[3][1] -= mins[1];
|
|
p[4] = org + maxs;
|
|
p[5] = p[4];
|
|
p[5][0] -= maxs[0];
|
|
p[6] = p[0];
|
|
p[6][1] -= maxs[1];
|
|
p[7] = p[0];
|
|
p[7][0] -= maxs[0];
|
|
p[7][1] -= maxs[1];
|
|
}
|
|
|
|
static bool loc_CanSee(edict_t *targ, edict_t *inflictor)
|
|
{
|
|
trace_t trace;
|
|
vec3_t targpoints[8];
|
|
int i;
|
|
vec3_t viewpoint;
|
|
|
|
// bmodels need special checking because their origin is 0,0,0
|
|
if (targ->movetype == MOVETYPE_PUSH)
|
|
return false; // bmodels not supported
|
|
|
|
loc_buildboxpoints(targpoints, targ->s.origin, targ->mins, targ->maxs);
|
|
|
|
viewpoint = inflictor->s.origin;
|
|
viewpoint[2] += inflictor->viewheight;
|
|
|
|
for (i = 0; i < 8; i++)
|
|
{
|
|
trace = gi.traceline(viewpoint, targpoints[i], inflictor, MASK_SOLID);
|
|
if (trace.fraction == 1.0f)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
#
|
|
|
|
/*--------------------------------------------------------------------------*/
|
|
|
|
void CTFSpawn()
|
|
{
|
|
memset(&ctfgame, 0, sizeof(ctfgame));
|
|
CTFSetupTechSpawn();
|
|
|
|
if (competition->integer > 1)
|
|
{
|
|
ctfgame.match = MATCH_SETUP;
|
|
ctfgame.matchtime = level.time + gtime_t::from_min(matchsetuptime->value);
|
|
}
|
|
}
|
|
|
|
void CTFInit()
|
|
{
|
|
ctf = gi.cvar("ctf", "0", CVAR_SERVERINFO | CVAR_LATCH);
|
|
competition = gi.cvar("competition", "0", CVAR_SERVERINFO);
|
|
matchlock = gi.cvar("matchlock", "1", CVAR_SERVERINFO);
|
|
electpercentage = gi.cvar("electpercentage", "66", CVAR_NOFLAGS);
|
|
matchtime = gi.cvar("matchtime", "20", CVAR_SERVERINFO);
|
|
matchsetuptime = gi.cvar("matchsetuptime", "10", CVAR_NOFLAGS);
|
|
matchstarttime = gi.cvar("matchstarttime", "20", CVAR_NOFLAGS);
|
|
admin_password = gi.cvar("admin_password", "", CVAR_NOFLAGS);
|
|
allow_admin = gi.cvar("allow_admin", "1", CVAR_NOFLAGS);
|
|
warp_list = gi.cvar("warp_list", "q2ctf1 q2ctf2 q2ctf3 q2ctf4 q2ctf5", CVAR_NOFLAGS);
|
|
warn_unbalanced = gi.cvar("warn_unbalanced", "0", CVAR_NOFLAGS);
|
|
}
|
|
|
|
/*
|
|
* Precache CTF items
|
|
*/
|
|
|
|
void CTFPrecache()
|
|
{
|
|
imageindex_i_ctf1 = gi.imageindex("i_ctf1");
|
|
imageindex_i_ctf2 = gi.imageindex("i_ctf2");
|
|
imageindex_i_ctf1d = gi.imageindex("i_ctf1d");
|
|
imageindex_i_ctf2d = gi.imageindex("i_ctf2d");
|
|
imageindex_i_ctf1t = gi.imageindex("i_ctf1t");
|
|
imageindex_i_ctf2t = gi.imageindex("i_ctf2t");
|
|
imageindex_i_ctfj = gi.imageindex("i_ctfj");
|
|
imageindex_sbfctf1 = gi.imageindex("sbfctf1");
|
|
imageindex_sbfctf2 = gi.imageindex("sbfctf2");
|
|
imageindex_ctfsb1 = gi.imageindex("tag4");
|
|
imageindex_ctfsb2 = gi.imageindex("tag5");
|
|
modelindex_flag1 = gi.modelindex("players/male/flag1.md2");
|
|
modelindex_flag2 = gi.modelindex("players/male/flag2.md2");
|
|
|
|
PrecacheItem(GetItemByIndex(IT_WEAPON_GRAPPLE));
|
|
}
|
|
|
|
/*--------------------------------------------------------------------------*/
|
|
|
|
const char *CTFTeamName(int team)
|
|
{
|
|
switch (team)
|
|
{
|
|
case CTF_TEAM1:
|
|
return "RED";
|
|
case CTF_TEAM2:
|
|
return "BLUE";
|
|
case CTF_NOTEAM:
|
|
return "SPECTATOR";
|
|
}
|
|
return "UNKNOWN"; // Hanzo pointed out this was spelled wrong as "UKNOWN"
|
|
}
|
|
|
|
const char *CTFOtherTeamName(int team)
|
|
{
|
|
switch (team)
|
|
{
|
|
case CTF_TEAM1:
|
|
return "BLUE";
|
|
case CTF_TEAM2:
|
|
return "RED";
|
|
}
|
|
return "UNKNOWN"; // Hanzo pointed out this was spelled wrong as "UKNOWN"
|
|
}
|
|
|
|
int CTFOtherTeam(int team)
|
|
{
|
|
switch (team)
|
|
{
|
|
case CTF_TEAM1:
|
|
return CTF_TEAM2;
|
|
case CTF_TEAM2:
|
|
return CTF_TEAM1;
|
|
}
|
|
return -1; // invalid value
|
|
}
|
|
|
|
/*--------------------------------------------------------------------------*/
|
|
|
|
float PlayersRangeFromSpot(edict_t *spot);
|
|
bool SpawnPointClear(edict_t *spot);
|
|
|
|
void CTFAssignSkin(edict_t *ent, const char *s)
|
|
{
|
|
int playernum = ent - g_edicts - 1;
|
|
std::string_view t(s);
|
|
|
|
if (size_t i = t.find_first_of('/'); i != std::string_view::npos)
|
|
t = t.substr(0, i + 1);
|
|
else
|
|
t = "male/";
|
|
|
|
switch (ent->client->resp.ctf_team)
|
|
{
|
|
case CTF_TEAM1:
|
|
t = G_Fmt("{}\\{}{}\\default", ent->client->pers.netname, t, CTF_TEAM1_SKIN);
|
|
break;
|
|
case CTF_TEAM2:
|
|
t = G_Fmt("{}\\{}{}\\default", ent->client->pers.netname, t, CTF_TEAM2_SKIN);
|
|
break;
|
|
default:
|
|
t = G_Fmt("{}\\{}\\default", ent->client->pers.netname, s);
|
|
break;
|
|
}
|
|
|
|
gi.configstring(CS_PLAYERSKINS + playernum, t.data());
|
|
|
|
// gi.LocClient_Print(ent, PRINT_HIGH, "$g_assigned_team", ent->client->pers.netname);
|
|
}
|
|
|
|
void CTFAssignTeam(gclient_t *who)
|
|
{
|
|
edict_t *player;
|
|
uint32_t team1count = 0, team2count = 0;
|
|
|
|
who->resp.ctf_state = 0;
|
|
|
|
if (!g_teamplay_force_join->integer && !(g_edicts[1 + (who - game.clients)].svflags & SVF_BOT))
|
|
{
|
|
who->resp.ctf_team = CTF_NOTEAM;
|
|
return;
|
|
}
|
|
|
|
for (uint32_t i = 1; i <= game.maxclients; i++)
|
|
{
|
|
player = &g_edicts[i];
|
|
|
|
if (!player->inuse || player->client == who)
|
|
continue;
|
|
|
|
switch (player->client->resp.ctf_team)
|
|
{
|
|
case CTF_TEAM1:
|
|
team1count++;
|
|
break;
|
|
case CTF_TEAM2:
|
|
team2count++;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
if (team1count < team2count)
|
|
who->resp.ctf_team = CTF_TEAM1;
|
|
else if (team2count < team1count)
|
|
who->resp.ctf_team = CTF_TEAM2;
|
|
else if (brandom())
|
|
who->resp.ctf_team = CTF_TEAM1;
|
|
else
|
|
who->resp.ctf_team = CTF_TEAM2;
|
|
}
|
|
|
|
/*
|
|
================
|
|
SelectCTFSpawnPoint
|
|
|
|
go to a ctf point, but NOT the two points closest
|
|
to other players
|
|
================
|
|
*/
|
|
edict_t *SelectCTFSpawnPoint(edict_t *ent, bool force_spawn)
|
|
{
|
|
if (ent->client->resp.ctf_state)
|
|
{
|
|
select_spawn_result_t result = SelectDeathmatchSpawnPoint(g_dm_spawn_farthest->integer, force_spawn, false);
|
|
|
|
if (result.any_valid)
|
|
return result.spot;
|
|
}
|
|
|
|
const char *cname;
|
|
|
|
switch (ent->client->resp.ctf_team)
|
|
{
|
|
case CTF_TEAM1:
|
|
cname = "info_player_team1";
|
|
break;
|
|
case CTF_TEAM2:
|
|
cname = "info_player_team2";
|
|
break;
|
|
default:
|
|
{
|
|
select_spawn_result_t result = SelectDeathmatchSpawnPoint(g_dm_spawn_farthest->integer, force_spawn, true);
|
|
|
|
if (result.any_valid)
|
|
return result.spot;
|
|
|
|
gi.Com_Error("can't find suitable spectator spawn point");
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
static std::vector<edict_t *> spawn_points;
|
|
edict_t *spot = nullptr;
|
|
|
|
spawn_points.clear();
|
|
|
|
while ((spot = G_FindByString<&edict_t::classname>(spot, cname)) != nullptr)
|
|
spawn_points.push_back(spot);
|
|
|
|
if (!spawn_points.size())
|
|
{
|
|
select_spawn_result_t result = SelectDeathmatchSpawnPoint(g_dm_spawn_farthest->integer, force_spawn, true);
|
|
|
|
if (!result.any_valid)
|
|
gi.Com_Error("can't find suitable CTF spawn point");
|
|
|
|
return result.spot;
|
|
}
|
|
|
|
std::shuffle(spawn_points.begin(), spawn_points.end(), mt_rand);
|
|
|
|
for (auto &point : spawn_points)
|
|
if (SpawnPointClear(point))
|
|
return point;
|
|
|
|
if (force_spawn)
|
|
return random_element(spawn_points);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*/
|
|
/*
|
|
CTFFragBonuses
|
|
|
|
Calculate the bonuses for flag defense, flag carrier defense, etc.
|
|
Note that bonuses are not cumaltive. You get one, they are in importance
|
|
order.
|
|
*/
|
|
void CTFFragBonuses(edict_t *targ, edict_t *inflictor, edict_t *attacker)
|
|
{
|
|
edict_t *ent;
|
|
item_id_t flag_item, enemy_flag_item;
|
|
int otherteam;
|
|
edict_t *flag, *carrier = nullptr;
|
|
const char *c;
|
|
vec3_t v1, v2;
|
|
|
|
if (targ->client && attacker->client)
|
|
{
|
|
if (attacker->client->resp.ghost)
|
|
if (attacker != targ)
|
|
attacker->client->resp.ghost->kills++;
|
|
if (targ->client->resp.ghost)
|
|
targ->client->resp.ghost->deaths++;
|
|
}
|
|
|
|
// no bonus for fragging yourself
|
|
if (!targ->client || !attacker->client || targ == attacker)
|
|
return;
|
|
|
|
otherteam = CTFOtherTeam(targ->client->resp.ctf_team);
|
|
if (otherteam < 0)
|
|
return; // whoever died isn't on a team
|
|
|
|
// same team, if the flag at base, check to he has the enemy flag
|
|
if (targ->client->resp.ctf_team == CTF_TEAM1)
|
|
{
|
|
flag_item = IT_FLAG1;
|
|
enemy_flag_item = IT_FLAG2;
|
|
}
|
|
else
|
|
{
|
|
flag_item = IT_FLAG2;
|
|
enemy_flag_item = IT_FLAG1;
|
|
}
|
|
|
|
// did the attacker frag the flag carrier?
|
|
if (targ->client->pers.inventory[enemy_flag_item])
|
|
{
|
|
attacker->client->resp.ctf_lastfraggedcarrier = level.time;
|
|
attacker->client->resp.score += CTF_FRAG_CARRIER_BONUS;
|
|
gi.LocClient_Print(attacker, PRINT_MEDIUM, "$g_bonus_enemy_carrier",
|
|
CTF_FRAG_CARRIER_BONUS);
|
|
|
|
// the target had the flag, clear the hurt carrier
|
|
// field on the other team
|
|
for (uint32_t i = 1; i <= game.maxclients; i++)
|
|
{
|
|
ent = g_edicts + i;
|
|
if (ent->inuse && ent->client->resp.ctf_team == otherteam)
|
|
ent->client->resp.ctf_lasthurtcarrier = 0_ms;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (targ->client->resp.ctf_lasthurtcarrier &&
|
|
level.time - targ->client->resp.ctf_lasthurtcarrier < CTF_CARRIER_DANGER_PROTECT_TIMEOUT &&
|
|
!attacker->client->pers.inventory[flag_item])
|
|
{
|
|
// attacker is on the same team as the flag carrier and
|
|
// fragged a guy who hurt our flag carrier
|
|
attacker->client->resp.score += CTF_CARRIER_DANGER_PROTECT_BONUS;
|
|
gi.LocBroadcast_Print(PRINT_MEDIUM, "$g_bonus_flag_defense",
|
|
attacker->client->pers.netname,
|
|
CTFTeamName(attacker->client->resp.ctf_team));
|
|
if (attacker->client->resp.ghost)
|
|
attacker->client->resp.ghost->carrierdef++;
|
|
return;
|
|
}
|
|
|
|
// flag and flag carrier area defense bonuses
|
|
|
|
// we have to find the flag and carrier entities
|
|
|
|
// find the flag
|
|
switch (attacker->client->resp.ctf_team)
|
|
{
|
|
case CTF_TEAM1:
|
|
c = "item_flag_team1";
|
|
break;
|
|
case CTF_TEAM2:
|
|
c = "item_flag_team2";
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
flag = nullptr;
|
|
while ((flag = G_FindByString<&edict_t::classname>(flag, c)) != nullptr)
|
|
{
|
|
if (!(flag->spawnflags & SPAWNFLAG_ITEM_DROPPED))
|
|
break;
|
|
}
|
|
|
|
if (!flag)
|
|
return; // can't find attacker's flag
|
|
|
|
// find attacker's team's flag carrier
|
|
for (uint32_t i = 1; i <= game.maxclients; i++)
|
|
{
|
|
carrier = g_edicts + i;
|
|
if (carrier->inuse &&
|
|
carrier->client->pers.inventory[flag_item])
|
|
break;
|
|
carrier = nullptr;
|
|
}
|
|
|
|
// ok we have the attackers flag and a pointer to the carrier
|
|
|
|
// check to see if we are defending the base's flag
|
|
v1 = targ->s.origin - flag->s.origin;
|
|
v2 = attacker->s.origin - flag->s.origin;
|
|
|
|
if ((v1.length() < CTF_TARGET_PROTECT_RADIUS ||
|
|
v2.length() < CTF_TARGET_PROTECT_RADIUS ||
|
|
loc_CanSee(flag, targ) || loc_CanSee(flag, attacker)) &&
|
|
attacker->client->resp.ctf_team != targ->client->resp.ctf_team)
|
|
{
|
|
// we defended the base flag
|
|
attacker->client->resp.score += CTF_FLAG_DEFENSE_BONUS;
|
|
if (flag->solid == SOLID_NOT)
|
|
gi.LocBroadcast_Print(PRINT_MEDIUM, "$g_bonus_defend_base",
|
|
attacker->client->pers.netname,
|
|
CTFTeamName(attacker->client->resp.ctf_team));
|
|
else
|
|
gi.LocBroadcast_Print(PRINT_MEDIUM, "$g_bonus_defend_flag",
|
|
attacker->client->pers.netname,
|
|
CTFTeamName(attacker->client->resp.ctf_team));
|
|
if (attacker->client->resp.ghost)
|
|
attacker->client->resp.ghost->basedef++;
|
|
return;
|
|
}
|
|
|
|
if (carrier && carrier != attacker)
|
|
{
|
|
v1 = targ->s.origin - carrier->s.origin;
|
|
v2 = attacker->s.origin - carrier->s.origin;
|
|
|
|
if (v1.length() < CTF_ATTACKER_PROTECT_RADIUS ||
|
|
v2.length() < CTF_ATTACKER_PROTECT_RADIUS ||
|
|
loc_CanSee(carrier, targ) || loc_CanSee(carrier, attacker))
|
|
{
|
|
attacker->client->resp.score += CTF_CARRIER_PROTECT_BONUS;
|
|
gi.LocBroadcast_Print(PRINT_MEDIUM, "$g_bonus_defend_carrier",
|
|
attacker->client->pers.netname,
|
|
CTFTeamName(attacker->client->resp.ctf_team));
|
|
if (attacker->client->resp.ghost)
|
|
attacker->client->resp.ghost->carrierdef++;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CTFCheckHurtCarrier(edict_t *targ, edict_t *attacker)
|
|
{
|
|
item_id_t flag_item;
|
|
|
|
if (!targ->client || !attacker->client)
|
|
return;
|
|
|
|
if (targ->client->resp.ctf_team == CTF_TEAM1)
|
|
flag_item = IT_FLAG2;
|
|
else
|
|
flag_item = IT_FLAG1;
|
|
|
|
if (targ->client->pers.inventory[flag_item] &&
|
|
targ->client->resp.ctf_team != attacker->client->resp.ctf_team)
|
|
attacker->client->resp.ctf_lasthurtcarrier = level.time;
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*/
|
|
|
|
void CTFResetFlag(int ctf_team)
|
|
{
|
|
const char *c;
|
|
edict_t *ent;
|
|
|
|
switch (ctf_team)
|
|
{
|
|
case CTF_TEAM1:
|
|
c = "item_flag_team1";
|
|
break;
|
|
case CTF_TEAM2:
|
|
c = "item_flag_team2";
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
ent = nullptr;
|
|
while ((ent = G_FindByString<&edict_t::classname>(ent, c)) != nullptr)
|
|
{
|
|
if (ent->spawnflags.has(SPAWNFLAG_ITEM_DROPPED))
|
|
G_FreeEdict(ent);
|
|
else
|
|
{
|
|
ent->svflags &= ~SVF_NOCLIENT;
|
|
ent->solid = SOLID_TRIGGER;
|
|
gi.linkentity(ent);
|
|
ent->s.event = EV_ITEM_RESPAWN;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CTFResetFlags()
|
|
{
|
|
CTFResetFlag(CTF_TEAM1);
|
|
CTFResetFlag(CTF_TEAM2);
|
|
}
|
|
|
|
bool CTFPickup_Flag(edict_t *ent, edict_t *other)
|
|
{
|
|
int ctf_team;
|
|
edict_t *player;
|
|
item_id_t flag_item, enemy_flag_item;
|
|
|
|
// figure out what team this flag is
|
|
if (ent->item->id == IT_FLAG1)
|
|
ctf_team = CTF_TEAM1;
|
|
else if (ent->item->id == IT_FLAG2)
|
|
ctf_team = CTF_TEAM2;
|
|
else
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Don't know what team the flag is on.\n");
|
|
return false;
|
|
}
|
|
|
|
// same team, if the flag at base, check to he has the enemy flag
|
|
if (ctf_team == CTF_TEAM1)
|
|
{
|
|
flag_item = IT_FLAG1;
|
|
enemy_flag_item = IT_FLAG2;
|
|
}
|
|
else
|
|
{
|
|
flag_item = IT_FLAG2;
|
|
enemy_flag_item = IT_FLAG1;
|
|
}
|
|
|
|
if (ctf_team == other->client->resp.ctf_team)
|
|
{
|
|
|
|
if (!(ent->spawnflags & SPAWNFLAG_ITEM_DROPPED))
|
|
{
|
|
// the flag is at home base. if the player has the enemy
|
|
// flag, he's just won!
|
|
|
|
if (other->client->pers.inventory[enemy_flag_item])
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_flag_captured",
|
|
other->client->pers.netname, CTFOtherTeamName(ctf_team));
|
|
other->client->pers.inventory[enemy_flag_item] = 0;
|
|
|
|
ctfgame.last_flag_capture = level.time;
|
|
ctfgame.last_capture_team = ctf_team;
|
|
if (ctf_team == CTF_TEAM1)
|
|
ctfgame.team1++;
|
|
else
|
|
ctfgame.team2++;
|
|
|
|
gi.sound(ent, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0);
|
|
|
|
// other gets another 10 frag bonus
|
|
other->client->resp.score += CTF_CAPTURE_BONUS;
|
|
if (other->client->resp.ghost)
|
|
other->client->resp.ghost->caps++;
|
|
|
|
// Ok, let's do the player loop, hand out the bonuses
|
|
for (uint32_t i = 1; i <= game.maxclients; i++)
|
|
{
|
|
player = &g_edicts[i];
|
|
if (!player->inuse)
|
|
continue;
|
|
|
|
if (player->client->resp.ctf_team != other->client->resp.ctf_team)
|
|
player->client->resp.ctf_lasthurtcarrier = -5_sec;
|
|
else if (player->client->resp.ctf_team == other->client->resp.ctf_team)
|
|
{
|
|
if (player != other)
|
|
player->client->resp.score += CTF_TEAM_BONUS;
|
|
// award extra points for capture assists
|
|
if (player->client->resp.ctf_lastreturnedflag && player->client->resp.ctf_lastreturnedflag + CTF_RETURN_FLAG_ASSIST_TIMEOUT > level.time)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_bonus_assist_return", player->client->pers.netname);
|
|
player->client->resp.score += CTF_RETURN_FLAG_ASSIST_BONUS;
|
|
}
|
|
if (player->client->resp.ctf_lastfraggedcarrier && player->client->resp.ctf_lastfraggedcarrier + CTF_FRAG_CARRIER_ASSIST_TIMEOUT > level.time)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_bonus_assist_frag_carrier", player->client->pers.netname);
|
|
player->client->resp.score += CTF_FRAG_CARRIER_ASSIST_BONUS;
|
|
}
|
|
}
|
|
}
|
|
|
|
CTFResetFlags();
|
|
return false;
|
|
}
|
|
return false; // its at home base already
|
|
}
|
|
// hey, its not home. return it by teleporting it back
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_returned_flag",
|
|
other->client->pers.netname, CTFTeamName(ctf_team));
|
|
other->client->resp.score += CTF_RECOVERY_BONUS;
|
|
other->client->resp.ctf_lastreturnedflag = level.time;
|
|
gi.sound(ent, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex("ctf/flagret.wav"), 1, ATTN_NONE, 0);
|
|
// CTFResetFlag will remove this entity! We must return false
|
|
CTFResetFlag(ctf_team);
|
|
return false;
|
|
}
|
|
|
|
// hey, its not our flag, pick it up
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_got_flag",
|
|
other->client->pers.netname, CTFTeamName(ctf_team));
|
|
other->client->resp.score += CTF_FLAG_BONUS;
|
|
|
|
other->client->pers.inventory[flag_item] = 1;
|
|
other->client->resp.ctf_flagsince = level.time;
|
|
|
|
// pick up the flag
|
|
// if it's not a dropped flag, we just make is disappear
|
|
// if it's dropped, it will be removed by the pickup caller
|
|
if (!(ent->spawnflags & SPAWNFLAG_ITEM_DROPPED))
|
|
{
|
|
ent->flags |= FL_RESPAWN;
|
|
ent->svflags |= SVF_NOCLIENT;
|
|
ent->solid = SOLID_NOT;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
TOUCH(CTFDropFlagTouch) (edict_t *ent, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
|
|
{
|
|
// owner (who dropped us) can't touch for two secs
|
|
if (other == ent->owner &&
|
|
ent->nextthink - level.time > CTF_AUTO_FLAG_RETURN_TIMEOUT - 2_sec)
|
|
return;
|
|
|
|
Touch_Item(ent, other, tr, other_touching_self);
|
|
}
|
|
|
|
THINK(CTFDropFlagThink) (edict_t *ent) -> void
|
|
{
|
|
// auto return the flag
|
|
// reset flag will remove ourselves
|
|
if (ent->item->id == IT_FLAG1)
|
|
{
|
|
CTFResetFlag(CTF_TEAM1);
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_flag_returned",
|
|
CTFTeamName(CTF_TEAM1));
|
|
}
|
|
else if (ent->item->id == IT_FLAG2)
|
|
{
|
|
CTFResetFlag(CTF_TEAM2);
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_flag_returned",
|
|
CTFTeamName(CTF_TEAM2));
|
|
}
|
|
}
|
|
|
|
// Called from PlayerDie, to drop the flag from a dying player
|
|
void CTFDeadDropFlag(edict_t *self)
|
|
{
|
|
edict_t *dropped = nullptr;
|
|
|
|
if (self->client->pers.inventory[IT_FLAG1])
|
|
{
|
|
dropped = Drop_Item(self, GetItemByIndex(IT_FLAG1));
|
|
self->client->pers.inventory[IT_FLAG1] = 0;
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_lost_flag",
|
|
self->client->pers.netname, CTFTeamName(CTF_TEAM1));
|
|
}
|
|
else if (self->client->pers.inventory[IT_FLAG2])
|
|
{
|
|
dropped = Drop_Item(self, GetItemByIndex(IT_FLAG2));
|
|
self->client->pers.inventory[IT_FLAG2] = 0;
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_lost_flag",
|
|
self->client->pers.netname, CTFTeamName(CTF_TEAM2));
|
|
}
|
|
|
|
if (dropped)
|
|
{
|
|
dropped->think = CTFDropFlagThink;
|
|
dropped->nextthink = level.time + CTF_AUTO_FLAG_RETURN_TIMEOUT;
|
|
dropped->touch = CTFDropFlagTouch;
|
|
}
|
|
}
|
|
|
|
void CTFDrop_Flag(edict_t *ent, gitem_t *item)
|
|
{
|
|
if (brandom())
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "$g_lusers_drop_flags");
|
|
else
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "$g_winners_drop_flags");
|
|
}
|
|
|
|
THINK(CTFFlagThink) (edict_t *ent) -> void
|
|
{
|
|
if (ent->solid != SOLID_NOT)
|
|
ent->s.frame = 173 + (((ent->s.frame - 173) + 1) % 16);
|
|
ent->nextthink = level.time + 10_hz;
|
|
}
|
|
|
|
THINK(CTFFlagSetup) (edict_t *ent) -> void
|
|
{
|
|
trace_t tr;
|
|
vec3_t dest;
|
|
|
|
ent->mins = { -15, -15, -15 };
|
|
ent->maxs = { 15, 15, 15 };
|
|
|
|
if (ent->model)
|
|
gi.setmodel(ent, ent->model);
|
|
else
|
|
gi.setmodel(ent, ent->item->world_model);
|
|
ent->solid = SOLID_TRIGGER;
|
|
ent->movetype = MOVETYPE_TOSS;
|
|
ent->touch = Touch_Item;
|
|
ent->s.frame = 173;
|
|
|
|
dest = ent->s.origin + vec3_t { 0, 0, -128 };
|
|
|
|
tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, dest, ent, MASK_SOLID);
|
|
if (tr.startsolid)
|
|
{
|
|
gi.Com_PrintFmt("CTFFlagSetup: {} startsolid at {}\n", ent->classname, ent->s.origin);
|
|
G_FreeEdict(ent);
|
|
return;
|
|
}
|
|
|
|
ent->s.origin = tr.endpos;
|
|
|
|
gi.linkentity(ent);
|
|
|
|
ent->nextthink = level.time + 10_hz;
|
|
ent->think = CTFFlagThink;
|
|
}
|
|
|
|
void CTFEffects(edict_t *player)
|
|
{
|
|
player->s.effects &= ~(EF_FLAG1 | EF_FLAG2);
|
|
if (player->health > 0)
|
|
{
|
|
if (player->client->pers.inventory[IT_FLAG1])
|
|
{
|
|
player->s.effects |= EF_FLAG1;
|
|
}
|
|
if (player->client->pers.inventory[IT_FLAG2])
|
|
{
|
|
player->s.effects |= EF_FLAG2;
|
|
}
|
|
}
|
|
|
|
if (player->client->pers.inventory[IT_FLAG1])
|
|
player->s.modelindex3 = modelindex_flag1;
|
|
else if (player->client->pers.inventory[IT_FLAG2])
|
|
player->s.modelindex3 = modelindex_flag2;
|
|
else
|
|
player->s.modelindex3 = 0;
|
|
}
|
|
|
|
// called when we enter the intermission
|
|
void CTFCalcScores()
|
|
{
|
|
ctfgame.total1 = ctfgame.total2 = 0;
|
|
for (uint32_t i = 0; i < game.maxclients; i++)
|
|
{
|
|
if (!g_edicts[i + 1].inuse)
|
|
continue;
|
|
if (game.clients[i].resp.ctf_team == CTF_TEAM1)
|
|
ctfgame.total1 += game.clients[i].resp.score;
|
|
else if (game.clients[i].resp.ctf_team == CTF_TEAM2)
|
|
ctfgame.total2 += game.clients[i].resp.score;
|
|
}
|
|
}
|
|
|
|
// [Paril-KEX] end game rankings
|
|
void CTFCalcRankings(std::array<uint32_t, MAX_CLIENTS> &player_ranks)
|
|
{
|
|
// we're all winners.. or losers. whatever
|
|
if (ctfgame.total1 == ctfgame.total2)
|
|
{
|
|
player_ranks.fill(1);
|
|
return;
|
|
}
|
|
|
|
ctfteam_t winning_team = (ctfgame.total1 > ctfgame.total2) ? CTF_TEAM1 : CTF_TEAM2;
|
|
|
|
for (auto player : active_players())
|
|
if (player->client->pers.spawned && player->client->resp.ctf_team != CTF_NOTEAM)
|
|
player_ranks[player->s.number - 1] = player->client->resp.ctf_team == winning_team ? 1 : 2;
|
|
}
|
|
|
|
void CheckEndTDMLevel()
|
|
{
|
|
if (ctfgame.total1 >= fraglimit->integer || ctfgame.total2 >= fraglimit->integer)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_fraglimit_hit");
|
|
EndDMLevel();
|
|
}
|
|
}
|
|
|
|
void CTFID_f(edict_t *ent)
|
|
{
|
|
if (ent->client->resp.id_state)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Disabling player identication display.\n");
|
|
ent->client->resp.id_state = false;
|
|
}
|
|
else
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Activating player identication display.\n");
|
|
ent->client->resp.id_state = true;
|
|
}
|
|
}
|
|
|
|
static void CTFSetIDView(edict_t *ent)
|
|
{
|
|
vec3_t forward, dir;
|
|
trace_t tr;
|
|
edict_t *who, *best;
|
|
float bd = 0, d;
|
|
|
|
// only check every few frames
|
|
if (level.time - ent->client->resp.lastidtime < 250_ms)
|
|
return;
|
|
ent->client->resp.lastidtime = level.time;
|
|
|
|
ent->client->ps.stats[STAT_CTF_ID_VIEW] = 0;
|
|
ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = 0;
|
|
|
|
AngleVectors(ent->client->v_angle, forward, nullptr, nullptr);
|
|
forward *= 1024;
|
|
forward = ent->s.origin + forward;
|
|
tr = gi.traceline(ent->s.origin, forward, ent, MASK_SOLID);
|
|
if (tr.fraction < 1 && tr.ent && tr.ent->client)
|
|
{
|
|
ent->client->ps.stats[STAT_CTF_ID_VIEW] = (tr.ent - g_edicts);
|
|
if (tr.ent->client->resp.ctf_team == CTF_TEAM1)
|
|
ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = imageindex_sbfctf1;
|
|
else if (tr.ent->client->resp.ctf_team == CTF_TEAM2)
|
|
ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = imageindex_sbfctf2;
|
|
return;
|
|
}
|
|
|
|
AngleVectors(ent->client->v_angle, forward, nullptr, nullptr);
|
|
best = nullptr;
|
|
for (uint32_t i = 1; i <= game.maxclients; i++)
|
|
{
|
|
who = g_edicts + i;
|
|
if (!who->inuse || who->solid == SOLID_NOT)
|
|
continue;
|
|
dir = who->s.origin - ent->s.origin;
|
|
dir.normalize();
|
|
d = forward.dot(dir);
|
|
|
|
// we have teammate indicators that are better for this
|
|
if (ent->client->resp.ctf_team == who->client->resp.ctf_team)
|
|
continue;
|
|
|
|
if (d > bd && loc_CanSee(ent, who))
|
|
{
|
|
bd = d;
|
|
best = who;
|
|
}
|
|
}
|
|
if (bd > 0.90f)
|
|
{
|
|
ent->client->ps.stats[STAT_CTF_ID_VIEW] = (best - g_edicts);
|
|
if (best->client->resp.ctf_team == CTF_TEAM1)
|
|
ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = imageindex_sbfctf1;
|
|
else if (best->client->resp.ctf_team == CTF_TEAM2)
|
|
ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = imageindex_sbfctf2;
|
|
}
|
|
}
|
|
|
|
void SetCTFStats(edict_t *ent)
|
|
{
|
|
uint32_t i;
|
|
int p1, p2;
|
|
edict_t *e;
|
|
|
|
if (ctfgame.match > MATCH_NONE)
|
|
ent->client->ps.stats[STAT_CTF_MATCH] = CONFIG_CTF_MATCH;
|
|
else
|
|
ent->client->ps.stats[STAT_CTF_MATCH] = 0;
|
|
|
|
if (ctfgame.warnactive)
|
|
ent->client->ps.stats[STAT_CTF_TEAMINFO] = CONFIG_CTF_TEAMINFO;
|
|
else
|
|
ent->client->ps.stats[STAT_CTF_TEAMINFO] = 0;
|
|
|
|
// ghosting
|
|
if (ent->client->resp.ghost)
|
|
{
|
|
ent->client->resp.ghost->score = ent->client->resp.score;
|
|
Q_strlcpy(ent->client->resp.ghost->netname, ent->client->pers.netname, sizeof(ent->client->resp.ghost->netname));
|
|
ent->client->resp.ghost->number = ent->s.number;
|
|
}
|
|
|
|
// logo headers for the frag display
|
|
ent->client->ps.stats[STAT_CTF_TEAM1_HEADER] = imageindex_ctfsb1;
|
|
ent->client->ps.stats[STAT_CTF_TEAM2_HEADER] = imageindex_ctfsb2;
|
|
|
|
bool blink = (level.time.milliseconds() % 1000) < 500;
|
|
|
|
// if during intermission, we must blink the team header of the winning team
|
|
if (level.intermissiontime && blink)
|
|
{
|
|
// blink half second
|
|
// note that ctfgame.total[12] is set when we go to intermission
|
|
if (ctfgame.team1 > ctfgame.team2)
|
|
ent->client->ps.stats[STAT_CTF_TEAM1_HEADER] = 0;
|
|
else if (ctfgame.team2 > ctfgame.team1)
|
|
ent->client->ps.stats[STAT_CTF_TEAM2_HEADER] = 0;
|
|
else if (ctfgame.total1 > ctfgame.total2) // frag tie breaker
|
|
ent->client->ps.stats[STAT_CTF_TEAM1_HEADER] = 0;
|
|
else if (ctfgame.total2 > ctfgame.total1)
|
|
ent->client->ps.stats[STAT_CTF_TEAM2_HEADER] = 0;
|
|
else
|
|
{ // tie game!
|
|
ent->client->ps.stats[STAT_CTF_TEAM1_HEADER] = 0;
|
|
ent->client->ps.stats[STAT_CTF_TEAM2_HEADER] = 0;
|
|
}
|
|
}
|
|
|
|
// tech icon
|
|
i = 0;
|
|
ent->client->ps.stats[STAT_CTF_TECH] = 0;
|
|
for (; i < q_countof(tech_ids); i++)
|
|
{
|
|
if (ent->client->pers.inventory[tech_ids[i]])
|
|
{
|
|
ent->client->ps.stats[STAT_CTF_TECH] = gi.imageindex(GetItemByIndex(tech_ids[i])->icon);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ctf->integer)
|
|
{
|
|
// figure out what icon to display for team logos
|
|
// three states:
|
|
// flag at base
|
|
// flag taken
|
|
// flag dropped
|
|
p1 = imageindex_i_ctf1;
|
|
e = G_FindByString<&edict_t::classname>(nullptr, "item_flag_team1");
|
|
if (e != nullptr)
|
|
{
|
|
if (e->solid == SOLID_NOT)
|
|
{
|
|
// not at base
|
|
// check if on player
|
|
p1 = imageindex_i_ctf1d; // default to dropped
|
|
for (i = 1; i <= game.maxclients; i++)
|
|
if (g_edicts[i].inuse &&
|
|
g_edicts[i].client->pers.inventory[IT_FLAG1])
|
|
{
|
|
// enemy has it
|
|
p1 = imageindex_i_ctf1t;
|
|
break;
|
|
}
|
|
}
|
|
else if (e->spawnflags.has(SPAWNFLAG_ITEM_DROPPED))
|
|
p1 = imageindex_i_ctf1d; // must be dropped
|
|
}
|
|
p2 = imageindex_i_ctf2;
|
|
e = G_FindByString<&edict_t::classname>(nullptr, "item_flag_team2");
|
|
if (e != nullptr)
|
|
{
|
|
if (e->solid == SOLID_NOT)
|
|
{
|
|
// not at base
|
|
// check if on player
|
|
p2 = imageindex_i_ctf2d; // default to dropped
|
|
for (i = 1; i <= game.maxclients; i++)
|
|
if (g_edicts[i].inuse &&
|
|
g_edicts[i].client->pers.inventory[IT_FLAG2])
|
|
{
|
|
// enemy has it
|
|
p2 = imageindex_i_ctf2t;
|
|
break;
|
|
}
|
|
}
|
|
else if (e->spawnflags.has(SPAWNFLAG_ITEM_DROPPED))
|
|
p2 = imageindex_i_ctf2d; // must be dropped
|
|
}
|
|
|
|
ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = p1;
|
|
ent->client->ps.stats[STAT_CTF_TEAM2_PIC] = p2;
|
|
|
|
if (ctfgame.last_flag_capture && level.time - ctfgame.last_flag_capture < 5_sec)
|
|
{
|
|
if (ctfgame.last_capture_team == CTF_TEAM1)
|
|
if (blink)
|
|
ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = p1;
|
|
else
|
|
ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = 0;
|
|
else if (blink)
|
|
ent->client->ps.stats[STAT_CTF_TEAM2_PIC] = p2;
|
|
else
|
|
ent->client->ps.stats[STAT_CTF_TEAM2_PIC] = 0;
|
|
}
|
|
|
|
ent->client->ps.stats[STAT_CTF_TEAM1_CAPS] = ctfgame.team1;
|
|
ent->client->ps.stats[STAT_CTF_TEAM2_CAPS] = ctfgame.team2;
|
|
|
|
ent->client->ps.stats[STAT_CTF_FLAG_PIC] = 0;
|
|
if (ent->client->resp.ctf_team == CTF_TEAM1 &&
|
|
ent->client->pers.inventory[IT_FLAG2] &&
|
|
(blink))
|
|
ent->client->ps.stats[STAT_CTF_FLAG_PIC] = imageindex_i_ctf2;
|
|
|
|
else if (ent->client->resp.ctf_team == CTF_TEAM2 &&
|
|
ent->client->pers.inventory[IT_FLAG1] &&
|
|
(blink))
|
|
ent->client->ps.stats[STAT_CTF_FLAG_PIC] = imageindex_i_ctf1;
|
|
}
|
|
else
|
|
{
|
|
ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = imageindex_i_ctf1;
|
|
ent->client->ps.stats[STAT_CTF_TEAM2_PIC] = imageindex_i_ctf2;
|
|
|
|
ent->client->ps.stats[STAT_CTF_TEAM1_CAPS] = ctfgame.total1;
|
|
ent->client->ps.stats[STAT_CTF_TEAM2_CAPS] = ctfgame.total2;
|
|
}
|
|
|
|
ent->client->ps.stats[STAT_CTF_JOINED_TEAM1_PIC] = 0;
|
|
ent->client->ps.stats[STAT_CTF_JOINED_TEAM2_PIC] = 0;
|
|
if (ent->client->resp.ctf_team == CTF_TEAM1)
|
|
ent->client->ps.stats[STAT_CTF_JOINED_TEAM1_PIC] = imageindex_i_ctfj;
|
|
else if (ent->client->resp.ctf_team == CTF_TEAM2)
|
|
ent->client->ps.stats[STAT_CTF_JOINED_TEAM2_PIC] = imageindex_i_ctfj;
|
|
|
|
if (ent->client->resp.id_state)
|
|
CTFSetIDView(ent);
|
|
else
|
|
{
|
|
ent->client->ps.stats[STAT_CTF_ID_VIEW] = 0;
|
|
ent->client->ps.stats[STAT_CTF_ID_VIEW_COLOR] = 0;
|
|
}
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*/
|
|
|
|
/*QUAKED info_player_team1 (1 0 0) (-16 -16 -24) (16 16 32)
|
|
potential team1 spawning position for ctf games
|
|
*/
|
|
void SP_info_player_team1(edict_t *self)
|
|
{
|
|
}
|
|
|
|
/*QUAKED info_player_team2 (0 0 1) (-16 -16 -24) (16 16 32)
|
|
potential team2 spawning position for ctf games
|
|
*/
|
|
void SP_info_player_team2(edict_t *self)
|
|
{
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*/
|
|
/* GRAPPLE */
|
|
/*------------------------------------------------------------------------*/
|
|
|
|
// ent is player
|
|
void CTFPlayerResetGrapple(edict_t *ent)
|
|
{
|
|
if (ent->client && ent->client->ctf_grapple)
|
|
CTFResetGrapple(ent->client->ctf_grapple);
|
|
}
|
|
|
|
// self is grapple, not player
|
|
void CTFResetGrapple(edict_t *self)
|
|
{
|
|
if (!self->owner->client->ctf_grapple)
|
|
return;
|
|
|
|
gi.sound(self->owner, CHAN_WEAPON, gi.soundindex("weapons/grapple/grreset.wav"), self->owner->client->silencer_shots ? 0.2f : 1.0f, ATTN_NORM, 0);
|
|
|
|
gclient_t *cl;
|
|
cl = self->owner->client;
|
|
cl->ctf_grapple = nullptr;
|
|
cl->ctf_grapplereleasetime = level.time + 1_sec;
|
|
cl->ctf_grapplestate = CTF_GRAPPLE_STATE_FLY; // we're firing, not on hook
|
|
self->owner->flags &= ~FL_NO_KNOCKBACK;
|
|
G_FreeEdict(self);
|
|
}
|
|
|
|
TOUCH(CTFGrappleTouch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
|
|
{
|
|
float volume = 1.0;
|
|
|
|
if (other == self->owner)
|
|
return;
|
|
|
|
if (self->owner->client->ctf_grapplestate != CTF_GRAPPLE_STATE_FLY)
|
|
return;
|
|
|
|
if (tr.surface && (tr.surface->flags & SURF_SKY))
|
|
{
|
|
CTFResetGrapple(self);
|
|
return;
|
|
}
|
|
|
|
self->velocity = {};
|
|
|
|
PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT);
|
|
|
|
if (other->takedamage)
|
|
{
|
|
if (self->dmg)
|
|
T_Damage(other, self, self->owner, self->velocity, self->s.origin, tr.plane.normal, self->dmg, 1, DAMAGE_NONE, MOD_GRAPPLE);
|
|
CTFResetGrapple(self);
|
|
return;
|
|
}
|
|
|
|
self->owner->client->ctf_grapplestate = CTF_GRAPPLE_STATE_PULL; // we're on hook
|
|
self->enemy = other;
|
|
|
|
self->solid = SOLID_NOT;
|
|
|
|
if (self->owner->client->silencer_shots)
|
|
volume = 0.2f;
|
|
|
|
gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/grapple/grhit.wav"), volume, ATTN_NORM, 0);
|
|
self->s.sound = gi.soundindex("weapons/grapple/grpull.wav");
|
|
|
|
gi.WriteByte(svc_temp_entity);
|
|
gi.WriteByte(TE_SPARKS);
|
|
gi.WritePosition(self->s.origin);
|
|
gi.WriteDir(tr.plane.normal);
|
|
gi.multicast(self->s.origin, MULTICAST_PVS, false);
|
|
}
|
|
|
|
// draw beam between grapple and self
|
|
void CTFGrappleDrawCable(edict_t *self)
|
|
{
|
|
if (self->owner->client->ctf_grapplestate == CTF_GRAPPLE_STATE_HANG)
|
|
return;
|
|
|
|
vec3_t start, dir;
|
|
P_ProjectSource(self->owner, self->owner->client->v_angle, { 7, 2, -9 }, start, dir);
|
|
|
|
gi.WriteByte(svc_temp_entity);
|
|
gi.WriteByte(TE_GRAPPLE_CABLE_2);
|
|
gi.WriteEntity(self->owner);
|
|
gi.WritePosition(start);
|
|
gi.WritePosition(self->s.origin);
|
|
gi.multicast(self->s.origin, MULTICAST_PVS, false);
|
|
}
|
|
|
|
void SV_AddGravity(edict_t *ent);
|
|
|
|
// pull the player toward the grapple
|
|
void CTFGrapplePull(edict_t *self)
|
|
{
|
|
vec3_t hookdir, v;
|
|
float vlen;
|
|
|
|
if (self->owner->client->pers.weapon && self->owner->client->pers.weapon->id == IT_WEAPON_GRAPPLE &&
|
|
!(self->owner->client->newweapon || ((self->owner->client->latched_buttons | self->owner->client->buttons) & BUTTON_HOLSTER)) &&
|
|
self->owner->client->weaponstate != WEAPON_FIRING &&
|
|
self->owner->client->weaponstate != WEAPON_ACTIVATING)
|
|
{
|
|
if (!self->owner->client->newweapon)
|
|
self->owner->client->newweapon = self->owner->client->pers.weapon;
|
|
|
|
CTFResetGrapple(self);
|
|
return;
|
|
}
|
|
|
|
if (self->enemy)
|
|
{
|
|
if (self->enemy->solid == SOLID_NOT)
|
|
{
|
|
CTFResetGrapple(self);
|
|
return;
|
|
}
|
|
if (self->enemy->solid == SOLID_BBOX)
|
|
{
|
|
v = self->enemy->size * 0.5f;
|
|
v += self->enemy->s.origin;
|
|
self->s.origin = v + self->enemy->mins;
|
|
gi.linkentity(self);
|
|
}
|
|
else
|
|
self->velocity = self->enemy->velocity;
|
|
|
|
if (self->enemy->deadflag)
|
|
{ // he died
|
|
CTFResetGrapple(self);
|
|
return;
|
|
}
|
|
}
|
|
|
|
CTFGrappleDrawCable(self);
|
|
|
|
if (self->owner->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY)
|
|
{
|
|
// pull player toward grapple
|
|
vec3_t forward, up;
|
|
|
|
AngleVectors(self->owner->client->v_angle, forward, nullptr, up);
|
|
v = self->owner->s.origin;
|
|
v[2] += self->owner->viewheight;
|
|
hookdir = self->s.origin - v;
|
|
|
|
vlen = hookdir.length();
|
|
|
|
if (self->owner->client->ctf_grapplestate == CTF_GRAPPLE_STATE_PULL &&
|
|
vlen < 64)
|
|
{
|
|
self->owner->client->ctf_grapplestate = CTF_GRAPPLE_STATE_HANG;
|
|
self->s.sound = gi.soundindex("weapons/grapple/grhang.wav");
|
|
}
|
|
|
|
hookdir.normalize();
|
|
hookdir = hookdir * g_grapple_pull_speed->value;
|
|
self->owner->velocity = hookdir;
|
|
self->owner->flags |= FL_NO_KNOCKBACK;
|
|
SV_AddGravity(self->owner);
|
|
}
|
|
}
|
|
|
|
DIE(grapple_die) (edict_t *self, edict_t *other, edict_t *inflictor, int damage, const vec3_t &point, const mod_t &mod) -> void
|
|
{
|
|
if (mod.id == MOD_CRUSH)
|
|
CTFResetGrapple(self);
|
|
}
|
|
|
|
bool CTFFireGrapple(edict_t *self, const vec3_t &start, const vec3_t &dir, int damage, int speed, effects_t effect)
|
|
{
|
|
edict_t *grapple;
|
|
trace_t tr;
|
|
vec3_t normalized = dir.normalized();
|
|
|
|
grapple = G_Spawn();
|
|
grapple->s.origin = start;
|
|
grapple->s.old_origin = start;
|
|
grapple->s.angles = vectoangles(normalized);
|
|
grapple->velocity = normalized * speed;
|
|
grapple->movetype = MOVETYPE_FLYMISSILE;
|
|
grapple->clipmask = MASK_PROJECTILE;
|
|
// [Paril-KEX]
|
|
if (self->client && !G_ShouldPlayersCollide(true))
|
|
grapple->clipmask &= ~CONTENTS_PLAYER;
|
|
grapple->solid = SOLID_BBOX;
|
|
grapple->s.effects |= effect;
|
|
grapple->s.modelindex = gi.modelindex("models/weapons/grapple/hook/tris.md2");
|
|
grapple->owner = self;
|
|
grapple->touch = CTFGrappleTouch;
|
|
grapple->dmg = damage;
|
|
grapple->flags |= FL_NO_KNOCKBACK | FL_NO_DAMAGE_EFFECTS;
|
|
grapple->takedamage = true;
|
|
grapple->die = grapple_die;
|
|
self->client->ctf_grapple = grapple;
|
|
self->client->ctf_grapplestate = CTF_GRAPPLE_STATE_FLY; // we're firing, not on hook
|
|
gi.linkentity(grapple);
|
|
|
|
tr = gi.traceline(self->s.origin, grapple->s.origin, grapple, grapple->clipmask);
|
|
if (tr.fraction < 1.0f)
|
|
{
|
|
grapple->s.origin = tr.endpos + (tr.plane.normal * 1.f);
|
|
grapple->touch(grapple, tr.ent, tr, false);
|
|
return false;
|
|
}
|
|
|
|
grapple->s.sound = gi.soundindex("weapons/grapple/grfly.wav");
|
|
|
|
return true;
|
|
}
|
|
|
|
void CTFGrappleFire(edict_t *ent, const vec3_t &g_offset, int damage, effects_t effect)
|
|
{
|
|
float volume = 1.0;
|
|
|
|
if (ent->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY)
|
|
return; // it's already out
|
|
|
|
vec3_t start, dir;
|
|
P_ProjectSource(ent, ent->client->v_angle, vec3_t{ 24, 8, -8 + 2 } + g_offset, start, dir);
|
|
|
|
if (ent->client->silencer_shots)
|
|
volume = 0.2f;
|
|
|
|
if (CTFFireGrapple(ent, start, dir, damage, g_grapple_fly_speed->value, effect))
|
|
gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/grapple/grfire.wav"), volume, ATTN_NORM, 0);
|
|
|
|
PlayerNoise(ent, start, PNOISE_WEAPON);
|
|
}
|
|
|
|
void CTFWeapon_Grapple_Fire(edict_t *ent)
|
|
{
|
|
CTFGrappleFire(ent, vec3_origin, g_grapple_damage->integer, EF_NONE);
|
|
}
|
|
|
|
void CTFWeapon_Grapple(edict_t *ent)
|
|
{
|
|
constexpr int pause_frames[] = { 10, 18, 27, 0 };
|
|
constexpr int fire_frames[] = { 6, 0 };
|
|
int prevstate;
|
|
|
|
// if the the attack button is still down, stay in the firing frame
|
|
if ((ent->client->buttons & (BUTTON_ATTACK | BUTTON_HOLSTER)) &&
|
|
ent->client->weaponstate == WEAPON_FIRING &&
|
|
ent->client->ctf_grapple)
|
|
ent->client->ps.gunframe = 6;
|
|
|
|
if (!(ent->client->buttons & (BUTTON_ATTACK | BUTTON_HOLSTER)) &&
|
|
ent->client->ctf_grapple)
|
|
{
|
|
CTFResetGrapple(ent->client->ctf_grapple);
|
|
if (ent->client->weaponstate == WEAPON_FIRING)
|
|
ent->client->weaponstate = WEAPON_READY;
|
|
}
|
|
|
|
if ((ent->client->newweapon || ((ent->client->latched_buttons | ent->client->buttons) & BUTTON_HOLSTER)) &&
|
|
ent->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY &&
|
|
ent->client->weaponstate == WEAPON_FIRING)
|
|
{
|
|
// he wants to change weapons while grappled
|
|
if (!ent->client->newweapon)
|
|
ent->client->newweapon = ent->client->pers.weapon;
|
|
ent->client->weaponstate = WEAPON_DROPPING;
|
|
ent->client->ps.gunframe = 32;
|
|
}
|
|
|
|
prevstate = ent->client->weaponstate;
|
|
Weapon_Generic(ent, 5, 10, 31, 36, pause_frames, fire_frames,
|
|
CTFWeapon_Grapple_Fire);
|
|
|
|
// if the the attack button is still down, stay in the firing frame
|
|
if ((ent->client->buttons & (BUTTON_ATTACK | BUTTON_HOLSTER)) &&
|
|
ent->client->weaponstate == WEAPON_FIRING &&
|
|
ent->client->ctf_grapple)
|
|
ent->client->ps.gunframe = 6;
|
|
|
|
// if we just switched back to grapple, immediately go to fire frame
|
|
if (prevstate == WEAPON_ACTIVATING &&
|
|
ent->client->weaponstate == WEAPON_READY &&
|
|
ent->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY)
|
|
{
|
|
if (!(ent->client->buttons & (BUTTON_ATTACK | BUTTON_HOLSTER)))
|
|
ent->client->ps.gunframe = 6;
|
|
else
|
|
ent->client->ps.gunframe = 5;
|
|
ent->client->weaponstate = WEAPON_FIRING;
|
|
}
|
|
}
|
|
|
|
void CTFDirtyTeamMenu()
|
|
{
|
|
for (auto player : active_players())
|
|
if (player->client->menu)
|
|
{
|
|
player->client->menudirty = true;
|
|
player->client->menutime = level.time;
|
|
}
|
|
}
|
|
|
|
void CTFTeam_f(edict_t *ent)
|
|
{
|
|
if (!G_TeamplayEnabled())
|
|
return;
|
|
|
|
const char *t;
|
|
ctfteam_t desired_team;
|
|
|
|
t = gi.args();
|
|
if (!*t)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "$g_you_are_on_team",
|
|
CTFTeamName(ent->client->resp.ctf_team));
|
|
return;
|
|
}
|
|
|
|
if (ctfgame.match > MATCH_SETUP)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "$g_cant_change_teams");
|
|
return;
|
|
}
|
|
|
|
// [Paril-KEX] with force-join, don't allow us to switch
|
|
// using this command.
|
|
if (g_teamplay_force_join->integer)
|
|
{
|
|
if (!(ent->svflags & SVF_BOT))
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "$g_cant_change_teams");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (Q_strcasecmp(t, "red") == 0)
|
|
desired_team = CTF_TEAM1;
|
|
else if (Q_strcasecmp(t, "blue") == 0)
|
|
desired_team = CTF_TEAM2;
|
|
else
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "$g_unknown_team", t);
|
|
return;
|
|
}
|
|
|
|
if (ent->client->resp.ctf_team == desired_team)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "$g_already_on_team",
|
|
CTFTeamName(ent->client->resp.ctf_team));
|
|
return;
|
|
}
|
|
|
|
////
|
|
ent->svflags = SVF_NONE;
|
|
ent->flags &= ~FL_GODMODE;
|
|
ent->client->resp.ctf_team = desired_team;
|
|
ent->client->resp.ctf_state = 0;
|
|
char value[MAX_INFO_VALUE] = { 0 };
|
|
gi.Info_ValueForKey(ent->client->pers.userinfo, "skin", value, sizeof(value));
|
|
CTFAssignSkin(ent, value);
|
|
|
|
// if anybody has a menu open, update it immediately
|
|
CTFDirtyTeamMenu();
|
|
|
|
if (ent->solid == SOLID_NOT)
|
|
{
|
|
// spectator
|
|
PutClientInServer(ent);
|
|
|
|
G_PostRespawn(ent);
|
|
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_joined_team",
|
|
ent->client->pers.netname, CTFTeamName(desired_team));
|
|
return;
|
|
}
|
|
|
|
ent->health = 0;
|
|
player_die(ent, ent, ent, 100000, vec3_origin, { MOD_SUICIDE, true });
|
|
|
|
// don't even bother waiting for death frames
|
|
ent->deadflag = true;
|
|
respawn(ent);
|
|
|
|
ent->client->resp.score = 0;
|
|
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_changed_team",
|
|
ent->client->pers.netname, CTFTeamName(desired_team));
|
|
}
|
|
|
|
constexpr size_t MAX_CTF_STAT_LENGTH = 1024;
|
|
|
|
/*
|
|
==================
|
|
CTFScoreboardMessage
|
|
==================
|
|
*/
|
|
void CTFScoreboardMessage(edict_t *ent, edict_t *killer)
|
|
{
|
|
uint32_t i, j, k, n;
|
|
uint32_t sorted[2][MAX_CLIENTS];
|
|
int32_t sortedscores[2][MAX_CLIENTS];
|
|
int score;
|
|
uint32_t total[2];
|
|
int totalscore[2];
|
|
uint32_t last[2];
|
|
gclient_t *cl;
|
|
edict_t *cl_ent;
|
|
int team;
|
|
|
|
// sort the clients by team and score
|
|
total[0] = total[1] = 0;
|
|
last[0] = last[1] = 0;
|
|
totalscore[0] = totalscore[1] = 0;
|
|
for (i = 0; i < game.maxclients; i++)
|
|
{
|
|
cl_ent = g_edicts + 1 + i;
|
|
if (!cl_ent->inuse)
|
|
continue;
|
|
if (game.clients[i].resp.ctf_team == CTF_TEAM1)
|
|
team = 0;
|
|
else if (game.clients[i].resp.ctf_team == CTF_TEAM2)
|
|
team = 1;
|
|
else
|
|
continue; // unknown team?
|
|
|
|
score = game.clients[i].resp.score;
|
|
for (j = 0; j < total[team]; j++)
|
|
{
|
|
if (score > sortedscores[team][j])
|
|
break;
|
|
}
|
|
for (k = total[team]; k > j; k--)
|
|
{
|
|
sorted[team][k] = sorted[team][k - 1];
|
|
sortedscores[team][k] = sortedscores[team][k - 1];
|
|
}
|
|
sorted[team][j] = i;
|
|
sortedscores[team][j] = score;
|
|
totalscore[team] += score;
|
|
total[team]++;
|
|
}
|
|
|
|
// print level name and exit rules
|
|
// add the clients in sorted order
|
|
static std::string string;
|
|
string.clear();
|
|
|
|
// [Paril-KEX] time & frags
|
|
if (teamplay->integer)
|
|
{
|
|
if (fraglimit->integer)
|
|
{
|
|
fmt::format_to(std::back_inserter(string), FMT_STRING("xv -20 yv -10 loc_string2 1 $g_score_frags \"{}\" "), fraglimit->integer);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (capturelimit->integer)
|
|
{
|
|
fmt::format_to(std::back_inserter(string), FMT_STRING("xv -20 yv -10 loc_string2 1 $g_score_captures \"{}\" "), capturelimit->integer);
|
|
}
|
|
}
|
|
if (timelimit->value)
|
|
{
|
|
fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 time_limit {} "), gi.ServerFrame() + ((gtime_t::from_min(timelimit->value) - level.time)).milliseconds() / gi.frame_time_ms);
|
|
}
|
|
|
|
// team one
|
|
if (teamplay->integer)
|
|
{
|
|
fmt::format_to(std::back_inserter(string),
|
|
FMT_STRING("if 25 xv -32 yv 8 pic 25 endif "
|
|
"xv -123 yv 28 cstring \"{}\" "
|
|
"xv 41 yv 12 num 3 19 "
|
|
"if 26 xv 208 yv 8 pic 26 endif "
|
|
"xv 117 yv 28 cstring \"{}\" "
|
|
"xv 280 yv 12 num 3 21 "),
|
|
total[0],
|
|
total[1]);
|
|
}
|
|
else
|
|
{
|
|
fmt::format_to(std::back_inserter(string),
|
|
FMT_STRING("if 25 xv -32 yv 8 pic 25 endif "
|
|
"xv 0 yv 28 string \"{:4}/{:<3}\" "
|
|
"xv 58 yv 12 num 2 19 "
|
|
"if 26 xv 208 yv 8 pic 26 endif "
|
|
"xv 240 yv 28 string \"{:4}/{:<3}\" "
|
|
"xv 296 yv 12 num 2 21 "),
|
|
totalscore[0], total[0],
|
|
totalscore[1], total[1]);
|
|
}
|
|
|
|
for (i = 0; i < 16; i++)
|
|
{
|
|
if (i >= total[0] && i >= total[1])
|
|
break; // we're done
|
|
|
|
// left side
|
|
if (i < total[0])
|
|
{
|
|
cl = &game.clients[sorted[0][i]];
|
|
cl_ent = g_edicts + 1 + sorted[0][i];
|
|
|
|
std::string_view entry = G_Fmt("ctf -40 {} {} {} {} {} ",
|
|
42 + i * 8,
|
|
sorted[0][i],
|
|
cl->resp.score,
|
|
cl->ping > 999 ? 999 : cl->ping,
|
|
cl_ent->client->pers.inventory[IT_FLAG2] ? "sbfctf2" : "\"\"");
|
|
|
|
if (string.size() + entry.size() < MAX_CTF_STAT_LENGTH)
|
|
{
|
|
string += entry;
|
|
last[0] = i;
|
|
}
|
|
}
|
|
|
|
// right side
|
|
if (i < total[1])
|
|
{
|
|
cl = &game.clients[sorted[1][i]];
|
|
cl_ent = g_edicts + 1 + sorted[1][i];
|
|
|
|
std::string_view entry = G_Fmt("ctf 200 {} {} {} {} {} ",
|
|
42 + i * 8,
|
|
sorted[1][i],
|
|
cl->resp.score,
|
|
cl->ping > 999 ? 999 : cl->ping,
|
|
cl_ent->client->pers.inventory[IT_FLAG1] ? "sbfctf1" : "\"\"");
|
|
|
|
if (string.size() + entry.size() < MAX_CTF_STAT_LENGTH)
|
|
{
|
|
string += entry;
|
|
last[1] = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
// put in spectators if we have enough room
|
|
if (last[0] > last[1])
|
|
j = last[0];
|
|
else
|
|
j = last[1];
|
|
j = (j + 2) * 8 + 42;
|
|
|
|
k = n = 0;
|
|
if (string.size() < MAX_CTF_STAT_LENGTH - 50)
|
|
{
|
|
for (i = 0; i < game.maxclients; i++)
|
|
{
|
|
cl_ent = g_edicts + 1 + i;
|
|
cl = &game.clients[i];
|
|
if (!cl_ent->inuse ||
|
|
cl_ent->solid != SOLID_NOT ||
|
|
cl_ent->client->resp.ctf_team != CTF_NOTEAM)
|
|
continue;
|
|
|
|
if (!k)
|
|
{
|
|
k = 1;
|
|
fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv {} loc_string2 0 \"$g_pc_spectators\" "), j);
|
|
j += 8;
|
|
}
|
|
|
|
std::string_view entry = G_Fmt("ctf {} {} {} {} {} \"\" ",
|
|
(n & 1) ? 200 : -40, // x
|
|
j, // y
|
|
i, // playernum
|
|
cl->resp.score,
|
|
cl->ping > 999 ? 999 : cl->ping);
|
|
|
|
if (string.size() + entry.size() < MAX_CTF_STAT_LENGTH)
|
|
string += entry;
|
|
|
|
if (n & 1)
|
|
j += 8;
|
|
n++;
|
|
}
|
|
}
|
|
|
|
if (total[0] - last[0] > 1) // couldn't fit everyone
|
|
fmt::format_to(std::back_inserter(string), FMT_STRING("xv -32 yv {} loc_string 1 $g_ctf_and_more {} "),
|
|
42 + (last[0] + 1) * 8, total[0] - last[0] - 1);
|
|
if (total[1] - last[1] > 1) // couldn't fit everyone
|
|
fmt::format_to(std::back_inserter(string), FMT_STRING("xv 208 yv {} loc_string 1 $g_ctf_and_more {} "),
|
|
42 + (last[1] + 1) * 8, total[1] - last[1] - 1);
|
|
|
|
if (level.intermissiontime)
|
|
fmt::format_to(std::back_inserter(string), FMT_STRING("ifgef {} yb -48 xv 0 loc_cstring2 0 \"$m_eou_press_button\" endif "), (level.intermission_server_frame + (5_sec).frames()));
|
|
|
|
gi.WriteByte(svc_layout);
|
|
gi.WriteString(string.c_str());
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*/
|
|
/* TECH */
|
|
/*------------------------------------------------------------------------*/
|
|
|
|
void CTFHasTech(edict_t *who)
|
|
{
|
|
if (level.time - who->client->ctf_lasttechmsg > 2_sec)
|
|
{
|
|
gi.LocCenter_Print(who, "$g_already_have_tech");
|
|
who->client->ctf_lasttechmsg = level.time;
|
|
}
|
|
}
|
|
|
|
gitem_t *CTFWhat_Tech(edict_t *ent)
|
|
{
|
|
int i;
|
|
|
|
i = 0;
|
|
for (; i < q_countof(tech_ids); i++)
|
|
{
|
|
if (ent->client->pers.inventory[tech_ids[i]])
|
|
{
|
|
return GetItemByIndex(tech_ids[i]);
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool CTFPickup_Tech(edict_t *ent, edict_t *other)
|
|
{
|
|
int i;
|
|
|
|
i = 0;
|
|
for (; i < q_countof(tech_ids); i++)
|
|
{
|
|
if (other->client->pers.inventory[tech_ids[i]])
|
|
{
|
|
CTFHasTech(other);
|
|
return false; // has this one
|
|
}
|
|
}
|
|
|
|
// client only gets one tech
|
|
other->client->pers.inventory[ent->item->id]++;
|
|
other->client->ctf_regentime = level.time;
|
|
return true;
|
|
}
|
|
|
|
static void SpawnTech(gitem_t *item, edict_t *spot);
|
|
|
|
static edict_t *FindTechSpawn()
|
|
{
|
|
return SelectDeathmatchSpawnPoint(false, true, true).spot;
|
|
}
|
|
|
|
THINK(TechThink) (edict_t *tech) -> void
|
|
{
|
|
edict_t *spot;
|
|
|
|
if ((spot = FindTechSpawn()) != nullptr)
|
|
{
|
|
SpawnTech(tech->item, spot);
|
|
G_FreeEdict(tech);
|
|
}
|
|
else
|
|
{
|
|
tech->nextthink = level.time + CTF_TECH_TIMEOUT;
|
|
tech->think = TechThink;
|
|
}
|
|
}
|
|
|
|
void CTFDrop_Tech(edict_t *ent, gitem_t *item)
|
|
{
|
|
edict_t *tech;
|
|
|
|
tech = Drop_Item(ent, item);
|
|
tech->nextthink = level.time + CTF_TECH_TIMEOUT;
|
|
tech->think = TechThink;
|
|
ent->client->pers.inventory[item->id] = 0;
|
|
}
|
|
|
|
void CTFDeadDropTech(edict_t *ent)
|
|
{
|
|
edict_t *dropped;
|
|
int i;
|
|
|
|
i = 0;
|
|
for (; i < q_countof(tech_ids); i++)
|
|
{
|
|
if (ent->client->pers.inventory[tech_ids[i]])
|
|
{
|
|
dropped = Drop_Item(ent, GetItemByIndex(tech_ids[i]));
|
|
// hack the velocity to make it bounce random
|
|
dropped->velocity[0] = crandom_open() * 300;
|
|
dropped->velocity[1] = crandom_open() * 300;
|
|
dropped->nextthink = level.time + CTF_TECH_TIMEOUT;
|
|
dropped->think = TechThink;
|
|
dropped->owner = nullptr;
|
|
ent->client->pers.inventory[tech_ids[i]] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void SpawnTech(gitem_t *item, edict_t *spot)
|
|
{
|
|
edict_t *ent;
|
|
vec3_t forward, right;
|
|
vec3_t angles;
|
|
|
|
ent = G_Spawn();
|
|
|
|
ent->classname = item->classname;
|
|
ent->item = item;
|
|
ent->spawnflags = SPAWNFLAG_ITEM_DROPPED;
|
|
ent->s.effects = item->world_model_flags;
|
|
ent->s.renderfx = RF_GLOW | RF_NO_LOD;
|
|
ent->mins = { -15, -15, -15 };
|
|
ent->maxs = { 15, 15, 15 };
|
|
gi.setmodel(ent, ent->item->world_model);
|
|
ent->solid = SOLID_TRIGGER;
|
|
ent->movetype = MOVETYPE_TOSS;
|
|
ent->touch = Touch_Item;
|
|
ent->owner = ent;
|
|
|
|
angles[0] = 0;
|
|
angles[1] = (float) irandom(360);
|
|
angles[2] = 0;
|
|
|
|
AngleVectors(angles, forward, right, nullptr);
|
|
ent->s.origin = spot->s.origin;
|
|
ent->s.origin[2] += 16;
|
|
ent->velocity = forward * 100;
|
|
ent->velocity[2] = 300;
|
|
|
|
ent->nextthink = level.time + CTF_TECH_TIMEOUT;
|
|
ent->think = TechThink;
|
|
|
|
gi.linkentity(ent);
|
|
}
|
|
|
|
THINK(SpawnTechs) (edict_t *ent) -> void
|
|
{
|
|
edict_t *spot;
|
|
int i;
|
|
|
|
i = 0;
|
|
for (; i < q_countof(tech_ids); i++)
|
|
{
|
|
if ((spot = FindTechSpawn()) != nullptr)
|
|
SpawnTech(GetItemByIndex(tech_ids[i]), spot);
|
|
}
|
|
if (ent)
|
|
G_FreeEdict(ent);
|
|
}
|
|
|
|
// frees the passed edict!
|
|
void CTFRespawnTech(edict_t *ent)
|
|
{
|
|
edict_t *spot;
|
|
|
|
if ((spot = FindTechSpawn()) != nullptr)
|
|
SpawnTech(ent->item, spot);
|
|
G_FreeEdict(ent);
|
|
}
|
|
|
|
void CTFSetupTechSpawn()
|
|
{
|
|
edict_t *ent;
|
|
bool techs_allowed;
|
|
|
|
// [Paril-KEX]
|
|
if (!strcmp(g_allow_techs->string, "auto"))
|
|
techs_allowed = !!ctf->integer;
|
|
else
|
|
techs_allowed = !!g_allow_techs->integer;
|
|
|
|
if (!techs_allowed)
|
|
return;
|
|
|
|
ent = G_Spawn();
|
|
ent->nextthink = level.time + 2_sec;
|
|
ent->think = SpawnTechs;
|
|
}
|
|
|
|
void CTFResetTech()
|
|
{
|
|
edict_t *ent;
|
|
uint32_t i;
|
|
|
|
for (ent = g_edicts + 1, i = 1; i < globals.num_edicts; i++, ent++)
|
|
{
|
|
if (ent->inuse)
|
|
if (ent->item && (ent->item->flags & IF_TECH))
|
|
G_FreeEdict(ent);
|
|
}
|
|
SpawnTechs(nullptr);
|
|
}
|
|
|
|
int CTFApplyResistance(edict_t *ent, int dmg)
|
|
{
|
|
float volume = 1.0;
|
|
|
|
if (ent->client && ent->client->silencer_shots)
|
|
volume = 0.2f;
|
|
|
|
if (dmg && ent->client && ent->client->pers.inventory[IT_TECH_RESISTANCE])
|
|
{
|
|
// make noise
|
|
gi.sound(ent, CHAN_AUX, gi.soundindex("ctf/tech1.wav"), volume, ATTN_NORM, 0);
|
|
return dmg / 2;
|
|
}
|
|
return dmg;
|
|
}
|
|
|
|
int CTFApplyStrength(edict_t *ent, int dmg)
|
|
{
|
|
if (dmg && ent->client && ent->client->pers.inventory[IT_TECH_STRENGTH])
|
|
{
|
|
return dmg * 2;
|
|
}
|
|
return dmg;
|
|
}
|
|
|
|
bool CTFApplyStrengthSound(edict_t *ent)
|
|
{
|
|
float volume = 1.0;
|
|
|
|
if (ent->client && ent->client->silencer_shots)
|
|
volume = 0.2f;
|
|
|
|
if (ent->client &&
|
|
ent->client->pers.inventory[IT_TECH_STRENGTH])
|
|
{
|
|
if (ent->client->ctf_techsndtime < level.time)
|
|
{
|
|
ent->client->ctf_techsndtime = level.time + 1_sec;
|
|
if (ent->client->quad_time > level.time)
|
|
gi.sound(ent, CHAN_AUX, gi.soundindex("ctf/tech2x.wav"), volume, ATTN_NORM, 0);
|
|
else
|
|
gi.sound(ent, CHAN_AUX, gi.soundindex("ctf/tech2.wav"), volume, ATTN_NORM, 0);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool CTFApplyHaste(edict_t *ent)
|
|
{
|
|
if (ent->client &&
|
|
ent->client->pers.inventory[IT_TECH_HASTE])
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
void CTFApplyHasteSound(edict_t *ent)
|
|
{
|
|
float volume = 1.0;
|
|
|
|
if (ent->client && ent->client->silencer_shots)
|
|
volume = 0.2f;
|
|
|
|
if (ent->client &&
|
|
ent->client->pers.inventory[IT_TECH_HASTE] &&
|
|
ent->client->ctf_techsndtime < level.time)
|
|
{
|
|
ent->client->ctf_techsndtime = level.time + 1_sec;
|
|
gi.sound(ent, CHAN_AUX, gi.soundindex("ctf/tech3.wav"), volume, ATTN_NORM, 0);
|
|
}
|
|
}
|
|
|
|
void CTFApplyRegeneration(edict_t *ent)
|
|
{
|
|
bool noise = false;
|
|
gclient_t *client;
|
|
int index;
|
|
float volume = 1.0;
|
|
|
|
client = ent->client;
|
|
if (!client)
|
|
return;
|
|
|
|
if (ent->client->silencer_shots)
|
|
volume = 0.2f;
|
|
|
|
if (client->pers.inventory[IT_TECH_REGENERATION])
|
|
{
|
|
if (client->ctf_regentime < level.time)
|
|
{
|
|
client->ctf_regentime = level.time;
|
|
if (ent->health < 150)
|
|
{
|
|
ent->health += 5;
|
|
if (ent->health > 150)
|
|
ent->health = 150;
|
|
client->ctf_regentime += 500_ms;
|
|
noise = true;
|
|
}
|
|
index = ArmorIndex(ent);
|
|
if (index && client->pers.inventory[index] < 150)
|
|
{
|
|
client->pers.inventory[index] += 5;
|
|
if (client->pers.inventory[index] > 150)
|
|
client->pers.inventory[index] = 150;
|
|
client->ctf_regentime += 500_ms;
|
|
noise = true;
|
|
}
|
|
}
|
|
if (noise && ent->client->ctf_techsndtime < level.time)
|
|
{
|
|
ent->client->ctf_techsndtime = level.time + 1_sec;
|
|
gi.sound(ent, CHAN_AUX, gi.soundindex("ctf/tech4.wav"), volume, ATTN_NORM, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CTFHasRegeneration(edict_t *ent)
|
|
{
|
|
if (ent->client &&
|
|
ent->client->pers.inventory[IT_TECH_REGENERATION])
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
void CTFSay_Team(edict_t *who, const char *msg_in)
|
|
{
|
|
edict_t *cl_ent;
|
|
char outmsg[256];
|
|
|
|
if (CheckFlood(who))
|
|
return;
|
|
|
|
Q_strlcpy(outmsg, msg_in, sizeof(outmsg));
|
|
|
|
char *msg = outmsg;
|
|
|
|
if (*msg == '\"')
|
|
{
|
|
msg[strlen(msg) - 1] = 0;
|
|
msg++;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < game.maxclients; i++)
|
|
{
|
|
cl_ent = g_edicts + 1 + i;
|
|
if (!cl_ent->inuse)
|
|
continue;
|
|
if (cl_ent->client->resp.ctf_team == who->client->resp.ctf_team)
|
|
gi.LocClient_Print(cl_ent, PRINT_CHAT, "({}): {}\n",
|
|
who->client->pers.netname, msg);
|
|
}
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------*/
|
|
/*QUAKED misc_ctf_banner (1 .5 0) (-4 -64 0) (4 64 248) TEAM2
|
|
The origin is the bottom of the banner.
|
|
The banner is 248 tall.
|
|
*/
|
|
THINK(misc_ctf_banner_think) (edict_t *ent) -> void
|
|
{
|
|
ent->s.frame = (ent->s.frame + 1) % 16;
|
|
ent->nextthink = level.time + 10_hz;
|
|
}
|
|
|
|
constexpr spawnflags_t SPAWNFLAG_CTF_BANNER_BLUE = 1_spawnflag;
|
|
|
|
void SP_misc_ctf_banner(edict_t *ent)
|
|
{
|
|
ent->movetype = MOVETYPE_NONE;
|
|
ent->solid = SOLID_NOT;
|
|
ent->s.modelindex = gi.modelindex("models/ctf/banner/tris.md2");
|
|
if (ent->spawnflags.has(SPAWNFLAG_CTF_BANNER_BLUE)) // team2
|
|
ent->s.skinnum = 1;
|
|
|
|
ent->s.frame = irandom(16);
|
|
gi.linkentity(ent);
|
|
|
|
ent->think = misc_ctf_banner_think;
|
|
ent->nextthink = level.time + 10_hz;
|
|
}
|
|
|
|
/*QUAKED misc_ctf_small_banner (1 .5 0) (-4 -32 0) (4 32 124) TEAM2
|
|
The origin is the bottom of the banner.
|
|
The banner is 124 tall.
|
|
*/
|
|
void SP_misc_ctf_small_banner(edict_t *ent)
|
|
{
|
|
ent->movetype = MOVETYPE_NONE;
|
|
ent->solid = SOLID_NOT;
|
|
ent->s.modelindex = gi.modelindex("models/ctf/banner/small.md2");
|
|
if (ent->spawnflags.has(SPAWNFLAG_CTF_BANNER_BLUE)) // team2
|
|
ent->s.skinnum = 1;
|
|
|
|
ent->s.frame = irandom(16);
|
|
gi.linkentity(ent);
|
|
|
|
ent->think = misc_ctf_banner_think;
|
|
ent->nextthink = level.time + 10_hz;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------*/
|
|
|
|
static void SetGameName(pmenu_t *p)
|
|
{
|
|
if (ctf->integer)
|
|
Q_strlcpy(p->text, "$g_pc_3wctf", sizeof(p->text));
|
|
else
|
|
Q_strlcpy(p->text, "$g_pc_teamplay", sizeof(p->text));
|
|
}
|
|
|
|
static void SetLevelName(pmenu_t *p)
|
|
{
|
|
static char levelname[33];
|
|
|
|
levelname[0] = '*';
|
|
if (g_edicts[0].message)
|
|
Q_strlcpy(levelname + 1, g_edicts[0].message, sizeof(levelname) - 1);
|
|
else
|
|
Q_strlcpy(levelname + 1, level.mapname, sizeof(levelname) - 1);
|
|
levelname[sizeof(levelname) - 1] = 0;
|
|
Q_strlcpy(p->text, levelname, sizeof(p->text));
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------*/
|
|
|
|
/* ELECTIONS */
|
|
|
|
bool CTFBeginElection(edict_t *ent, elect_t type, const char *msg)
|
|
{
|
|
int count;
|
|
edict_t *e;
|
|
|
|
if (electpercentage->value == 0)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Elections are disabled, only an admin can process this action.\n");
|
|
return false;
|
|
}
|
|
|
|
if (ctfgame.election != ELECT_NONE)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Election already in progress.\n");
|
|
return false;
|
|
}
|
|
|
|
// clear votes
|
|
count = 0;
|
|
for (uint32_t i = 1; i <= game.maxclients; i++)
|
|
{
|
|
e = g_edicts + i;
|
|
e->client->resp.voted = false;
|
|
if (e->inuse)
|
|
count++;
|
|
}
|
|
|
|
if (count < 2)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Not enough players for election.\n");
|
|
return false;
|
|
}
|
|
|
|
ctfgame.etarget = ent;
|
|
ctfgame.election = type;
|
|
ctfgame.evotes = 0;
|
|
ctfgame.needvotes = (int) ((count * electpercentage->value) / 100);
|
|
ctfgame.electtime = level.time + 20_sec; // twenty seconds for election
|
|
Q_strlcpy(ctfgame.emsg, msg, sizeof(ctfgame.emsg));
|
|
|
|
// tell everyone
|
|
gi.Broadcast_Print(PRINT_CHAT, ctfgame.emsg);
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "Type YES or NO to vote on this request.\n");
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "Votes: {} Needed: {} Time left: {}s\n", ctfgame.evotes, ctfgame.needvotes,
|
|
(ctfgame.electtime - level.time).seconds<int>());
|
|
|
|
return true;
|
|
}
|
|
|
|
void DoRespawn(edict_t *ent);
|
|
|
|
void CTFResetAllPlayers()
|
|
{
|
|
uint32_t i;
|
|
edict_t *ent;
|
|
|
|
for (i = 1; i <= game.maxclients; i++)
|
|
{
|
|
ent = g_edicts + i;
|
|
if (!ent->inuse)
|
|
continue;
|
|
|
|
if (ent->client->menu)
|
|
PMenu_Close(ent);
|
|
|
|
CTFPlayerResetGrapple(ent);
|
|
CTFDeadDropFlag(ent);
|
|
CTFDeadDropTech(ent);
|
|
|
|
ent->client->resp.ctf_team = CTF_NOTEAM;
|
|
ent->client->resp.ready = false;
|
|
|
|
ent->svflags = SVF_NONE;
|
|
ent->flags &= ~FL_GODMODE;
|
|
PutClientInServer(ent);
|
|
}
|
|
|
|
// reset the level
|
|
CTFResetTech();
|
|
CTFResetFlags();
|
|
|
|
for (ent = g_edicts + 1, i = 1; i < globals.num_edicts; i++, ent++)
|
|
{
|
|
if (ent->inuse && !ent->client)
|
|
{
|
|
if (ent->solid == SOLID_NOT && ent->think == DoRespawn &&
|
|
ent->nextthink >= level.time)
|
|
{
|
|
ent->nextthink = 0_ms;
|
|
DoRespawn(ent);
|
|
}
|
|
}
|
|
}
|
|
if (ctfgame.match == MATCH_SETUP)
|
|
ctfgame.matchtime = level.time + gtime_t::from_min(matchsetuptime->value);
|
|
}
|
|
|
|
void CTFAssignGhost(edict_t *ent)
|
|
{
|
|
int ghost, i;
|
|
|
|
for (ghost = 0; ghost < MAX_CLIENTS; ghost++)
|
|
if (!ctfgame.ghosts[ghost].code)
|
|
break;
|
|
if (ghost == MAX_CLIENTS)
|
|
return;
|
|
ctfgame.ghosts[ghost].team = ent->client->resp.ctf_team;
|
|
ctfgame.ghosts[ghost].score = 0;
|
|
for (;;)
|
|
{
|
|
ctfgame.ghosts[ghost].code = irandom(10000, 100000);
|
|
for (i = 0; i < MAX_CLIENTS; i++)
|
|
if (i != ghost && ctfgame.ghosts[i].code == ctfgame.ghosts[ghost].code)
|
|
break;
|
|
if (i == MAX_CLIENTS)
|
|
break;
|
|
}
|
|
ctfgame.ghosts[ghost].ent = ent;
|
|
Q_strlcpy(ctfgame.ghosts[ghost].netname, ent->client->pers.netname, sizeof(ctfgame.ghosts[ghost].netname));
|
|
ent->client->resp.ghost = ctfgame.ghosts + ghost;
|
|
gi.LocClient_Print(ent, PRINT_CHAT, "Your ghost code is **** {} ****\n", ctfgame.ghosts[ghost].code);
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "If you lose connection, you can rejoin with your score intact by typing \"ghost {}\".\n",
|
|
ctfgame.ghosts[ghost].code);
|
|
}
|
|
|
|
// start a match
|
|
void CTFStartMatch()
|
|
{
|
|
edict_t *ent;
|
|
|
|
ctfgame.match = MATCH_GAME;
|
|
ctfgame.matchtime = level.time + gtime_t::from_min(matchtime->value);
|
|
ctfgame.countdown = false;
|
|
|
|
ctfgame.team1 = ctfgame.team2 = 0;
|
|
|
|
memset(ctfgame.ghosts, 0, sizeof(ctfgame.ghosts));
|
|
|
|
for (uint32_t i = 1; i <= game.maxclients; i++)
|
|
{
|
|
ent = g_edicts + i;
|
|
if (!ent->inuse)
|
|
continue;
|
|
|
|
ent->client->resp.score = 0;
|
|
ent->client->resp.ctf_state = 0;
|
|
ent->client->resp.ghost = nullptr;
|
|
|
|
gi.LocCenter_Print(ent, "******************\n\nMATCH HAS STARTED!\n\n******************");
|
|
|
|
if (ent->client->resp.ctf_team != CTF_NOTEAM)
|
|
{
|
|
// make up a ghost code
|
|
CTFAssignGhost(ent);
|
|
CTFPlayerResetGrapple(ent);
|
|
ent->svflags = SVF_NOCLIENT;
|
|
ent->flags &= ~FL_GODMODE;
|
|
|
|
ent->client->respawn_time = level.time + random_time(1_sec, 4_sec);
|
|
ent->client->ps.pmove.pm_type = PM_DEAD;
|
|
ent->client->anim_priority = ANIM_DEATH;
|
|
ent->s.frame = FRAME_death308 - 1;
|
|
ent->client->anim_end = FRAME_death308;
|
|
ent->deadflag = true;
|
|
ent->movetype = MOVETYPE_NOCLIP;
|
|
ent->client->ps.gunindex = 0;
|
|
ent->client->ps.gunskin = 0;
|
|
gi.linkentity(ent);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CTFEndMatch()
|
|
{
|
|
ctfgame.match = MATCH_POST;
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "MATCH COMPLETED!\n");
|
|
|
|
CTFCalcScores();
|
|
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "RED TEAM: {} captures, {} points\n",
|
|
ctfgame.team1, ctfgame.total1);
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "BLUE TEAM: {} captures, {} points\n",
|
|
ctfgame.team2, ctfgame.total2);
|
|
|
|
if (ctfgame.team1 > ctfgame.team2)
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "$g_ctf_red_wins_caps",
|
|
ctfgame.team1 - ctfgame.team2);
|
|
else if (ctfgame.team2 > ctfgame.team1)
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "$g_ctf_blue_wins_caps",
|
|
ctfgame.team2 - ctfgame.team1);
|
|
else if (ctfgame.total1 > ctfgame.total2) // frag tie breaker
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "$g_ctf_red_wins_points",
|
|
ctfgame.total1 - ctfgame.total2);
|
|
else if (ctfgame.total2 > ctfgame.total1)
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "$g_ctf_blue_wins_points",
|
|
ctfgame.total2 - ctfgame.total1);
|
|
else
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "$g_ctf_tie_game");
|
|
|
|
EndDMLevel();
|
|
}
|
|
|
|
bool CTFNextMap()
|
|
{
|
|
if (ctfgame.match == MATCH_POST)
|
|
{
|
|
ctfgame.match = MATCH_SETUP;
|
|
CTFResetAllPlayers();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void CTFWinElection()
|
|
{
|
|
switch (ctfgame.election)
|
|
{
|
|
case ELECT_MATCH:
|
|
// reset into match mode
|
|
if (competition->integer < 3)
|
|
gi.cvar_set("competition", "2");
|
|
ctfgame.match = MATCH_SETUP;
|
|
CTFResetAllPlayers();
|
|
break;
|
|
|
|
case ELECT_ADMIN:
|
|
ctfgame.etarget->client->resp.admin = true;
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} has become an admin.\n", ctfgame.etarget->client->pers.netname);
|
|
gi.LocClient_Print(ctfgame.etarget, PRINT_HIGH, "Type 'admin' to access the adminstration menu.\n");
|
|
break;
|
|
|
|
case ELECT_MAP:
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} is warping to level {}.\n",
|
|
ctfgame.etarget->client->pers.netname, ctfgame.elevel);
|
|
Q_strlcpy(level.forcemap, ctfgame.elevel, sizeof(level.forcemap));
|
|
EndDMLevel();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
ctfgame.election = ELECT_NONE;
|
|
}
|
|
|
|
void CTFVoteYes(edict_t *ent)
|
|
{
|
|
if (ctfgame.election == ELECT_NONE)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "No election is in progress.\n");
|
|
return;
|
|
}
|
|
if (ent->client->resp.voted)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "You already voted.\n");
|
|
return;
|
|
}
|
|
if (ctfgame.etarget == ent)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "You can't vote for yourself.\n");
|
|
return;
|
|
}
|
|
|
|
ent->client->resp.voted = true;
|
|
|
|
ctfgame.evotes++;
|
|
if (ctfgame.evotes == ctfgame.needvotes)
|
|
{
|
|
// the election has been won
|
|
CTFWinElection();
|
|
return;
|
|
}
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{}\n", ctfgame.emsg);
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "Votes: {} Needed: {} Time left: {}s\n", ctfgame.evotes, ctfgame.needvotes,
|
|
(ctfgame.electtime - level.time).seconds<int>());
|
|
}
|
|
|
|
void CTFVoteNo(edict_t *ent)
|
|
{
|
|
if (ctfgame.election == ELECT_NONE)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "No election is in progress.\n");
|
|
return;
|
|
}
|
|
if (ent->client->resp.voted)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "You already voted.\n");
|
|
return;
|
|
}
|
|
if (ctfgame.etarget == ent)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "You can't vote for yourself.\n");
|
|
return;
|
|
}
|
|
|
|
ent->client->resp.voted = true;
|
|
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{}\n", ctfgame.emsg);
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "Votes: {} Needed: {} Time left: {}s\n", ctfgame.evotes, ctfgame.needvotes,
|
|
(ctfgame.electtime - level.time).seconds<int>());
|
|
}
|
|
|
|
void CTFReady(edict_t *ent)
|
|
{
|
|
uint32_t i, j;
|
|
edict_t *e;
|
|
uint32_t t1, t2;
|
|
|
|
if (ent->client->resp.ctf_team == CTF_NOTEAM)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Pick a team first (hit <TAB> for menu)\n");
|
|
return;
|
|
}
|
|
|
|
if (ctfgame.match != MATCH_SETUP)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "A match is not being setup.\n");
|
|
return;
|
|
}
|
|
|
|
if (ent->client->resp.ready)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "You have already commited.\n");
|
|
return;
|
|
}
|
|
|
|
ent->client->resp.ready = true;
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} is ready.\n", ent->client->pers.netname);
|
|
|
|
t1 = t2 = 0;
|
|
for (j = 0, i = 1; i <= game.maxclients; i++)
|
|
{
|
|
e = g_edicts + i;
|
|
if (!e->inuse)
|
|
continue;
|
|
if (e->client->resp.ctf_team != CTF_NOTEAM && !e->client->resp.ready)
|
|
j++;
|
|
if (e->client->resp.ctf_team == CTF_TEAM1)
|
|
t1++;
|
|
else if (e->client->resp.ctf_team == CTF_TEAM2)
|
|
t2++;
|
|
}
|
|
if (!j && t1 && t2)
|
|
{
|
|
// everyone has commited
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "All players have committed. Match starting\n");
|
|
ctfgame.match = MATCH_PREGAME;
|
|
ctfgame.matchtime = level.time + gtime_t::from_sec(matchstarttime->value);
|
|
ctfgame.countdown = false;
|
|
gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("misc/talk1.wav"), 1, ATTN_NONE, 0);
|
|
}
|
|
}
|
|
|
|
void CTFNotReady(edict_t *ent)
|
|
{
|
|
if (ent->client->resp.ctf_team == CTF_NOTEAM)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Pick a team first (hit <TAB> for menu)\n");
|
|
return;
|
|
}
|
|
|
|
if (ctfgame.match != MATCH_SETUP && ctfgame.match != MATCH_PREGAME)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "A match is not being setup.\n");
|
|
return;
|
|
}
|
|
|
|
if (!ent->client->resp.ready)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "You haven't commited.\n");
|
|
return;
|
|
}
|
|
|
|
ent->client->resp.ready = false;
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} is no longer ready.\n", ent->client->pers.netname);
|
|
|
|
if (ctfgame.match == MATCH_PREGAME)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "Match halted.\n");
|
|
ctfgame.match = MATCH_SETUP;
|
|
ctfgame.matchtime = level.time + gtime_t::from_min(matchsetuptime->value);
|
|
}
|
|
}
|
|
|
|
void CTFGhost(edict_t *ent)
|
|
{
|
|
int i;
|
|
int n;
|
|
|
|
if (gi.argc() < 2)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Usage: ghost <code>\n");
|
|
return;
|
|
}
|
|
|
|
if (ent->client->resp.ctf_team != CTF_NOTEAM)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "You are already in the game.\n");
|
|
return;
|
|
}
|
|
if (ctfgame.match != MATCH_GAME)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "No match is in progress.\n");
|
|
return;
|
|
}
|
|
|
|
n = atoi(gi.argv(1));
|
|
|
|
for (i = 0; i < MAX_CLIENTS; i++)
|
|
{
|
|
if (ctfgame.ghosts[i].code && ctfgame.ghosts[i].code == n)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Ghost code accepted, your position has been reinstated.\n");
|
|
ctfgame.ghosts[i].ent->client->resp.ghost = nullptr;
|
|
ent->client->resp.ctf_team = ctfgame.ghosts[i].team;
|
|
ent->client->resp.ghost = ctfgame.ghosts + i;
|
|
ent->client->resp.score = ctfgame.ghosts[i].score;
|
|
ent->client->resp.ctf_state = 0;
|
|
ctfgame.ghosts[i].ent = ent;
|
|
ent->svflags = SVF_NONE;
|
|
ent->flags &= ~FL_GODMODE;
|
|
PutClientInServer(ent);
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} has been reinstated to {} team.\n",
|
|
ent->client->pers.netname, CTFTeamName(ent->client->resp.ctf_team));
|
|
return;
|
|
}
|
|
}
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Invalid ghost code.\n");
|
|
}
|
|
|
|
bool CTFMatchSetup()
|
|
{
|
|
if (ctfgame.match == MATCH_SETUP || ctfgame.match == MATCH_PREGAME)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
bool CTFMatchOn()
|
|
{
|
|
if (ctfgame.match == MATCH_GAME)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------*/
|
|
|
|
void CTFJoinTeam1(edict_t *ent, pmenuhnd_t *p);
|
|
void CTFJoinTeam2(edict_t *ent, pmenuhnd_t *p);
|
|
void CTFReturnToMain(edict_t *ent, pmenuhnd_t *p);
|
|
void CTFChaseCam(edict_t *ent, pmenuhnd_t *p);
|
|
|
|
static const int jmenu_level = 1;
|
|
static const int jmenu_match = 2;
|
|
static const int jmenu_red = 4;
|
|
static const int jmenu_blue = 7;
|
|
static const int jmenu_chase = 10;
|
|
static const int jmenu_reqmatch = 12;
|
|
|
|
const pmenu_t joinmenu[] = {
|
|
{ "*$g_pc_3wctf", PMENU_ALIGN_CENTER, nullptr },
|
|
{ "", PMENU_ALIGN_CENTER, nullptr },
|
|
{ "", PMENU_ALIGN_CENTER, nullptr },
|
|
{ "", PMENU_ALIGN_CENTER, nullptr },
|
|
{ "$g_pc_join_red_team", PMENU_ALIGN_LEFT, CTFJoinTeam1 },
|
|
{ "", PMENU_ALIGN_LEFT, nullptr },
|
|
{ "", PMENU_ALIGN_LEFT, nullptr },
|
|
{ "$g_pc_join_blue_team", PMENU_ALIGN_LEFT, CTFJoinTeam2 },
|
|
{ "", PMENU_ALIGN_LEFT, nullptr },
|
|
{ "", PMENU_ALIGN_LEFT, nullptr },
|
|
{ "$g_pc_chase_camera", PMENU_ALIGN_LEFT, CTFChaseCam },
|
|
{ "", PMENU_ALIGN_LEFT, nullptr },
|
|
{ "", PMENU_ALIGN_LEFT, nullptr },
|
|
};
|
|
|
|
const pmenu_t nochasemenu[] = {
|
|
{ "$g_pc_3wctf", PMENU_ALIGN_CENTER, nullptr },
|
|
{ "", PMENU_ALIGN_CENTER, nullptr },
|
|
{ "", PMENU_ALIGN_CENTER, nullptr },
|
|
{ "$g_pc_no_chase", PMENU_ALIGN_LEFT, nullptr },
|
|
{ "", PMENU_ALIGN_CENTER, nullptr },
|
|
{ "$g_pc_return", PMENU_ALIGN_LEFT, CTFReturnToMain }
|
|
};
|
|
|
|
void CTFJoinTeam(edict_t *ent, ctfteam_t desired_team)
|
|
{
|
|
PMenu_Close(ent);
|
|
|
|
ent->svflags &= ~SVF_NOCLIENT;
|
|
ent->client->resp.ctf_team = desired_team;
|
|
ent->client->resp.ctf_state = 0;
|
|
char value[MAX_INFO_VALUE] = { 0 };
|
|
gi.Info_ValueForKey(ent->client->pers.userinfo, "skin", value, sizeof(value));
|
|
CTFAssignSkin(ent, value);
|
|
|
|
// assign a ghost if we are in match mode
|
|
if (ctfgame.match == MATCH_GAME)
|
|
{
|
|
if (ent->client->resp.ghost)
|
|
ent->client->resp.ghost->code = 0;
|
|
ent->client->resp.ghost = nullptr;
|
|
CTFAssignGhost(ent);
|
|
}
|
|
|
|
PutClientInServer(ent);
|
|
|
|
G_PostRespawn(ent);
|
|
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_joined_team",
|
|
ent->client->pers.netname, CTFTeamName(desired_team));
|
|
|
|
if (ctfgame.match == MATCH_SETUP)
|
|
{
|
|
gi.LocCenter_Print(ent, "Type \"ready\" in console to ready up.\n");
|
|
}
|
|
|
|
// if anybody has a menu open, update it immediately
|
|
CTFDirtyTeamMenu();
|
|
}
|
|
|
|
void CTFJoinTeam1(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
CTFJoinTeam(ent, CTF_TEAM1);
|
|
}
|
|
|
|
void CTFJoinTeam2(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
CTFJoinTeam(ent, CTF_TEAM2);
|
|
}
|
|
|
|
static void CTFNoChaseCamUpdate(edict_t *ent)
|
|
{
|
|
pmenu_t *entries = ent->client->menu->entries;
|
|
|
|
SetGameName(&entries[0]);
|
|
SetLevelName(&entries[jmenu_level]);
|
|
}
|
|
|
|
void CTFChaseCam(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
edict_t *e;
|
|
|
|
CTFJoinTeam(ent, CTF_NOTEAM);
|
|
|
|
if (ent->client->chase_target)
|
|
{
|
|
ent->client->chase_target = nullptr;
|
|
ent->client->ps.pmove.pm_flags &= ~(PMF_NO_POSITIONAL_PREDICTION | PMF_NO_ANGULAR_PREDICTION);
|
|
PMenu_Close(ent);
|
|
return;
|
|
}
|
|
|
|
for (uint32_t i = 1; i <= game.maxclients; i++)
|
|
{
|
|
e = g_edicts + i;
|
|
if (e->inuse && e->solid != SOLID_NOT)
|
|
{
|
|
ent->client->chase_target = e;
|
|
PMenu_Close(ent);
|
|
ent->client->update_chase = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
PMenu_Close(ent);
|
|
PMenu_Open(ent, nochasemenu, -1, sizeof(nochasemenu) / sizeof(pmenu_t), nullptr, CTFNoChaseCamUpdate);
|
|
}
|
|
|
|
void CTFReturnToMain(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
PMenu_Close(ent);
|
|
CTFOpenJoinMenu(ent);
|
|
}
|
|
|
|
void CTFRequestMatch(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
PMenu_Close(ent);
|
|
|
|
CTFBeginElection(ent, ELECT_MATCH, G_Fmt("{} has requested to switch to competition mode.\n",
|
|
ent->client->pers.netname).data());
|
|
}
|
|
|
|
void DeathmatchScoreboard(edict_t *ent);
|
|
|
|
void CTFShowScores(edict_t *ent, pmenu_t *p)
|
|
{
|
|
PMenu_Close(ent);
|
|
|
|
ent->client->showscores = true;
|
|
ent->client->showinventory = false;
|
|
DeathmatchScoreboard(ent);
|
|
}
|
|
|
|
void CTFUpdateJoinMenu(edict_t *ent)
|
|
{
|
|
pmenu_t *entries = ent->client->menu->entries;
|
|
|
|
SetGameName(entries);
|
|
|
|
if (ctfgame.match >= MATCH_PREGAME && matchlock->integer)
|
|
{
|
|
Q_strlcpy(entries[jmenu_red].text, "MATCH IS LOCKED", sizeof(entries[jmenu_red].text));
|
|
entries[jmenu_red].SelectFunc = nullptr;
|
|
Q_strlcpy(entries[jmenu_blue].text, " (entry is not permitted)", sizeof(entries[jmenu_blue].text));
|
|
entries[jmenu_blue].SelectFunc = nullptr;
|
|
}
|
|
else
|
|
{
|
|
if (ctfgame.match >= MATCH_PREGAME)
|
|
{
|
|
Q_strlcpy(entries[jmenu_red].text, "Join Red MATCH Team", sizeof(entries[jmenu_red].text));
|
|
Q_strlcpy(entries[jmenu_blue].text, "Join Blue MATCH Team", sizeof(entries[jmenu_blue].text));
|
|
}
|
|
else
|
|
{
|
|
Q_strlcpy(entries[jmenu_red].text, "$g_pc_join_red_team", sizeof(entries[jmenu_red].text));
|
|
Q_strlcpy(entries[jmenu_blue].text, "$g_pc_join_blue_team", sizeof(entries[jmenu_blue].text));
|
|
}
|
|
entries[jmenu_red].SelectFunc = CTFJoinTeam1;
|
|
entries[jmenu_blue].SelectFunc = CTFJoinTeam2;
|
|
}
|
|
|
|
// KEX_FIXME: what's this for?
|
|
if (g_teamplay_force_join->string && *g_teamplay_force_join->string)
|
|
{
|
|
if (Q_strcasecmp(g_teamplay_force_join->string, "red") == 0)
|
|
{
|
|
entries[jmenu_blue].text[0] = '\0';
|
|
entries[jmenu_blue].SelectFunc = nullptr;
|
|
}
|
|
else if (Q_strcasecmp(g_teamplay_force_join->string, "blue") == 0)
|
|
{
|
|
entries[jmenu_red].text[0] = '\0';
|
|
entries[jmenu_red].SelectFunc = nullptr;
|
|
}
|
|
}
|
|
|
|
if (ent->client->chase_target)
|
|
Q_strlcpy(entries[jmenu_chase].text, "$g_pc_leave_chase_camera", sizeof(entries[jmenu_chase].text));
|
|
else
|
|
Q_strlcpy(entries[jmenu_chase].text, "$g_pc_chase_camera", sizeof(entries[jmenu_chase].text));
|
|
|
|
SetLevelName(entries + jmenu_level);
|
|
|
|
uint32_t num1 = 0, num2 = 0;
|
|
for (uint32_t i = 0; i < game.maxclients; i++)
|
|
{
|
|
if (!g_edicts[i + 1].inuse)
|
|
continue;
|
|
if (game.clients[i].resp.ctf_team == CTF_TEAM1)
|
|
num1++;
|
|
else if (game.clients[i].resp.ctf_team == CTF_TEAM2)
|
|
num2++;
|
|
}
|
|
|
|
switch (ctfgame.match)
|
|
{
|
|
case MATCH_NONE:
|
|
entries[jmenu_match].text[0] = '\0';
|
|
break;
|
|
|
|
case MATCH_SETUP:
|
|
Q_strlcpy(entries[jmenu_match].text, "*MATCH SETUP IN PROGRESS", sizeof(entries[jmenu_match].text));
|
|
break;
|
|
|
|
case MATCH_PREGAME:
|
|
Q_strlcpy(entries[jmenu_match].text, "*MATCH STARTING", sizeof(entries[jmenu_match].text));
|
|
break;
|
|
|
|
case MATCH_GAME:
|
|
Q_strlcpy(entries[jmenu_match].text, "*MATCH IN PROGRESS", sizeof(entries[jmenu_match].text));
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (*entries[jmenu_red].text)
|
|
{
|
|
Q_strlcpy(entries[jmenu_red + 1].text, "$g_pc_playercount", sizeof(entries[jmenu_red + 1].text));
|
|
G_FmtTo(entries[jmenu_red + 1].text_arg1, "{}", num1);
|
|
}
|
|
else
|
|
{
|
|
entries[jmenu_red + 1].text[0] = '\0';
|
|
entries[jmenu_red + 1].text_arg1[0] = '\0';
|
|
}
|
|
if (*entries[jmenu_blue].text)
|
|
{
|
|
Q_strlcpy(entries[jmenu_blue + 1].text, "$g_pc_playercount", sizeof(entries[jmenu_blue + 1].text));
|
|
G_FmtTo(entries[jmenu_blue + 1].text_arg1, "{}", num2);
|
|
}
|
|
else
|
|
{
|
|
entries[jmenu_blue + 1].text[0] = '\0';
|
|
entries[jmenu_blue + 1].text_arg1[0] = '\0';
|
|
}
|
|
|
|
entries[jmenu_reqmatch].text[0] = '\0';
|
|
entries[jmenu_reqmatch].SelectFunc = nullptr;
|
|
if (competition->integer && ctfgame.match < MATCH_SETUP)
|
|
{
|
|
Q_strlcpy(entries[jmenu_reqmatch].text, "Request Match", sizeof(entries[jmenu_reqmatch].text));
|
|
entries[jmenu_reqmatch].SelectFunc = CTFRequestMatch;
|
|
}
|
|
}
|
|
|
|
void CTFOpenJoinMenu(edict_t *ent)
|
|
{
|
|
uint32_t num1 = 0, num2 = 0;
|
|
for (uint32_t i = 0; i < game.maxclients; i++)
|
|
{
|
|
if (!g_edicts[i + 1].inuse)
|
|
continue;
|
|
if (game.clients[i].resp.ctf_team == CTF_TEAM1)
|
|
num1++;
|
|
else if (game.clients[i].resp.ctf_team == CTF_TEAM2)
|
|
num2++;
|
|
}
|
|
|
|
int team;
|
|
|
|
if (num1 > num2)
|
|
team = CTF_TEAM1;
|
|
else if (num2 > num1)
|
|
team = CTF_TEAM2;
|
|
team = brandom() ? CTF_TEAM1 : CTF_TEAM2;
|
|
|
|
PMenu_Open(ent, joinmenu, team, sizeof(joinmenu) / sizeof(pmenu_t), nullptr, CTFUpdateJoinMenu);
|
|
}
|
|
|
|
bool CTFStartClient(edict_t *ent)
|
|
{
|
|
if (!G_TeamplayEnabled())
|
|
return false;
|
|
|
|
if (ent->client->resp.ctf_team != CTF_NOTEAM)
|
|
return false;
|
|
|
|
if ((!(ent->svflags & SVF_BOT) && !g_teamplay_force_join->integer) || ctfgame.match >= MATCH_SETUP)
|
|
{
|
|
// start as 'observer'
|
|
ent->movetype = MOVETYPE_NOCLIP;
|
|
ent->solid = SOLID_NOT;
|
|
ent->svflags |= SVF_NOCLIENT;
|
|
ent->client->resp.ctf_team = CTF_NOTEAM;
|
|
ent->client->resp.spectator = true;
|
|
ent->client->ps.gunindex = 0;
|
|
ent->client->ps.gunskin = 0;
|
|
gi.linkentity(ent);
|
|
|
|
CTFOpenJoinMenu(ent);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void CTFObserver(edict_t *ent)
|
|
{
|
|
if (!G_TeamplayEnabled())
|
|
return;
|
|
|
|
// start as 'observer'
|
|
if (ent->movetype == MOVETYPE_NOCLIP)
|
|
CTFPlayerResetGrapple(ent);
|
|
|
|
CTFDeadDropFlag(ent);
|
|
CTFDeadDropTech(ent);
|
|
|
|
ent->deadflag = false;
|
|
ent->movetype = MOVETYPE_NOCLIP;
|
|
ent->solid = SOLID_NOT;
|
|
ent->svflags |= SVF_NOCLIENT;
|
|
ent->client->resp.ctf_team = CTF_NOTEAM;
|
|
ent->client->ps.gunindex = 0;
|
|
ent->client->ps.gunskin = 0;
|
|
ent->client->resp.score = 0;
|
|
PutClientInServer(ent);
|
|
}
|
|
|
|
bool CTFInMatch()
|
|
{
|
|
if (ctfgame.match > MATCH_NONE)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
bool CTFCheckRules()
|
|
{
|
|
int t;
|
|
uint32_t i, j;
|
|
char text[64];
|
|
edict_t *ent;
|
|
|
|
if (ctfgame.election != ELECT_NONE && ctfgame.electtime <= level.time)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "Election timed out and has been cancelled.\n");
|
|
ctfgame.election = ELECT_NONE;
|
|
}
|
|
|
|
if (ctfgame.match != MATCH_NONE)
|
|
{
|
|
t = (ctfgame.matchtime - level.time).seconds<int>();
|
|
|
|
// no team warnings in match mode
|
|
ctfgame.warnactive = 0;
|
|
|
|
if (t <= 0)
|
|
{ // time ended on something
|
|
switch (ctfgame.match)
|
|
{
|
|
case MATCH_SETUP:
|
|
// go back to normal mode
|
|
if (competition->integer < 3)
|
|
{
|
|
ctfgame.match = MATCH_NONE;
|
|
gi.cvar_set("competition", "1");
|
|
CTFResetAllPlayers();
|
|
}
|
|
else
|
|
{
|
|
// reset the time
|
|
ctfgame.matchtime = level.time + gtime_t::from_min(matchsetuptime->value);
|
|
}
|
|
return false;
|
|
|
|
case MATCH_PREGAME:
|
|
// match started!
|
|
CTFStartMatch();
|
|
gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("misc/tele_up.wav"), 1, ATTN_NONE, 0);
|
|
return false;
|
|
|
|
case MATCH_GAME:
|
|
// match ended!
|
|
CTFEndMatch();
|
|
gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("misc/bigtele.wav"), 1, ATTN_NONE, 0);
|
|
return false;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (t == ctfgame.lasttime)
|
|
return false;
|
|
|
|
ctfgame.lasttime = t;
|
|
|
|
switch (ctfgame.match)
|
|
{
|
|
case MATCH_SETUP:
|
|
for (j = 0, i = 1; i <= game.maxclients; i++)
|
|
{
|
|
ent = g_edicts + i;
|
|
if (!ent->inuse)
|
|
continue;
|
|
if (ent->client->resp.ctf_team != CTF_NOTEAM &&
|
|
!ent->client->resp.ready)
|
|
j++;
|
|
}
|
|
|
|
if (competition->integer < 3)
|
|
G_FmtTo(text, "{:02}:{:02} SETUP: {} not ready", t / 60, t % 60, j);
|
|
else
|
|
G_FmtTo(text, "SETUP: {} not ready", j);
|
|
|
|
gi.configstring(CONFIG_CTF_MATCH, text);
|
|
break;
|
|
|
|
case MATCH_PREGAME:
|
|
G_FmtTo(text, "{:02}:{:02} UNTIL START", t / 60, t % 60);
|
|
gi.configstring(CONFIG_CTF_MATCH, text);
|
|
|
|
if (t <= 10 && !ctfgame.countdown)
|
|
{
|
|
ctfgame.countdown = true;
|
|
gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("world/10_0.wav"), 1, ATTN_NONE, 0);
|
|
}
|
|
break;
|
|
|
|
case MATCH_GAME:
|
|
G_FmtTo(text, "{:02}:{:02} MATCH", t / 60, t % 60);
|
|
gi.configstring(CONFIG_CTF_MATCH, text);
|
|
if (t <= 10 && !ctfgame.countdown)
|
|
{
|
|
ctfgame.countdown = true;
|
|
gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("world/10_0.wav"), 1, ATTN_NONE, 0);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
int team1 = 0, team2 = 0;
|
|
|
|
if (level.time == gtime_t::from_sec(ctfgame.lasttime))
|
|
return false;
|
|
ctfgame.lasttime = level.time.seconds<int>();
|
|
// this is only done in non-match (public) mode
|
|
|
|
if (warn_unbalanced->integer)
|
|
{
|
|
// count up the team totals
|
|
for (i = 1; i <= game.maxclients; i++)
|
|
{
|
|
ent = g_edicts + i;
|
|
if (!ent->inuse)
|
|
continue;
|
|
if (ent->client->resp.ctf_team == CTF_TEAM1)
|
|
team1++;
|
|
else if (ent->client->resp.ctf_team == CTF_TEAM2)
|
|
team2++;
|
|
}
|
|
|
|
if (team1 - team2 >= 2 && team2 >= 2)
|
|
{
|
|
if (ctfgame.warnactive != CTF_TEAM1)
|
|
{
|
|
ctfgame.warnactive = CTF_TEAM1;
|
|
gi.configstring(CONFIG_CTF_TEAMINFO, "WARNING: Red has too many players");
|
|
}
|
|
}
|
|
else if (team2 - team1 >= 2 && team1 >= 2)
|
|
{
|
|
if (ctfgame.warnactive != CTF_TEAM2)
|
|
{
|
|
ctfgame.warnactive = CTF_TEAM2;
|
|
gi.configstring(CONFIG_CTF_TEAMINFO, "WARNING: Blue has too many players");
|
|
}
|
|
}
|
|
else
|
|
ctfgame.warnactive = 0;
|
|
}
|
|
else
|
|
ctfgame.warnactive = 0;
|
|
}
|
|
|
|
if (capturelimit->integer &&
|
|
(ctfgame.team1 >= capturelimit->integer ||
|
|
ctfgame.team2 >= capturelimit->integer))
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "$g_capturelimit_hit");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*--------------------------------------------------------------------------
|
|
* just here to help old map conversions
|
|
*--------------------------------------------------------------------------*/
|
|
|
|
TOUCH(old_teleporter_touch) (edict_t *self, edict_t *other, const trace_t &tr, bool other_touching_self) -> void
|
|
{
|
|
edict_t *dest;
|
|
vec3_t forward;
|
|
|
|
if (!other->client)
|
|
return;
|
|
dest = G_PickTarget(self->target);
|
|
if (!dest)
|
|
{
|
|
gi.Com_Print("Couldn't find destination\n");
|
|
return;
|
|
}
|
|
|
|
// ZOID
|
|
CTFPlayerResetGrapple(other);
|
|
// ZOID
|
|
|
|
// unlink to make sure it can't possibly interfere with KillBox
|
|
gi.unlinkentity(other);
|
|
|
|
other->s.origin = dest->s.origin;
|
|
other->s.old_origin = dest->s.origin;
|
|
// other->s.origin[2] += 10;
|
|
|
|
// clear the velocity and hold them in place briefly
|
|
other->velocity = {};
|
|
other->client->ps.pmove.pm_time = 160; // hold time
|
|
other->client->ps.pmove.pm_flags |= PMF_TIME_TELEPORT;
|
|
|
|
// draw the teleport splash at source and on the player
|
|
self->enemy->s.event = EV_PLAYER_TELEPORT;
|
|
other->s.event = EV_PLAYER_TELEPORT;
|
|
|
|
// set angles
|
|
other->client->ps.pmove.delta_angles = dest->s.angles - other->client->resp.cmd_angles;
|
|
|
|
other->s.angles[PITCH] = 0;
|
|
other->s.angles[YAW] = dest->s.angles[YAW];
|
|
other->s.angles[ROLL] = 0;
|
|
other->client->ps.viewangles = dest->s.angles;
|
|
other->client->v_angle = dest->s.angles;
|
|
|
|
// give a little forward velocity
|
|
AngleVectors(other->client->v_angle, forward, nullptr, nullptr);
|
|
other->velocity = forward * 200;
|
|
|
|
gi.linkentity(other);
|
|
|
|
// kill anything at the destination
|
|
if (!KillBox(other, true))
|
|
{
|
|
}
|
|
|
|
// [Paril-KEX] move sphere, if we own it
|
|
if (other->client->owned_sphere)
|
|
{
|
|
edict_t *sphere = other->client->owned_sphere;
|
|
sphere->s.origin = other->s.origin;
|
|
sphere->s.origin[2] = other->absmax[2];
|
|
sphere->s.angles[YAW] = other->s.angles[YAW];
|
|
gi.linkentity(sphere);
|
|
}
|
|
}
|
|
|
|
/*QUAKED trigger_ctf_teleport (0.5 0.5 0.5) ?
|
|
Players touching this will be teleported
|
|
*/
|
|
void SP_trigger_ctf_teleport(edict_t *ent)
|
|
{
|
|
edict_t *s;
|
|
int i;
|
|
|
|
if (!ent->target)
|
|
{
|
|
gi.Com_Print("teleporter without a target.\n");
|
|
G_FreeEdict(ent);
|
|
return;
|
|
}
|
|
|
|
ent->svflags |= SVF_NOCLIENT;
|
|
ent->solid = SOLID_TRIGGER;
|
|
ent->touch = old_teleporter_touch;
|
|
gi.setmodel(ent, ent->model);
|
|
gi.linkentity(ent);
|
|
|
|
// noise maker and splash effect dude
|
|
s = G_Spawn();
|
|
ent->enemy = s;
|
|
for (i = 0; i < 3; i++)
|
|
s->s.origin[i] = ent->mins[i] + (ent->maxs[i] - ent->mins[i]) / 2;
|
|
s->s.sound = gi.soundindex("world/hum1.wav");
|
|
gi.linkentity(s);
|
|
}
|
|
|
|
/*QUAKED info_ctf_teleport_destination (0.5 0.5 0.5) (-16 -16 -24) (16 16 32)
|
|
Point trigger_teleports at these.
|
|
*/
|
|
void SP_info_ctf_teleport_destination(edict_t *ent)
|
|
{
|
|
ent->s.origin[2] += 16;
|
|
}
|
|
|
|
/*----------------------------------------------------------------------------------*/
|
|
/* ADMIN */
|
|
|
|
struct admin_settings_t
|
|
{
|
|
int matchlen;
|
|
int matchsetuplen;
|
|
int matchstartlen;
|
|
bool weaponsstay;
|
|
bool instantitems;
|
|
bool quaddrop;
|
|
bool instantweap;
|
|
bool matchlock;
|
|
};
|
|
|
|
void CTFAdmin_UpdateSettings(edict_t *ent, pmenuhnd_t *setmenu);
|
|
void CTFOpenAdminMenu(edict_t *ent);
|
|
|
|
void CTFAdmin_SettingsApply(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
admin_settings_t *settings = (admin_settings_t *) p->arg;
|
|
|
|
if (settings->matchlen != matchtime->value)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} changed the match length to {} minutes.\n",
|
|
ent->client->pers.netname, settings->matchlen);
|
|
if (ctfgame.match == MATCH_GAME)
|
|
{
|
|
// in the middle of a match, change it on the fly
|
|
ctfgame.matchtime = (ctfgame.matchtime - gtime_t::from_min(matchtime->value)) + gtime_t::from_min(settings->matchlen);
|
|
}
|
|
;
|
|
gi.cvar_set("matchtime", G_Fmt("{}", settings->matchlen).data());
|
|
}
|
|
|
|
if (settings->matchsetuplen != matchsetuptime->value)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} changed the match setup time to {} minutes.\n",
|
|
ent->client->pers.netname, settings->matchsetuplen);
|
|
if (ctfgame.match == MATCH_SETUP)
|
|
{
|
|
// in the middle of a match, change it on the fly
|
|
ctfgame.matchtime = (ctfgame.matchtime - gtime_t::from_min(matchsetuptime->value)) + gtime_t::from_min(settings->matchsetuplen);
|
|
}
|
|
;
|
|
gi.cvar_set("matchsetuptime", G_Fmt("{}", settings->matchsetuplen).data());
|
|
}
|
|
|
|
if (settings->matchstartlen != matchstarttime->value)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} changed the match start time to {} seconds.\n",
|
|
ent->client->pers.netname, settings->matchstartlen);
|
|
if (ctfgame.match == MATCH_PREGAME)
|
|
{
|
|
// in the middle of a match, change it on the fly
|
|
ctfgame.matchtime = (ctfgame.matchtime - gtime_t::from_sec(matchstarttime->value)) + gtime_t::from_sec(settings->matchstartlen);
|
|
}
|
|
gi.cvar_set("matchstarttime", G_Fmt("{}", settings->matchstartlen).data());
|
|
}
|
|
|
|
if (settings->weaponsstay != !!g_dm_weapons_stay->integer)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} turned {} weapons stay.\n",
|
|
ent->client->pers.netname, settings->weaponsstay ? "on" : "off");
|
|
gi.cvar_set("g_dm_weapons_stay", settings->weaponsstay ? "1" : "0");
|
|
}
|
|
|
|
if (settings->instantitems != !!g_dm_instant_items->integer)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} turned {} instant items.\n",
|
|
ent->client->pers.netname, settings->instantitems ? "on" : "off");
|
|
gi.cvar_set("g_dm_instant_items", settings->instantitems ? "1" : "0");
|
|
}
|
|
|
|
if (settings->quaddrop != (bool) !g_dm_no_quad_drop->integer)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} turned {} quad drop.\n",
|
|
ent->client->pers.netname, settings->quaddrop ? "on" : "off");
|
|
gi.cvar_set("g_dm_no_quad_drop", !settings->quaddrop ? "1" : "0");
|
|
}
|
|
|
|
if (settings->instantweap != !!g_instant_weapon_switch->integer)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} turned {} instant weapons.\n",
|
|
ent->client->pers.netname, settings->instantweap ? "on" : "off");
|
|
gi.cvar_set("g_instant_weapon_switch", settings->instantweap ? "1" : "0");
|
|
}
|
|
|
|
if (settings->matchlock != !!matchlock->integer)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} turned {} match lock.\n",
|
|
ent->client->pers.netname, settings->matchlock ? "on" : "off");
|
|
gi.cvar_set("matchlock", settings->matchlock ? "1" : "0");
|
|
}
|
|
|
|
PMenu_Close(ent);
|
|
CTFOpenAdminMenu(ent);
|
|
}
|
|
|
|
void CTFAdmin_SettingsCancel(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
PMenu_Close(ent);
|
|
CTFOpenAdminMenu(ent);
|
|
}
|
|
|
|
void CTFAdmin_ChangeMatchLen(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
admin_settings_t *settings = (admin_settings_t *) p->arg;
|
|
|
|
settings->matchlen = (settings->matchlen % 60) + 5;
|
|
if (settings->matchlen < 5)
|
|
settings->matchlen = 5;
|
|
|
|
CTFAdmin_UpdateSettings(ent, p);
|
|
}
|
|
|
|
void CTFAdmin_ChangeMatchSetupLen(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
admin_settings_t *settings = (admin_settings_t *) p->arg;
|
|
|
|
settings->matchsetuplen = (settings->matchsetuplen % 60) + 5;
|
|
if (settings->matchsetuplen < 5)
|
|
settings->matchsetuplen = 5;
|
|
|
|
CTFAdmin_UpdateSettings(ent, p);
|
|
}
|
|
|
|
void CTFAdmin_ChangeMatchStartLen(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
admin_settings_t *settings = (admin_settings_t *) p->arg;
|
|
|
|
settings->matchstartlen = (settings->matchstartlen % 600) + 10;
|
|
if (settings->matchstartlen < 20)
|
|
settings->matchstartlen = 20;
|
|
|
|
CTFAdmin_UpdateSettings(ent, p);
|
|
}
|
|
|
|
void CTFAdmin_ChangeWeapStay(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
admin_settings_t *settings = (admin_settings_t *) p->arg;
|
|
|
|
settings->weaponsstay = !settings->weaponsstay;
|
|
CTFAdmin_UpdateSettings(ent, p);
|
|
}
|
|
|
|
void CTFAdmin_ChangeInstantItems(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
admin_settings_t *settings = (admin_settings_t *) p->arg;
|
|
|
|
settings->instantitems = !settings->instantitems;
|
|
CTFAdmin_UpdateSettings(ent, p);
|
|
}
|
|
|
|
void CTFAdmin_ChangeQuadDrop(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
admin_settings_t *settings = (admin_settings_t *) p->arg;
|
|
|
|
settings->quaddrop = !settings->quaddrop;
|
|
CTFAdmin_UpdateSettings(ent, p);
|
|
}
|
|
|
|
void CTFAdmin_ChangeInstantWeap(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
admin_settings_t *settings = (admin_settings_t *) p->arg;
|
|
|
|
settings->instantweap = !settings->instantweap;
|
|
CTFAdmin_UpdateSettings(ent, p);
|
|
}
|
|
|
|
void CTFAdmin_ChangeMatchLock(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
admin_settings_t *settings = (admin_settings_t *) p->arg;
|
|
|
|
settings->matchlock = !settings->matchlock;
|
|
CTFAdmin_UpdateSettings(ent, p);
|
|
}
|
|
|
|
void CTFAdmin_UpdateSettings(edict_t *ent, pmenuhnd_t *setmenu)
|
|
{
|
|
int i = 2;
|
|
admin_settings_t *settings = (admin_settings_t *) setmenu->arg;
|
|
|
|
PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Match Len: {:2} mins", settings->matchlen).data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeMatchLen);
|
|
i++;
|
|
|
|
PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Match Setup Len: {:2} mins", settings->matchsetuplen).data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeMatchSetupLen);
|
|
i++;
|
|
|
|
PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Match Start Len: {:2} secs", settings->matchstartlen).data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeMatchStartLen);
|
|
i++;
|
|
|
|
PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Weapons Stay: {}", settings->weaponsstay ? "Yes" : "No").data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeWeapStay);
|
|
i++;
|
|
|
|
PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Instant Items: {}", settings->instantitems ? "Yes" : "No").data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeInstantItems);
|
|
i++;
|
|
|
|
PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Quad Drop: {}", settings->quaddrop ? "Yes" : "No").data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeQuadDrop);
|
|
i++;
|
|
|
|
PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Instant Weapons: {}", settings->instantweap ? "Yes" : "No").data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeInstantWeap);
|
|
i++;
|
|
|
|
PMenu_UpdateEntry(setmenu->entries + i, G_Fmt("Match Lock: {}", settings->matchlock ? "Yes" : "No").data(), PMENU_ALIGN_LEFT, CTFAdmin_ChangeMatchLock);
|
|
i++;
|
|
|
|
PMenu_Update(ent);
|
|
}
|
|
|
|
const pmenu_t def_setmenu[] = {
|
|
{ "*Settings Menu", PMENU_ALIGN_CENTER, nullptr },
|
|
{ "", PMENU_ALIGN_CENTER, nullptr },
|
|
{ "", PMENU_ALIGN_LEFT, nullptr }, // int matchlen;
|
|
{ "", PMENU_ALIGN_LEFT, nullptr }, // int matchsetuplen;
|
|
{ "", PMENU_ALIGN_LEFT, nullptr }, // int matchstartlen;
|
|
{ "", PMENU_ALIGN_LEFT, nullptr }, // bool weaponsstay;
|
|
{ "", PMENU_ALIGN_LEFT, nullptr }, // bool instantitems;
|
|
{ "", PMENU_ALIGN_LEFT, nullptr }, // bool quaddrop;
|
|
{ "", PMENU_ALIGN_LEFT, nullptr }, // bool instantweap;
|
|
{ "", PMENU_ALIGN_LEFT, nullptr }, // bool matchlock;
|
|
{ "", PMENU_ALIGN_LEFT, nullptr },
|
|
{ "Apply", PMENU_ALIGN_LEFT, CTFAdmin_SettingsApply },
|
|
{ "Cancel", PMENU_ALIGN_LEFT, CTFAdmin_SettingsCancel }
|
|
};
|
|
|
|
void CTFAdmin_Settings(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
admin_settings_t *settings;
|
|
pmenuhnd_t *menu;
|
|
|
|
PMenu_Close(ent);
|
|
|
|
settings = (admin_settings_t *) gi.TagMalloc(sizeof(*settings), TAG_LEVEL);
|
|
|
|
settings->matchlen = matchtime->integer;
|
|
settings->matchsetuplen = matchsetuptime->integer;
|
|
settings->matchstartlen = matchstarttime->integer;
|
|
settings->weaponsstay = g_dm_weapons_stay->integer;
|
|
settings->instantitems = g_dm_instant_items->integer;
|
|
settings->quaddrop = !g_dm_no_quad_drop->integer;
|
|
settings->instantweap = g_instant_weapon_switch->integer != 0;
|
|
settings->matchlock = matchlock->integer != 0;
|
|
|
|
menu = PMenu_Open(ent, def_setmenu, -1, sizeof(def_setmenu) / sizeof(pmenu_t), settings, nullptr);
|
|
CTFAdmin_UpdateSettings(ent, menu);
|
|
}
|
|
|
|
void CTFAdmin_MatchSet(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
PMenu_Close(ent);
|
|
|
|
if (ctfgame.match == MATCH_SETUP)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "Match has been forced to start.\n");
|
|
ctfgame.match = MATCH_PREGAME;
|
|
ctfgame.matchtime = level.time + gtime_t::from_sec(matchstarttime->value);
|
|
gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("misc/talk1.wav"), 1, ATTN_NONE, 0);
|
|
ctfgame.countdown = false;
|
|
}
|
|
else if (ctfgame.match == MATCH_GAME)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "Match has been forced to terminate.\n");
|
|
ctfgame.match = MATCH_SETUP;
|
|
ctfgame.matchtime = level.time + gtime_t::from_min(matchsetuptime->value);
|
|
CTFResetAllPlayers();
|
|
}
|
|
}
|
|
|
|
void CTFAdmin_MatchMode(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
PMenu_Close(ent);
|
|
|
|
if (ctfgame.match != MATCH_SETUP)
|
|
{
|
|
if (competition->integer < 3)
|
|
gi.cvar_set("competition", "2");
|
|
ctfgame.match = MATCH_SETUP;
|
|
CTFResetAllPlayers();
|
|
}
|
|
}
|
|
|
|
void CTFAdmin_Reset(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
PMenu_Close(ent);
|
|
|
|
// go back to normal mode
|
|
gi.LocBroadcast_Print(PRINT_CHAT, "Match mode has been terminated, reseting to normal game.\n");
|
|
ctfgame.match = MATCH_NONE;
|
|
gi.cvar_set("competition", "1");
|
|
CTFResetAllPlayers();
|
|
}
|
|
|
|
void CTFAdmin_Cancel(edict_t *ent, pmenuhnd_t *p)
|
|
{
|
|
PMenu_Close(ent);
|
|
}
|
|
|
|
pmenu_t adminmenu[] = {
|
|
{ "*Administration Menu", PMENU_ALIGN_CENTER, nullptr },
|
|
{ "", PMENU_ALIGN_CENTER, nullptr }, // blank
|
|
{ "Settings", PMENU_ALIGN_LEFT, CTFAdmin_Settings },
|
|
{ "", PMENU_ALIGN_LEFT, nullptr },
|
|
{ "", PMENU_ALIGN_LEFT, nullptr },
|
|
{ "Cancel", PMENU_ALIGN_LEFT, CTFAdmin_Cancel },
|
|
{ "", PMENU_ALIGN_CENTER, nullptr },
|
|
};
|
|
|
|
void CTFOpenAdminMenu(edict_t *ent)
|
|
{
|
|
adminmenu[3].text[0] = '\0';
|
|
adminmenu[3].SelectFunc = nullptr;
|
|
adminmenu[4].text[0] = '\0';
|
|
adminmenu[4].SelectFunc = nullptr;
|
|
if (ctfgame.match == MATCH_SETUP)
|
|
{
|
|
Q_strlcpy(adminmenu[3].text, "Force start match", sizeof(adminmenu[3].text));
|
|
adminmenu[3].SelectFunc = CTFAdmin_MatchSet;
|
|
Q_strlcpy(adminmenu[4].text, "Reset to pickup mode", sizeof(adminmenu[4].text));
|
|
adminmenu[4].SelectFunc = CTFAdmin_Reset;
|
|
}
|
|
else if (ctfgame.match == MATCH_GAME || ctfgame.match == MATCH_PREGAME)
|
|
{
|
|
Q_strlcpy(adminmenu[3].text, "Cancel match", sizeof(adminmenu[3].text));
|
|
adminmenu[3].SelectFunc = CTFAdmin_MatchSet;
|
|
}
|
|
else if (ctfgame.match == MATCH_NONE && competition->integer)
|
|
{
|
|
Q_strlcpy(adminmenu[3].text, "Switch to match mode", sizeof(adminmenu[3].text));
|
|
adminmenu[3].SelectFunc = CTFAdmin_MatchMode;
|
|
}
|
|
|
|
// if (ent->client->menu)
|
|
// PMenu_Close(ent->client->menu);
|
|
|
|
PMenu_Open(ent, adminmenu, -1, sizeof(adminmenu) / sizeof(pmenu_t), nullptr, nullptr);
|
|
}
|
|
|
|
void CTFAdmin(edict_t *ent)
|
|
{
|
|
if (!allow_admin->integer)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Administration is disabled\n");
|
|
return;
|
|
}
|
|
|
|
if (gi.argc() > 1 && admin_password->string && *admin_password->string &&
|
|
!ent->client->resp.admin && strcmp(admin_password->string, gi.argv(1)) == 0)
|
|
{
|
|
ent->client->resp.admin = true;
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} has become an admin.\n", ent->client->pers.netname);
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Type 'admin' to access the adminstration menu.\n");
|
|
}
|
|
|
|
if (!ent->client->resp.admin)
|
|
{
|
|
CTFBeginElection(ent, ELECT_ADMIN, G_Fmt("{} has requested admin rights.\n",
|
|
ent->client->pers.netname).data());
|
|
return;
|
|
}
|
|
|
|
if (ent->client->menu)
|
|
PMenu_Close(ent);
|
|
|
|
CTFOpenAdminMenu(ent);
|
|
}
|
|
|
|
/*----------------------------------------------------------------*/
|
|
|
|
void CTFStats(edict_t *ent)
|
|
{
|
|
if (!G_TeamplayEnabled())
|
|
return;
|
|
|
|
ghost_t *g;
|
|
static std::string text;
|
|
edict_t *e2;
|
|
|
|
text.clear();
|
|
|
|
if (ctfgame.match == MATCH_SETUP)
|
|
{
|
|
for (uint32_t i = 1; i <= game.maxclients; i++)
|
|
{
|
|
e2 = g_edicts + i;
|
|
if (!e2->inuse)
|
|
continue;
|
|
if (!e2->client->resp.ready && e2->client->resp.ctf_team != CTF_NOTEAM)
|
|
{
|
|
std::string_view str = G_Fmt("{} is not ready.\n", e2->client->pers.netname);
|
|
|
|
if (text.length() + str.length() < MAX_CTF_STAT_LENGTH - 50)
|
|
text += str;
|
|
}
|
|
}
|
|
}
|
|
|
|
uint32_t i;
|
|
for (i = 0, g = ctfgame.ghosts; i < MAX_CLIENTS; i++, g++)
|
|
if (g->ent)
|
|
break;
|
|
|
|
if (i == MAX_CLIENTS)
|
|
{
|
|
if (!text.length())
|
|
text = "No statistics available.\n";
|
|
|
|
gi.Client_Print(ent, PRINT_HIGH, text.c_str());
|
|
return;
|
|
}
|
|
|
|
text += " #|Name |Score|Kills|Death|BasDf|CarDf|Effcy|\n";
|
|
|
|
for (i = 0, g = ctfgame.ghosts; i < MAX_CLIENTS; i++, g++)
|
|
{
|
|
if (!*g->netname)
|
|
continue;
|
|
|
|
int32_t e;
|
|
|
|
if (g->deaths + g->kills == 0)
|
|
e = 50;
|
|
else
|
|
e = g->kills * 100 / (g->kills + g->deaths);
|
|
std::string_view str = G_Fmt("{:3}|{:<16.16}|{:5}|{:5}|{:5}|{:5}|{:5}|{:4}%|\n",
|
|
g->number,
|
|
g->netname,
|
|
g->score,
|
|
g->kills,
|
|
g->deaths,
|
|
g->basedef,
|
|
g->carrierdef,
|
|
e);
|
|
|
|
if (text.length() + str.length() > MAX_CTF_STAT_LENGTH - 50)
|
|
{
|
|
text += "And more...\n";
|
|
break;
|
|
}
|
|
|
|
text += str;
|
|
}
|
|
|
|
gi.Client_Print(ent, PRINT_HIGH, text.c_str());
|
|
}
|
|
|
|
void CTFPlayerList(edict_t *ent)
|
|
{
|
|
static std::string text;
|
|
edict_t *e2;
|
|
|
|
// number, name, connect time, ping, score, admin
|
|
text.clear();
|
|
|
|
for (uint32_t i = 1; i <= game.maxclients; i++)
|
|
{
|
|
e2 = g_edicts + i;
|
|
if (!e2->inuse)
|
|
continue;
|
|
|
|
std::string_view str = G_Fmt("{:3} {:<16.16} {:02}:{:02} {:4} {:3}{}{}\n",
|
|
i,
|
|
e2->client->pers.netname,
|
|
(level.time - e2->client->resp.entertime).milliseconds() / 60000,
|
|
((level.time - e2->client->resp.entertime).milliseconds() % 60000) / 1000,
|
|
e2->client->ping,
|
|
e2->client->resp.score,
|
|
(ctfgame.match == MATCH_SETUP || ctfgame.match == MATCH_PREGAME) ? (e2->client->resp.ready ? " (ready)" : " (notready)") : "",
|
|
e2->client->resp.admin ? " (admin)" : "");
|
|
|
|
if (text.length() + str.length() > MAX_CTF_STAT_LENGTH - 50)
|
|
{
|
|
text += "And more...\n";
|
|
break;
|
|
|
|
}
|
|
|
|
text += str;
|
|
}
|
|
|
|
gi.Client_Print(ent, PRINT_HIGH, text.data());
|
|
}
|
|
|
|
void CTFWarp(edict_t *ent)
|
|
{
|
|
char *token;
|
|
|
|
if (gi.argc() < 2)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Where do you want to warp to?\n");
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Available levels are: {}\n", warp_list->string);
|
|
return;
|
|
}
|
|
|
|
const char *mlist = warp_list->string;
|
|
|
|
while (*(token = COM_Parse(&mlist)))
|
|
{
|
|
if (Q_strcasecmp(token, gi.argv(1)) == 0)
|
|
break;
|
|
}
|
|
|
|
if (!*token)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Unknown CTF level.\n");
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Available levels are: {}\n", warp_list->string);
|
|
return;
|
|
}
|
|
|
|
if (ent->client->resp.admin)
|
|
{
|
|
gi.LocBroadcast_Print(PRINT_HIGH, "{} is warping to level {}.\n",
|
|
ent->client->pers.netname, gi.argv(1));
|
|
Q_strlcpy(level.forcemap, gi.argv(1), sizeof(level.forcemap));
|
|
EndDMLevel();
|
|
return;
|
|
}
|
|
|
|
if (CTFBeginElection(ent, ELECT_MAP, G_Fmt("{} has requested warping to level {}.\n",
|
|
ent->client->pers.netname, gi.argv(1)).data()))
|
|
Q_strlcpy(ctfgame.elevel, gi.argv(1), sizeof(ctfgame.elevel));
|
|
}
|
|
|
|
void CTFBoot(edict_t *ent)
|
|
{
|
|
edict_t *targ;
|
|
|
|
if (!ent->client->resp.admin)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "You are not an admin.\n");
|
|
return;
|
|
}
|
|
|
|
if (gi.argc() < 2)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Who do you want to kick?\n");
|
|
return;
|
|
}
|
|
|
|
if (*gi.argv(1) < '0' && *gi.argv(1) > '9')
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Specify the player number to kick.\n");
|
|
return;
|
|
}
|
|
|
|
uint32_t i = strtoul(gi.argv(1), nullptr, 10);
|
|
if (i < 1 || i > game.maxclients)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "Invalid player number.\n");
|
|
return;
|
|
}
|
|
|
|
targ = g_edicts + i;
|
|
if (!targ->inuse)
|
|
{
|
|
gi.LocClient_Print(ent, PRINT_HIGH, "That player number is not connected.\n");
|
|
return;
|
|
}
|
|
|
|
gi.AddCommandString(G_Fmt("kick {}\n", i - 1).data());
|
|
}
|
|
|
|
void CTFSetPowerUpEffect(edict_t *ent, effects_t def)
|
|
{
|
|
if (ent->client->resp.ctf_team == CTF_TEAM1 && def == EF_QUAD)
|
|
ent->s.effects |= EF_PENT; // red
|
|
else if (ent->client->resp.ctf_team == CTF_TEAM2 && def == EF_PENT)
|
|
ent->s.effects |= EF_QUAD; // blue
|
|
else
|
|
ent->s.effects |= def;
|
|
}
|