diff --git a/src/game/baseq2/g_ctf.c b/src/game/baseq2/g_ctf.c new file mode 100644 index 00000000..39b634a7 --- /dev/null +++ b/src/game/baseq2/g_ctf.c @@ -0,0 +1,5420 @@ +/* + * Copyright (C) 1997-2001 Id Software, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * ======================================================================= + * + * The CTF gamemode with all support functions. + * + * ======================================================================= + */ + +#ifdef CTF + +#include "g_local.h" +#include "m_player.h" + +typedef enum match_s +{ + MATCH_NONE, + MATCH_SETUP, + MATCH_PREGAME, + MATCH_GAME, + MATCH_POST +} match_t; + +typedef enum +{ + ELECT_NONE, + ELECT_MATCH, + ELECT_ADMIN, + ELECT_MAP +} elect_t; + +typedef struct ctfgame_s +{ + int team1, team2; + int total1, total2; /* these are only set when going into intermission! */ + float last_flag_capture; + int last_capture_team; + + match_t match; /* match state */ + float matchtime; /* time for match start/end (depends on state) */ + int lasttime; /* last time update */ + qboolean 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 */ + float 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_t ctfgame; + +cvar_t *ctf; +cvar_t *ctf_forcejoin; + +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; + +char *ctf_statusbar = + "yb -24 " + + /* health */ + "xv 0 " + "hnum " + "xv 50 " + "pic 0 " + + /* ammo */ + "if 2 " + " xv 100 " + " anum " + " xv 150 " + " pic 2 " + "endif " + + /* armor */ + "if 4 " + " xv 200 " + " rnum " + " xv 250 " + " pic 4 " + "endif " + + /* selected item */ + "if 6 " + " xv 296 " + " pic 6 " + "endif " + + "yb -50 " + + /* picked up item */ + "if 7 " + " xv 0 " + " pic 7 " + " xv 26 " + " yb -42 " + " stat_string 8 " + " yb -50 " + "endif " + + /* timer */ + "if 9 " + "xv 246 " + "num 2 10 " + "xv 296 " + "pic 9 " + "endif " + + /* help / weapon icon */ + "if 11 " + "xv 148 " + "pic 11 " + "endif " + + /* frags */ + "xr -50 " + "yt 2 " + "num 3 14 " + + /* tech */ + "yb -129 " + "if 26 " + "xr -26 " + "pic 26 " + "endif " + + /* red team */ + "yb -102 " + "if 17 " + "xr -26 " + "pic 17 " + "endif " + "xr -62 " + "num 2 18 " + + /* joined overlay */ + "if 22 " + "yb -104 " + "xr -28 " + "pic 22 " + "endif " + + /* blue team */ + "yb -75 " + "if 19 " + "xr -26 " + "pic 19 " + "endif " + "xr -62 " + "num 2 20 " + "if 23 " + "yb -77 " + "xr -28 " + "pic 23 " + "endif " + + /* have flag graph */ + "if 21 " + "yt 26 " + "xr -24 " + "pic 21 " + "endif " + + /* id view state */ + "if 27 " + "xv 112 " + "yb -58 " + "stat_string 27 " + "endif " + + "if 29 " + "xv 96 " + "yb -58 " + "pic 29 " + "endif " + + "if 28 " + "xl 0 " + "yb -78 " + "stat_string 28 " + "endif " + + "if 30 " + "xl 0 " + "yb -88 " + "stat_string 30 " + "endif " +; + +static char *tnames[] = { + "item_tech1", "item_tech2", "item_tech3", "item_tech4", + NULL +}; + +void +stuffcmd(edict_t *ent, char *s) +{ + gi.WriteByte(11); + gi.WriteString(s); + gi.unicast(ent, true); +} + +/*--------------------------------------------------------------------------*/ + +/* + * Returns entities that have origins + * within a spherical area. + */ +static edict_t * +loc_findradius(edict_t *from, 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.5); + } + + if (VectorLength(eorg) > rad) + { + continue; + } + + return from; + } + + return NULL; +} + +static void +loc_buildboxpoints(vec3_t p[8], vec3_t org, vec3_t mins, vec3_t maxs) +{ + VectorAdd(org, mins, p[0]); + VectorCopy(p[0], p[1]); + p[1][0] -= mins[0]; + VectorCopy(p[0], p[2]); + p[2][1] -= mins[1]; + VectorCopy(p[0], p[3]); + p[3][0] -= mins[0]; + p[3][1] -= mins[1]; + VectorAdd(org, maxs, p[4]); + VectorCopy(p[4], p[5]); + p[5][0] -= maxs[0]; + VectorCopy(p[0], p[6]); + p[6][1] -= maxs[1]; + VectorCopy(p[0], p[7]); + p[7][0] -= maxs[0]; + p[7][1] -= maxs[1]; +} + +static qboolean +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); + + VectorCopy(inflictor->s.origin, viewpoint); + viewpoint[2] += inflictor->viewheight; + + for (i = 0; i < 8; i++) + { + trace = gi.trace(viewpoint, vec3_origin, vec3_origin, + targpoints[i], inflictor, MASK_SOLID); + + if (trace.fraction == 1.0) + { + return true; + } + } + + return false; +} + +/*--------------------------------------------------------------------------*/ + +static gitem_t *flag1_item; +static gitem_t *flag2_item; + +void +CTFSpawn(void) +{ + if (!flag1_item) + { + flag1_item = FindItemByClassname("item_flag_team1"); + } + + if (!flag2_item) + { + flag2_item = FindItemByClassname("item_flag_team2"); + } + + memset(&ctfgame, 0, sizeof(ctfgame)); + CTFSetupTechSpawn(); + + if (competition->value > 1) + { + ctfgame.match = MATCH_SETUP; + ctfgame.matchtime = level.time + matchsetuptime->value * 60; + } +} + +void +CTFInit(void) +{ + ctf = gi.cvar("ctf", "1", CVAR_SERVERINFO); + ctf_forcejoin = gi.cvar("ctf_forcejoin", "", 0); + competition = gi.cvar("competition", "0", CVAR_SERVERINFO); + matchlock = gi.cvar("matchlock", "1", CVAR_SERVERINFO); + electpercentage = gi.cvar("electpercentage", "66", 0); + matchtime = gi.cvar("matchtime", "20", CVAR_SERVERINFO); + matchsetuptime = gi.cvar("matchsetuptime", "10", 0); + matchstarttime = gi.cvar("matchstarttime", "20", 0); + admin_password = gi.cvar("admin_password", "", 0); + allow_admin = gi.cvar("allow_admin", "1", 0); + warp_list = gi.cvar("warp_list", "q2ctf1 q2ctf2 q2ctf3 q2ctf4 q2ctf5", 0); + warn_unbalanced = gi.cvar("warn_unbalanced", "1", 0); +} + +/* + * Precache CTF items + */ +void +CTFPrecache(void) +{ + 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("ctfsb1"); + imageindex_ctfsb2 = gi.imageindex("ctfsb2"); +} + +/*--------------------------------------------------------------------------*/ + +char * +CTFTeamName(int team) +{ + switch (team) + { + case CTF_TEAM1: + return "RED"; + case CTF_TEAM2: + return "BLUE"; + } + + return "UNKNOWN"; +} + +char * +CTFOtherTeamName(int team) +{ + switch (team) + { + case CTF_TEAM1: + return "BLUE"; + case CTF_TEAM2: + return "RED"; + } + + return "UNKNOWN"; +} + +int +CTFOtherTeam(int team) +{ + switch (team) + { + case CTF_TEAM1: + return CTF_TEAM2; + case CTF_TEAM2: + return CTF_TEAM1; + } + + return -1; /* invalid value */ +} + +/*--------------------------------------------------------------------------*/ + +edict_t *SelectRandomDeathmatchSpawnPoint(void); +edict_t *SelectFarthestDeathmatchSpawnPoint(void); +float PlayersRangeFromSpot(edict_t *spot); + +void +CTFAssignSkin(edict_t *ent, char *s) +{ + int playernum = ent - g_edicts - 1; + char *p; + char t[64]; + + Com_sprintf(t, sizeof(t), "%s", s); + + if ((p = strchr(t, '/')) != NULL) + { + p[1] = 0; + } + else + { + strcpy(t, "male/"); + } + + switch (ent->client->resp.ctf_team) + { + case CTF_TEAM1: + gi.configstring(CS_PLAYERSKINS + playernum, va("%s\\%s%s", + ent->client->pers.netname, t, CTF_TEAM1_SKIN)); + break; + case CTF_TEAM2: + gi.configstring(CS_PLAYERSKINS + playernum, + va("%s\\%s%s", ent->client->pers.netname, t, CTF_TEAM2_SKIN)); + break; + default: + gi.configstring(CS_PLAYERSKINS + playernum, + va("%s\\%s", ent->client->pers.netname, s)); + break; + } +} + +void +CTFAssignTeam(gclient_t *who) +{ + edict_t *player; + int i; + int team1count = 0, team2count = 0; + + who->resp.ctf_state = 0; + + if (!((int)dmflags->value & DF_CTF_FORCEJOIN)) + { + who->resp.ctf_team = CTF_NOTEAM; + return; + } + + for (i = 1; i <= maxclients->value; 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++; + } + } + + if (team1count < team2count) + { + who->resp.ctf_team = CTF_TEAM1; + } + else if (team2count < team1count) + { + who->resp.ctf_team = CTF_TEAM2; + } + else if (rand() & 1) + { + who->resp.ctf_team = CTF_TEAM1; + } + else + { + who->resp.ctf_team = CTF_TEAM2; + } +} + +/* + * go to a ctf point, but NOT the two + * points closest to other players + */ +edict_t * +SelectCTFSpawnPoint(edict_t *ent) +{ + edict_t *spot, *spot1, *spot2; + int count = 0; + int selection; + float range, range1, range2; + char *cname; + + if (ent->client->resp.ctf_state) + { + if ((int)(dmflags->value) & DF_SPAWN_FARTHEST) + { + return SelectFarthestDeathmatchSpawnPoint(); + } + else + { + return SelectRandomDeathmatchSpawnPoint(); + } + } + + ent->client->resp.ctf_state++; + + switch (ent->client->resp.ctf_team) + { + case CTF_TEAM1: + cname = "info_player_team1"; + break; + case CTF_TEAM2: + cname = "info_player_team2"; + break; + default: + return SelectRandomDeathmatchSpawnPoint(); + } + + spot = NULL; + range1 = range2 = 99999; + spot1 = spot2 = NULL; + + while ((spot = G_Find(spot, FOFS(classname), cname)) != NULL) + { + count++; + range = PlayersRangeFromSpot(spot); + + if (range < range1) + { + range1 = range; + spot1 = spot; + } + else if (range < range2) + { + range2 = range; + spot2 = spot; + } + } + + if (!count) + { + return SelectRandomDeathmatchSpawnPoint(); + } + + if (count <= 2) + { + spot1 = spot2 = NULL; + } + else + { + count -= 2; + } + + selection = rand() % count; + + spot = NULL; + + do + { + spot = G_Find(spot, FOFS(classname), cname); + + if ((spot == spot1) || (spot == spot2)) + { + selection++; + } + } + while (selection--); + + return spot; +} + +/*------------------------------------------------------------------------*/ + +/* + * 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) +{ + int i; + edict_t *ent; + gitem_t *flag_item, *enemy_flag_item; + int otherteam; + edict_t *flag, *carrier; + 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 = flag1_item; + enemy_flag_item = flag2_item; + } + else + { + flag_item = flag2_item; + enemy_flag_item = flag1_item; + } + + /* did the attacker frag the flag carrier? */ + if (targ->client->pers.inventory[ITEM_INDEX(enemy_flag_item)]) + { + attacker->client->resp.ctf_lastfraggedcarrier = level.time; + attacker->client->resp.score += CTF_FRAG_CARRIER_BONUS; + gi.cprintf(attacker, PRINT_MEDIUM, + "BONUS: %d points for fragging enemy flag carrier.\n", + CTF_FRAG_CARRIER_BONUS); + + /* the target had the flag, clear the hurt + * carrier field on the other team */ + for (i = 1; i <= maxclients->value; i++) + { + ent = g_edicts + i; + + if (ent->inuse && (ent->client->resp.ctf_team == otherteam)) + { + ent->client->resp.ctf_lasthurtcarrier = 0; + } + } + + return; + } + + if (targ->client->resp.ctf_lasthurtcarrier && + (level.time - targ->client->resp.ctf_lasthurtcarrier < + CTF_CARRIER_DANGER_PROTECT_TIMEOUT) && + !attacker->client->pers.inventory[ITEM_INDEX(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.bprintf(PRINT_MEDIUM, + "%s defends %s's flag carrier against an agressive enemy\n", + 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 = NULL; + + while ((flag = G_Find(flag, FOFS(classname), c)) != NULL) + { + if (!(flag->spawnflags & DROPPED_ITEM)) + { + break; + } + } + + if (!flag) + { + return; /* can't find attacker's flag */ + } + + carrier = NULL; + + /* find attacker's team's flag carrier */ + for (i = 1; i <= maxclients->value; i++) + { + carrier = g_edicts + i; + + if (carrier->inuse && + carrier->client->pers.inventory[ITEM_INDEX(flag_item)]) + { + break; + } + + carrier = NULL; + } + + /* ok we have the attackers flag and a pointer to the carrier */ + + /* check to see if we are defending the base's flag */ + VectorSubtract(targ->s.origin, flag->s.origin, v1); + VectorSubtract(attacker->s.origin, flag->s.origin, v2); + + if (((VectorLength(v1) < CTF_TARGET_PROTECT_RADIUS) || + (VectorLength(v2) < 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.bprintf(PRINT_MEDIUM, "%s defends the %s base.\n", + attacker->client->pers.netname, + CTFTeamName(attacker->client->resp.ctf_team)); + } + else + { + gi.bprintf(PRINT_MEDIUM, "%s defends the %s flag.\n", + 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)) + { + VectorSubtract(targ->s.origin, carrier->s.origin, v1); + VectorSubtract(attacker->s.origin, carrier->s.origin, v1); + + if ((VectorLength(v1) < CTF_ATTACKER_PROTECT_RADIUS) || + (VectorLength(v2) < CTF_ATTACKER_PROTECT_RADIUS) || + loc_CanSee(carrier, targ) || loc_CanSee(carrier, attacker)) + { + attacker->client->resp.score += CTF_CARRIER_PROTECT_BONUS; + gi.bprintf(PRINT_MEDIUM, "%s defends the %s's flag carrier.\n", + 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) +{ + gitem_t *flag_item; + + if (!targ->client || !attacker->client) + { + return; + } + + if (targ->client->resp.ctf_team == CTF_TEAM1) + { + flag_item = flag2_item; + } + else + { + flag_item = flag1_item; + } + + if (targ->client->pers.inventory[ITEM_INDEX(flag_item)] && + (targ->client->resp.ctf_team != attacker->client->resp.ctf_team)) + { + attacker->client->resp.ctf_lasthurtcarrier = level.time; + } +} + +/*------------------------------------------------------------------------*/ + +void +CTFResetFlag(int ctf_team) +{ + 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 = NULL; + + while ((ent = G_Find(ent, FOFS(classname), c)) != NULL) + { + if (ent->spawnflags & DROPPED_ITEM) + { + G_FreeEdict(ent); + } + else + { + ent->svflags &= ~SVF_NOCLIENT; + ent->solid = SOLID_TRIGGER; + gi.linkentity(ent); + ent->s.event = EV_ITEM_RESPAWN; + } + } +} + +void +CTFResetFlags(void) +{ + CTFResetFlag(CTF_TEAM1); + CTFResetFlag(CTF_TEAM2); +} + +qboolean +CTFPickup_Flag(edict_t *ent, edict_t *other) +{ + int ctf_team; + int i; + edict_t *player; + gitem_t *flag_item, *enemy_flag_item; + + /* figure out what team this flag is */ + if (strcmp(ent->classname, "item_flag_team1") == 0) + { + ctf_team = CTF_TEAM1; + } + else if (strcmp(ent->classname, "item_flag_team2") == 0) + { + ctf_team = CTF_TEAM2; + } + else + { + gi.cprintf(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 = flag1_item; + enemy_flag_item = flag2_item; + } + else + { + flag_item = flag2_item; + enemy_flag_item = flag1_item; + } + + if (ctf_team == other->client->resp.ctf_team) + { + if (!(ent->spawnflags & DROPPED_ITEM)) + { + /* the flag is at home base. if the player has + * the enemy flag, he's just won! */ + if (other->client->pers.inventory[ITEM_INDEX(enemy_flag_item)]) + { + gi.bprintf(PRINT_HIGH, "%s captured the %s flag!\n", + other->client->pers.netname, CTFOtherTeamName(ctf_team)); + other->client->pers.inventory[ITEM_INDEX(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_VOICE, + 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 (i = 1; i <= maxclients->value; 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; + } + 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 + + CTF_RETURN_FLAG_ASSIST_TIMEOUT > level.time) + { + gi.bprintf( PRINT_HIGH, + "%s gets an assist for returning the flag!\n", + player->client->pers.netname); + player->client->resp.score += + CTF_RETURN_FLAG_ASSIST_BONUS; + } + + if (player->client->resp.ctf_lastfraggedcarrier + + CTF_FRAG_CARRIER_ASSIST_TIMEOUT > level.time) + { + gi.bprintf( PRINT_HIGH, + "%s gets an assist for fragging the flag carrier!\n", + 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.bprintf(PRINT_HIGH, "%s returned the %s flag!\n", + 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_VOICE, + 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.bprintf(PRINT_HIGH, "%s got the %s flag!\n", + other->client->pers.netname, CTFTeamName(ctf_team)); + other->client->resp.score += CTF_FLAG_BONUS; + + other->client->pers.inventory[ITEM_INDEX(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 & DROPPED_ITEM)) + { + ent->flags |= FL_RESPAWN; + ent->svflags |= SVF_NOCLIENT; + ent->solid = SOLID_NOT; + } + + return true; +} + +static void +CTFDropFlagTouch(edict_t *ent, edict_t *other, cplane_t *plane, + csurface_t *surf) +{ + /* owner (who dropped us) can't touch for two secs */ + if ((other == ent->owner) && + (ent->nextthink - level.time > CTF_AUTO_FLAG_RETURN_TIMEOUT - 2)) + { + return; + } + + Touch_Item(ent, other, plane, surf); +} + +static void +CTFDropFlagThink(edict_t *ent) +{ + /* auto return the flag + reset flag will remove ourselves */ + if (strcmp(ent->classname, "item_flag_team1") == 0) + { + CTFResetFlag(CTF_TEAM1); + gi.bprintf(PRINT_HIGH, "The %s flag has returned!\n", + CTFTeamName(CTF_TEAM1)); + } + else if (strcmp(ent->classname, "item_flag_team2") == 0) + { + CTFResetFlag(CTF_TEAM2); + gi.bprintf(PRINT_HIGH, "The %s flag has returned!\n", + CTFTeamName(CTF_TEAM2)); + } +} + +/* + * Called from PlayerDie, + * to drop the flag from + * a dying player + */ +void +CTFDeadDropFlag(edict_t *self) +{ + edict_t *dropped = NULL; + + if (self->client->pers.inventory[ITEM_INDEX(flag1_item)]) + { + dropped = Drop_Item(self, flag1_item); + self->client->pers.inventory[ITEM_INDEX(flag1_item)] = 0; + gi.bprintf(PRINT_HIGH, "%s lost the %s flag!\n", + self->client->pers.netname, CTFTeamName(CTF_TEAM1)); + } + else if (self->client->pers.inventory[ITEM_INDEX(flag2_item)]) + { + dropped = Drop_Item(self, flag2_item); + self->client->pers.inventory[ITEM_INDEX(flag2_item)] = 0; + gi.bprintf(PRINT_HIGH, "%s lost the %s flag!\n", + 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 (rand() & 1) + { + gi.cprintf(ent, PRINT_HIGH, "Only lusers drop flags.\n"); + } + else + { + gi.cprintf(ent, PRINT_HIGH, "Winners don't drop flags.\n"); + } +} + +static void +CTFFlagThink(edict_t *ent) +{ + if (ent->solid != SOLID_NOT) + { + ent->s.frame = 173 + (((ent->s.frame - 173) + 1) % 16); + } + + ent->nextthink = level.time + FRAMETIME; +} + +void +CTFFlagSetup(edict_t *ent) +{ + trace_t tr; + vec3_t dest; + float *v; + + v = tv(-15, -15, -15); + VectorCopy(v, ent->mins); + v = tv(15, 15, 15); + VectorCopy(v, ent->maxs); + + 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; + + v = tv(0, 0, -128); + VectorAdd(ent->s.origin, v, dest); + + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, dest, ent, MASK_SOLID); + + if (tr.startsolid) + { + gi.dprintf("CTFFlagSetup: %s startsolid at %s\n", ent->classname, + vtos(ent->s.origin)); + G_FreeEdict(ent); + return; + } + + VectorCopy(tr.endpos, ent->s.origin); + + gi.linkentity(ent); + + ent->nextthink = level.time + FRAMETIME; + ent->think = CTFFlagThink; +} + +void +CTFEffects(edict_t *player) +{ + player->s.effects &= ~(EF_FLAG1 | EF_FLAG2); + + if (player->health > 0) + { + if (player->client->pers.inventory[ITEM_INDEX(flag1_item)]) + { + player->s.effects |= EF_FLAG1; + } + + if (player->client->pers.inventory[ITEM_INDEX(flag2_item)]) + { + player->s.effects |= EF_FLAG2; + } + } + + if (player->client->pers.inventory[ITEM_INDEX(flag1_item)]) + { + player->s.modelindex3 = gi.modelindex("players/male/flag1.md2"); + } + else if (player->client->pers.inventory[ITEM_INDEX(flag2_item)]) + { + player->s.modelindex3 = gi.modelindex("players/male/flag2.md2"); + } + else + { + player->s.modelindex3 = 0; + } +} + +/* + * Called when we enter the intermission + */ +void +CTFCalcScores(void) +{ + int i; + + ctfgame.total1 = ctfgame.total2 = 0; + + for (i = 0; i < maxclients->value; 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; + } + } +} + +void +CTFID_f(edict_t *ent) +{ + if (ent->client->resp.id_state) + { + gi.cprintf(ent, PRINT_HIGH, "Disabling player identication display.\n"); + ent->client->resp.id_state = false; + } + else + { + gi.cprintf(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; + int i; + + /* only check every few frames */ + if (level.time - ent->client->resp.lastidtime < 0.25) + { + 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, NULL, NULL); + VectorScale(forward, 1024, forward); + VectorAdd(ent->s.origin, forward, forward); + tr = gi.trace(ent->s.origin, NULL, NULL, forward, ent, MASK_SOLID); + + if ((tr.fraction < 1) && tr.ent && tr.ent->client) + { + ent->client->ps.stats[STAT_CTF_ID_VIEW] = + CS_GENERAL + (tr.ent - g_edicts - 1); + + 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, NULL, NULL); + best = NULL; + + for (i = 1; i <= maxclients->value; i++) + { + who = g_edicts + i; + + if (!who->inuse || (who->solid == SOLID_NOT)) + { + continue; + } + + VectorSubtract(who->s.origin, ent->s.origin, dir); + VectorNormalize(dir); + d = DotProduct(forward, dir); + + if ((d > bd) && loc_CanSee(ent, who)) + { + bd = d; + best = who; + } + } + + if (bd > 0.90) + { + ent->client->ps.stats[STAT_CTF_ID_VIEW] = + CS_GENERAL + (best - g_edicts - 1); + + 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) +{ + gitem_t *tech; + int 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; + strcpy(ent->client->resp.ghost->netname, ent->client->pers.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; + + /* if during intermission, we must blink the team header of the winning team */ + if (level.intermissiontime && (level.framenum & 8)) /* blink 1/8th 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; + + while (tnames[i]) + { + if (((tech = FindItemByClassname(tnames[i])) != NULL) && + ent->client->pers.inventory[ITEM_INDEX(tech)]) + { + ent->client->ps.stats[STAT_CTF_TECH] = gi.imageindex(tech->icon); + break; + } + + i++; + } + + /* figure out what icon to display for team logos + three states: + flag at base + flag taken + flag dropped */ + p1 = imageindex_i_ctf1; + e = G_Find(NULL, FOFS(classname), "item_flag_team1"); + + if (e != NULL) + { + if (e->solid == SOLID_NOT) + { + int i; + + /* not at base + check if on player */ + p1 = imageindex_i_ctf1d; /* default to dropped */ + + for (i = 1; i <= maxclients->value; i++) + { + if (g_edicts[i].inuse && + g_edicts[i].client->pers.inventory[ITEM_INDEX(flag1_item)]) + { + /* enemy has it */ + p1 = imageindex_i_ctf1t; + break; + } + } + } + else if (e->spawnflags & DROPPED_ITEM) + { + p1 = imageindex_i_ctf1d; /* must be dropped */ + } + } + + p2 = imageindex_i_ctf2; + e = G_Find(NULL, FOFS(classname), "item_flag_team2"); + + if (e != NULL) + { + if (e->solid == SOLID_NOT) + { + int i; + + /* not at base + check if on player */ + p2 = imageindex_i_ctf2d; /* default to dropped */ + + for (i = 1; i <= maxclients->value; i++) + { + if (g_edicts[i].inuse && + g_edicts[i].client->pers.inventory[ITEM_INDEX(flag2_item)]) + { + /* enemy has it */ + p2 = imageindex_i_ctf2t; + break; + } + } + } + else if (e->spawnflags & DROPPED_ITEM) + { + 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)) + { + if (ctfgame.last_capture_team == CTF_TEAM1) + { + if (level.framenum & 8) + { + ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = p1; + } + else + { + ent->client->ps.stats[STAT_CTF_TEAM1_PIC] = 0; + } + } + else + { + if (level.framenum & 8) + { + 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[ITEM_INDEX(flag2_item)] && + (level.framenum & 8)) + { + ent->client->ps.stats[STAT_CTF_FLAG_PIC] = imageindex_i_ctf2; + } + + else if ((ent->client->resp.ctf_team == CTF_TEAM2) && + ent->client->pers.inventory[ITEM_INDEX(flag1_item)] && + (level.framenum & 8)) + { + ent->client->ps.stats[STAT_CTF_FLAG_PIC] = imageindex_i_ctf1; + } + + 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) + { + float volume = 1.0; + gclient_t *cl; + + if (self->owner->client->silencer_shots) + { + volume = 0.2; + } + + gi.sound(self->owner, CHAN_RELIABLE + CHAN_WEAPON, + gi.soundindex( + "weapons/grapple/grreset.wav"), volume, ATTN_NORM, 0); + cl = self->owner->client; + cl->ctf_grapple = NULL; + cl->ctf_grapplereleasetime = level.time; + cl->ctf_grapplestate = CTF_GRAPPLE_STATE_FLY; /* we're firing, not on hook */ + cl->ps.pmove.pm_flags &= ~PMF_NO_PREDICTION; + G_FreeEdict(self); + } +} + +void +CTFGrappleTouch(edict_t *self, edict_t *other, cplane_t *plane, + csurface_t *surf) +{ + float volume = 1.0; + + if (other == self->owner) + { + return; + } + + if (self->owner->client->ctf_grapplestate != CTF_GRAPPLE_STATE_FLY) + { + return; + } + + if (surf && (surf->flags & SURF_SKY)) + { + CTFResetGrapple(self); + return; + } + + VectorCopy(vec3_origin, self->velocity); + + PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT); + + if (other->takedamage) + { + T_Damage(other, self, self->owner, self->velocity, self->s.origin, + plane->normal, self->dmg, 1, 0, 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.2; + } + + gi.sound(self->owner, CHAN_RELIABLE + CHAN_WEAPON, + gi.soundindex("weapons/grapple/grpull.wav"), volume, ATTN_NORM, 0); + gi.sound(self, CHAN_WEAPON, gi.soundindex( + "weapons/grapple/grhit.wav"), volume, ATTN_NORM, 0); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_SPARKS); + gi.WritePosition(self->s.origin); + + if (!plane) + { + gi.WriteDir(vec3_origin); + } + else + { + gi.WriteDir(plane->normal); + } + + gi.multicast(self->s.origin, MULTICAST_PVS); +} + +/* + * draw beam between grapple and self + */ +void +CTFGrappleDrawCable(edict_t *self) +{ + vec3_t offset, start, end, f, r; + vec3_t dir; + float distance; + + AngleVectors(self->owner->client->v_angle, f, r, NULL); + VectorSet(offset, 16, 16, self->owner->viewheight - 8); + P_ProjectSource(self->owner->client, self->owner->s.origin, + offset, f, r, start); + + VectorSubtract(start, self->owner->s.origin, offset); + + VectorSubtract(start, self->s.origin, dir); + distance = VectorLength(dir); + + /* don't draw cable if close */ + if (distance < 64) + { + return; + } + + /* adjust start for beam origin being in middle of a segment */ + VectorCopy(self->s.origin, end); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_GRAPPLE_CABLE); + gi.WriteShort(self->owner - g_edicts); + gi.WritePosition(self->owner->s.origin); + gi.WritePosition(end); + gi.WritePosition(offset); + gi.multicast(self->s.origin, MULTICAST_PVS); +} + +void SV_AddGravity(edict_t *ent); + +/* + * pull the player toward the grapple + */ +void +CTFGrapplePull(edict_t *self) +{ + vec3_t hookdir, v; + float vlen; + + if ((strcmp(self->owner->client->pers.weapon->classname, + "weapon_grapple") == 0) && + !self->owner->client->newweapon && + (self->owner->client->weaponstate != WEAPON_FIRING) && + (self->owner->client->weaponstate != WEAPON_ACTIVATING)) + { + CTFResetGrapple(self); + return; + } + + if (self->enemy) + { + if (self->enemy->solid == SOLID_NOT) + { + CTFResetGrapple(self); + return; + } + + if (self->enemy->solid == SOLID_BBOX) + { + VectorScale(self->enemy->size, 0.5, v); + VectorAdd(v, self->enemy->s.origin, v); + VectorAdd(v, self->enemy->mins, self->s.origin); + gi.linkentity(self); + } + else + { + VectorCopy(self->enemy->velocity, self->velocity); + } + + if (self->enemy->takedamage && + !CheckTeamDamage(self->enemy, self->owner)) + { + float volume = 1.0; + + if (self->owner->client->silencer_shots) + { + volume = 0.2; + } + + T_Damage(self->enemy, self, self->owner, self->velocity, + self->s.origin, vec3_origin, 1, 1, 0, MOD_GRAPPLE); + gi.sound(self, CHAN_WEAPON, gi.soundindex( + "weapons/grapple/grhurt.wav"), volume, ATTN_NORM, 0); + } + + 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 + this causes icky stuff with prediction, we need to extend + the prediction layer to include two new fields in the player + move stuff: a point and a velocity. The client should add + that velociy in the direction of the point */ + vec3_t forward, up; + + AngleVectors(self->owner->client->v_angle, forward, NULL, up); + VectorCopy(self->owner->s.origin, v); + v[2] += self->owner->viewheight; + VectorSubtract(self->s.origin, v, hookdir); + + vlen = VectorLength(hookdir); + + if ((self->owner->client->ctf_grapplestate == + CTF_GRAPPLE_STATE_PULL) && (vlen < 64)) + { + float volume = 1.0; + + if (self->owner->client->silencer_shots) + { + volume = 0.2; + } + + self->owner->client->ps.pmove.pm_flags |= PMF_NO_PREDICTION; + gi.sound(self->owner, CHAN_RELIABLE + CHAN_WEAPON, + gi.soundindex("weapons/grapple/grhang.wav"), volume, ATTN_NORM, 0); + self->owner->client->ctf_grapplestate = CTF_GRAPPLE_STATE_HANG; + } + + VectorNormalize(hookdir); + VectorScale(hookdir, CTF_GRAPPLE_PULL_SPEED, hookdir); + VectorCopy(hookdir, self->owner->velocity); + SV_AddGravity(self->owner); + } +} + +void +CTFFireGrapple(edict_t *self, vec3_t start, vec3_t dir, + int damage, int speed, int effect) +{ + edict_t *grapple; + trace_t tr; + + VectorNormalize(dir); + + grapple = G_Spawn(); + VectorCopy(start, grapple->s.origin); + VectorCopy(start, grapple->s.old_origin); + vectoangles(dir, grapple->s.angles); + VectorScale(dir, speed, grapple->velocity); + grapple->movetype = MOVETYPE_FLYMISSILE; + grapple->clipmask = MASK_SHOT; + grapple->solid = SOLID_BBOX; + grapple->s.effects |= effect; + VectorClear(grapple->mins); + VectorClear(grapple->maxs); + grapple->s.modelindex = gi.modelindex( + "models/weapons/grapple/hook/tris.md2"); + grapple->owner = self; + grapple->touch = CTFGrappleTouch; + grapple->dmg = damage; + self->client->ctf_grapple = grapple; + self->client->ctf_grapplestate = CTF_GRAPPLE_STATE_FLY; /* we're firing, not on hook */ + gi.linkentity(grapple); + + tr = gi.trace(self->s.origin, NULL, NULL, grapple->s.origin, + grapple, MASK_SHOT); + + if (tr.fraction < 1.0) + { + VectorMA(grapple->s.origin, -10, dir, grapple->s.origin); + grapple->touch(grapple, tr.ent, NULL, NULL); + } +} + +void +CTFGrappleFire(edict_t *ent, vec3_t g_offset, int damage, int effect) +{ + vec3_t forward, right; + vec3_t start; + vec3_t offset; + float volume = 1.0; + + if (ent->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY) + { + return; /* it's already out */ + } + + AngleVectors(ent->client->v_angle, forward, right, NULL); + VectorSet(offset, 24, 8, ent->viewheight - 8 + 2); + VectorAdd(offset, g_offset, offset); + P_ProjectSource(ent->client, ent->s.origin, offset, forward, right, start); + + VectorScale(forward, -2, ent->client->kick_origin); + ent->client->kick_angles[0] = -1; + + if (ent->client->silencer_shots) + { + volume = 0.2; + } + + gi.sound(ent, CHAN_RELIABLE + CHAN_WEAPON, + gi.soundindex("weapons/grapple/grfire.wav"), volume, ATTN_NORM, 0); + CTFFireGrapple(ent, start, forward, damage, CTF_GRAPPLE_SPEED, effect); + PlayerNoise(ent, start, PNOISE_WEAPON); +} + +void +CTFWeapon_Grapple_Fire(edict_t *ent) +{ + int damage; + + damage = 10; + CTFGrappleFire(ent, vec3_origin, damage, 0); + ent->client->ps.gunframe++; +} + +void +CTFWeapon_Grapple(edict_t *ent) +{ + static int pause_frames[] = {10, 18, 27, 0}; + static 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) && + (ent->client->weaponstate == WEAPON_FIRING) && + ent->client->ctf_grapple) + { + ent->client->ps.gunframe = 9; + } + + if (!(ent->client->buttons & BUTTON_ATTACK) && + 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->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY) && + (ent->client->weaponstate == WEAPON_FIRING)) + { + /* he wants to change weapons while grappled */ + ent->client->weaponstate = WEAPON_DROPPING; + ent->client->ps.gunframe = 32; + } + + prevstate = ent->client->weaponstate; + Weapon_Generic(ent, 5, 9, 31, 36, pause_frames, fire_frames, + CTFWeapon_Grapple_Fire); + + /* 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)) + { + ent->client->ps.gunframe = 9; + } + else + { + ent->client->ps.gunframe = 5; + } + + ent->client->weaponstate = WEAPON_FIRING; + } +} + +void +CTFTeam_f(edict_t *ent) +{ + char *t, *s; + int desired_team; + + t = gi.args(); + + if (!*t) + { + gi.cprintf(ent, PRINT_HIGH, "You are on the %s team.\n", + CTFTeamName(ent->client->resp.ctf_team)); + return; + } + + if (ctfgame.match > MATCH_SETUP) + { + gi.cprintf(ent, PRINT_HIGH, "Can't change teams in a match.\n"); + return; + } + + if (Q_stricmp(t, "red") == 0) + { + desired_team = CTF_TEAM1; + } + else if (Q_stricmp(t, "blue") == 0) + { + desired_team = CTF_TEAM2; + } + else + { + gi.cprintf(ent, PRINT_HIGH, "Unknown team %s.\n", t); + return; + } + + if (ent->client->resp.ctf_team == desired_team) + { + gi.cprintf(ent, PRINT_HIGH, "You are already on the %s team.\n", + CTFTeamName(ent->client->resp.ctf_team)); + return; + } + + ent->svflags = 0; + ent->flags &= ~FL_GODMODE; + ent->client->resp.ctf_team = desired_team; + ent->client->resp.ctf_state = 0; + s = Info_ValueForKey(ent->client->pers.userinfo, "skin"); + CTFAssignSkin(ent, s); + + if (ent->solid == SOLID_NOT) /* spectator */ + { + PutClientInServer(ent); + /* add a teleportation effect */ + ent->s.event = EV_PLAYER_TELEPORT; + /* hold in place briefly */ + ent->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT; + ent->client->ps.pmove.pm_time = 14; + gi.bprintf(PRINT_HIGH, "%s joined the %s team.\n", + ent->client->pers.netname, CTFTeamName(desired_team)); + return; + } + + ent->health = 0; + player_die(ent, ent, ent, 100000, vec3_origin); + /* don't even bother waiting for death frames */ + ent->deadflag = DEAD_DEAD; + respawn(ent); + + ent->client->resp.score = 0; + + gi.bprintf(PRINT_HIGH, "%s changed to the %s team.\n", + ent->client->pers.netname, CTFTeamName(desired_team)); +} + +void +CTFScoreboardMessage(edict_t *ent, edict_t *killer) +{ + char entry[1024]; + char string[1400]; + int len; + int i, j, k, n; + int sorted[2][MAX_CLIENTS]; + int sortedscores[2][MAX_CLIENTS]; + int score, total[2], totalscore[2]; + int last[2]; + gclient_t *cl; + edict_t *cl_ent; + int team; + int maxsize = 1000; + + /* 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 */ + *string = 0; + len = 0; + + /* team one */ + sprintf(string, "if 24 xv 8 yv 8 pic 24 endif " + "xv 40 yv 28 string \"%4d/%-3d\" " + "xv 98 yv 12 num 2 18 " + "if 25 xv 168 yv 8 pic 25 endif " + "xv 200 yv 28 string \"%4d/%-3d\" " + "xv 256 yv 12 num 2 20 ", + totalscore[0], total[0], + totalscore[1], total[1]); + len = strlen(string); + + for (i = 0; i < 16; i++) + { + if ((i >= total[0]) && (i >= total[1])) + { + break; /* we're done */ + } + + *entry = 0; + + /* left side */ + if (i < total[0]) + { + cl = &game.clients[sorted[0][i]]; + cl_ent = g_edicts + 1 + sorted[0][i]; + + sprintf(entry + strlen(entry), "ctf 0 %d %d %d %d ", + 42 + i * 8, sorted[0][i], cl->resp.score, + cl->ping > 999 ? 999 : cl->ping); + + if (cl_ent->client->pers.inventory[ITEM_INDEX(flag2_item)]) + { + sprintf(entry + strlen(entry), "xv 56 yv %d picn sbfctf2 ", + 42 + i * 8); + } + + if (maxsize - len > strlen(entry)) + { + strcat(string, entry); + len = strlen(string); + last[0] = i; + } + } + + /* right side */ + if (i < total[1]) + { + cl = &game.clients[sorted[1][i]]; + cl_ent = g_edicts + 1 + sorted[1][i]; + + sprintf(entry + strlen(entry), "ctf 160 %d %d %d %d ", + 42 + i * 8, sorted[1][i], cl->resp.score, + cl->ping > 999 ? 999 : cl->ping); + + if (cl_ent->client->pers.inventory[ITEM_INDEX(flag1_item)]) + { + sprintf(entry + strlen(entry), "xv 216 yv %d picn sbfctf1 ", + 42 + i * 8); + } + + if (maxsize - len > strlen(entry)) + { + strcat(string, entry); + len = strlen(string); + 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 (maxsize - len > 50) + { + for (i = 0; i < maxclients->value; 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; + sprintf(entry, "xv 0 yv %d string2 \"Spectators\" ", j); + strcat(string, entry); + len = strlen(string); + j += 8; + } + + sprintf(entry + strlen(entry), "ctf %d %d %d %d %d ", + (n & 1) ? 160 : 0, /* x */ j, /* y */ + i, /* playernum */ cl->resp.score, + cl->ping > 999 ? 999 : cl->ping); + + if (maxsize - len > strlen(entry)) + { + strcat(string, entry); + len = strlen(string); + } + + if (n & 1) + { + j += 8; + } + + n++; + } + } + + if (total[0] - last[0] > 1) /* couldn't fit everyone */ + { + sprintf(string + strlen(string), "xv 8 yv %d string \"..and %d more\" ", + 42 + (last[0] + 1) * 8, total[0] - last[0] - 1); + } + + if (total[1] - last[1] > 1) /* couldn't fit everyone */ + { + sprintf(string + strlen( + string), "xv 168 yv %d string \"..and %d more\" ", + 42 + (last[1] + 1) * 8, total[1] - last[1] - 1); + } + + gi.WriteByte(svc_layout); + gi.WriteString(string); +} + +/*------------------------------------------------------------------------*/ +/* TECH */ +/*------------------------------------------------------------------------*/ + +void +CTFHasTech(edict_t *who) +{ + if (level.time - who->client->ctf_lasttechmsg > 2) + { + gi.centerprintf(who, "You already have a TECH powerup."); + who->client->ctf_lasttechmsg = level.time; + } +} + +gitem_t * +CTFWhat_Tech(edict_t *ent) +{ + gitem_t *tech; + int i; + + i = 0; + + while (tnames[i]) + { + if (((tech = FindItemByClassname(tnames[i])) != NULL) && + ent->client->pers.inventory[ITEM_INDEX(tech)]) + { + return tech; + } + + i++; + } + + return NULL; +} + +qboolean +CTFPickup_Tech(edict_t *ent, edict_t *other) +{ + gitem_t *tech; + int i; + + i = 0; + + while (tnames[i]) + { + if (((tech = FindItemByClassname(tnames[i])) != NULL) && + other->client->pers.inventory[ITEM_INDEX(tech)]) + { + CTFHasTech(other); + return false; /* has this one */ + } + + i++; + } + + /* client only gets one tech */ + other->client->pers.inventory[ITEM_INDEX(ent->item)]++; + other->client->ctf_regentime = level.time; + return true; +} + +static void SpawnTech(gitem_t *item, edict_t *spot); + +static edict_t * +FindTechSpawn(void) +{ + edict_t *spot = NULL; + int i = rand() % 16; + + while (i--) + { + spot = G_Find(spot, FOFS(classname), "info_player_deathmatch"); + } + + if (!spot) + { + spot = G_Find(spot, FOFS(classname), "info_player_deathmatch"); + } + + return spot; +} + +static void +TechThink(edict_t *tech) +{ + edict_t *spot; + + if ((spot = FindTechSpawn()) != NULL) + { + 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_INDEX(item)] = 0; +} + +void +CTFDeadDropTech(edict_t *ent) +{ + gitem_t *tech; + edict_t *dropped; + int i; + + i = 0; + + while (tnames[i]) + { + if (((tech = FindItemByClassname(tnames[i])) != NULL) && + ent->client->pers.inventory[ITEM_INDEX(tech)]) + { + dropped = Drop_Item(ent, tech); + + /* hack the velocity to make it bounce random */ + dropped->velocity[0] = (rand() % 600) - 300; + dropped->velocity[1] = (rand() % 600) - 300; + dropped->nextthink = level.time + CTF_TECH_TIMEOUT; + dropped->think = TechThink; + dropped->owner = NULL; + ent->client->pers.inventory[ITEM_INDEX(tech)] = 0; + } + + i++; + } +} + +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 = DROPPED_ITEM; + ent->s.effects = item->world_model_flags; + ent->s.renderfx = RF_GLOW; + VectorSet(ent->mins, -15, -15, -15); + VectorSet(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] = rand() % 360; + angles[2] = 0; + + AngleVectors(angles, forward, right, NULL); + VectorCopy(spot->s.origin, ent->s.origin); + ent->s.origin[2] += 16; + VectorScale(forward, 100, ent->velocity); + ent->velocity[2] = 300; + + ent->nextthink = level.time + CTF_TECH_TIMEOUT; + ent->think = TechThink; + + gi.linkentity(ent); +} + +static void +SpawnTechs(edict_t *ent) +{ + gitem_t *tech; + edict_t *spot; + int i; + + i = 0; + + while (tnames[i]) + { + if (((tech = FindItemByClassname(tnames[i])) != NULL) && + ((spot = FindTechSpawn()) != NULL)) + { + SpawnTech(tech, spot); + } + + i++; + } + + if (ent) + { + G_FreeEdict(ent); + } +} + +/* + * Frees the passed edict! + */ +void +CTFRespawnTech(edict_t *ent) +{ + edict_t *spot; + + if ((spot = FindTechSpawn()) != NULL) + { + SpawnTech(ent->item, spot); + } + + G_FreeEdict(ent); +} + +void +CTFSetupTechSpawn(void) +{ + edict_t *ent; + + if (((int)dmflags->value & DF_CTF_NO_TECH)) + { + return; + } + + ent = G_Spawn(); + ent->nextthink = level.time + 2; + ent->think = SpawnTechs; +} + +void +CTFResetTech(void) +{ + edict_t *ent; + int i; + + for (ent = g_edicts + 1, i = 1; i < globals.num_edicts; i++, ent++) + { + if (ent->inuse) + { + if (ent->item && (ent->item->flags & IT_TECH)) + { + G_FreeEdict(ent); + } + } + } + + SpawnTechs(NULL); +} + +int +CTFApplyResistance(edict_t *ent, int dmg) +{ + static gitem_t *tech = NULL; + float volume = 1.0; + + if (ent->client && ent->client->silencer_shots) + { + volume = 0.2; + } + + if (!tech) + { + tech = FindItemByClassname("item_tech1"); + } + + if (dmg && tech && ent->client && + ent->client->pers.inventory[ITEM_INDEX(tech)]) + { + /* make noise */ + gi.sound(ent, CHAN_VOICE, gi.soundindex( + "ctf/tech1.wav"), volume, ATTN_NORM, 0); + return dmg / 2; + } + + return dmg; +} + +int +CTFApplyStrength(edict_t *ent, int dmg) +{ + static gitem_t *tech = NULL; + + if (!tech) + { + tech = FindItemByClassname("item_tech2"); + } + + if (dmg && tech && ent->client && + ent->client->pers.inventory[ITEM_INDEX(tech)]) + { + return dmg * 2; + } + + return dmg; +} + +qboolean +CTFApplyStrengthSound(edict_t *ent) +{ + static gitem_t *tech = NULL; + float volume = 1.0; + + if (ent->client && ent->client->silencer_shots) + { + volume = 0.2; + } + + if (!tech) + { + tech = FindItemByClassname("item_tech2"); + } + + if (tech && ent->client && + ent->client->pers.inventory[ITEM_INDEX(tech)]) + { + if (ent->client->ctf_techsndtime < level.time) + { + ent->client->ctf_techsndtime = level.time + 1; + + if (ent->client->quad_framenum > level.framenum) + { + gi.sound(ent, CHAN_VOICE, gi.soundindex( + "ctf/tech2x.wav"), volume, ATTN_NORM, 0); + } + else + { + gi.sound(ent, CHAN_VOICE, gi.soundindex( + "ctf/tech2.wav"), volume, ATTN_NORM, 0); + } + } + + return true; + } + + return false; +} + +qboolean +CTFApplyHaste(edict_t *ent) +{ + static gitem_t *tech = NULL; + + if (!tech) + { + tech = FindItemByClassname("item_tech3"); + } + + if (tech && ent->client && + ent->client->pers.inventory[ITEM_INDEX(tech)]) + { + return true; + } + + return false; +} + +void +CTFApplyHasteSound(edict_t *ent) +{ + static gitem_t *tech = NULL; + float volume = 1.0; + + if (ent->client && ent->client->silencer_shots) + { + volume = 0.2; + } + + if (!tech) + { + tech = FindItemByClassname("item_tech3"); + } + + if (tech && ent->client && + ent->client->pers.inventory[ITEM_INDEX(tech)] && + (ent->client->ctf_techsndtime < level.time)) + { + ent->client->ctf_techsndtime = level.time + 1; + gi.sound(ent, CHAN_VOICE, gi.soundindex( + "ctf/tech3.wav"), volume, ATTN_NORM, 0); + } +} + +void +CTFApplyRegeneration(edict_t *ent) +{ + static gitem_t *tech = NULL; + qboolean noise = false; + gclient_t *client; + int index; + float volume = 1.0; + + client = ent->client; + + if (!client) + { + return; + } + + if (ent->client->silencer_shots) + { + volume = 0.2; + } + + if (!tech) + { + tech = FindItemByClassname("item_tech4"); + } + + if (tech && client->pers.inventory[ITEM_INDEX(tech)]) + { + 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 += 0.5; + 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 += 0.5; + noise = true; + } + } + + if (noise && (ent->client->ctf_techsndtime < level.time)) + { + ent->client->ctf_techsndtime = level.time + 1; + gi.sound(ent, CHAN_VOICE, gi.soundindex( + "ctf/tech4.wav"), volume, ATTN_NORM, 0); + } + } +} + +qboolean +CTFHasRegeneration(edict_t *ent) +{ + static gitem_t *tech = NULL; + + if (!tech) + { + tech = FindItemByClassname("item_tech4"); + } + + if (tech && ent->client && + ent->client->pers.inventory[ITEM_INDEX(tech)]) + { + return true; + } + + return false; +} + +/* + * ====================================================================== + * + * SAY_TEAM + * + * ====================================================================== + */ + +/* This array is in 'importance order', + it indicates what items are more + important when reporting their + names. */ +struct +{ + char *classname; + int priority; +} loc_names[] = { + {"item_flag_team1", 1}, + {"item_flag_team2", 1}, + {"item_quad", 2}, + {"item_invulnerability", 2}, + {"weapon_bfg", 3}, + {"weapon_railgun", 4}, + {"weapon_rocketlauncher", 4}, + {"weapon_hyperblaster", 4}, + {"weapon_chaingun", 4}, + {"weapon_grenadelauncher", 4}, + {"weapon_machinegun", 4}, + {"weapon_supershotgun", 4}, + {"weapon_shotgun", 4}, + {"item_power_screen", 5}, + {"item_power_shield", 5}, + {"item_armor_body", 6}, + {"item_armor_combat", 6}, + {"item_armor_jacket", 6}, + {"item_silencer", 7}, + {"item_breather", 7}, + {"item_enviro", 7}, + {"item_adrenaline", 7}, + {"item_bandolier", 8}, + {"item_pack", 8}, + {NULL, 0} +}; + +static void +CTFSay_Team_Location(edict_t *who, char *buf) +{ + edict_t *what = NULL; + edict_t *hot = NULL; + float hotdist = 999999, newdist; + vec3_t v; + int hotindex = 999; + int i; + gitem_t *item; + int nearteam = -1; + edict_t *flag1, *flag2; + qboolean hotsee = false; + qboolean cansee; + + while ((what = loc_findradius(what, who->s.origin, 1024)) != NULL) + { + /* find what in loc_classnames */ + for (i = 0; loc_names[i].classname; i++) + { + if (strcmp(what->classname, loc_names[i].classname) == 0) + { + break; + } + } + + if (!loc_names[i].classname) + { + continue; + } + + /* something we can see get priority over something we can't */ + cansee = loc_CanSee(what, who); + + if (cansee && !hotsee) + { + hotsee = true; + hotindex = loc_names[i].priority; + hot = what; + VectorSubtract(what->s.origin, who->s.origin, v); + hotdist = VectorLength(v); + continue; + } + + /* if we can't see this, but we have something we can see, skip it */ + if (hotsee && !cansee) + { + continue; + } + + if (hotsee && (hotindex < loc_names[i].priority)) + { + continue; + } + + VectorSubtract(what->s.origin, who->s.origin, v); + newdist = VectorLength(v); + + if ((newdist < hotdist) || + (cansee && (loc_names[i].priority < hotindex))) + { + hot = what; + hotdist = newdist; + hotindex = i; + hotsee = loc_CanSee(hot, who); + } + } + + if (!hot) + { + strcpy(buf, "nowhere"); + return; + } + + /* we now have the closest item + see if there's more than one in the map, if so + we need to determine what team is closest */ + what = NULL; + + while ((what = G_Find(what, FOFS(classname), hot->classname)) != NULL) + { + if (what == hot) + { + continue; + } + + /* if we are here, there is more than one, + find out if hot is closer to red flag + or blue flag */ + if (((flag1 = G_Find(NULL, FOFS(classname), "item_flag_team1")) != NULL) && + ((flag2 = G_Find(NULL, FOFS(classname), "item_flag_team2")) != NULL)) + { + VectorSubtract(hot->s.origin, flag1->s.origin, v); + hotdist = VectorLength(v); + VectorSubtract(hot->s.origin, flag2->s.origin, v); + newdist = VectorLength(v); + + if (hotdist < newdist) + { + nearteam = CTF_TEAM1; + } + else if (hotdist > newdist) + { + nearteam = CTF_TEAM2; + } + } + + break; + } + + if ((item = FindItemByClassname(hot->classname)) == NULL) + { + strcpy(buf, "nowhere"); + return; + } + + /* in water? */ + if (who->waterlevel) + { + strcpy(buf, "in the water "); + } + else + { + *buf = 0; + } + + /* near or above */ + VectorSubtract(who->s.origin, hot->s.origin, v); + + if ((fabs(v[2]) > fabs(v[0])) && (fabs(v[2]) > fabs(v[1]))) + { + if (v[2] > 0) + { + strcat(buf, "above "); + } + else + { + strcat(buf, "below "); + } + } + else + { + strcat(buf, "near "); + } + + if (nearteam == CTF_TEAM1) + { + strcat(buf, "the red "); + } + else if (nearteam == CTF_TEAM2) + { + strcat(buf, "the blue "); + } + else + { + strcat(buf, "the "); + } + + strcat(buf, item->pickup_name); +} + +static void +CTFSay_Team_Armor(edict_t *who, char *buf) +{ + gitem_t *item; + int index, cells; + int power_armor_type; + + *buf = 0; + + power_armor_type = PowerArmorType(who); + + if (power_armor_type) + { + cells = who->client->pers.inventory[ITEM_INDEX(FindItem("cells"))]; + + if (cells) + { + sprintf(buf + strlen(buf), "%s with %i cells ", + (power_armor_type == POWER_ARMOR_SCREEN) ? + "Power Screen" : "Power Shield", cells); + } + } + + index = ArmorIndex(who); + + if (index) + { + item = GetItemByIndex(index); + + if (item) + { + if (*buf) + { + strcat(buf, "and "); + } + + sprintf(buf + strlen(buf), "%i units of %s", + who->client->pers.inventory[index], item->pickup_name); + } + } + + if (!*buf) + { + strcpy(buf, "no armor"); + } +} + +static void +CTFSay_Team_Health(edict_t *who, char *buf) +{ + if (who->health <= 0) + { + strcpy(buf, "dead"); + } + else + { + sprintf(buf, "%i health", who->health); + } +} + +static void +CTFSay_Team_Tech(edict_t *who, char *buf) +{ + gitem_t *tech; + int i; + + /* see if the player has a tech powerup */ + i = 0; + + while (tnames[i]) + { + if (((tech = FindItemByClassname(tnames[i])) != NULL) && + who->client->pers.inventory[ITEM_INDEX(tech)]) + { + sprintf(buf, "the %s", tech->pickup_name); + return; + } + + i++; + } + + strcpy(buf, "no powerup"); +} + +static void +CTFSay_Team_Weapon(edict_t *who, char *buf) +{ + if (who->client->pers.weapon) + { + strcpy(buf, who->client->pers.weapon->pickup_name); + } + else + { + strcpy(buf, "none"); + } +} + +static void +CTFSay_Team_Sight(edict_t *who, char *buf) +{ + int i; + edict_t *targ; + int n = 0; + char s[1024]; + char s2[1024]; + + *s = *s2 = 0; + + for (i = 1; i <= maxclients->value; i++) + { + targ = g_edicts + i; + + if (!targ->inuse || + (targ == who) || + !loc_CanSee(targ, who)) + { + continue; + } + + if (*s2) + { + if (strlen(s) + strlen(s2) + 3 < sizeof(s)) + { + if (n) + { + strcat(s, ", "); + } + + strcat(s, s2); + *s2 = 0; + } + + n++; + } + + strcpy(s2, targ->client->pers.netname); + } + + if (*s2) + { + if (strlen(s) + strlen(s2) + 6 < sizeof(s)) + { + if (n) + { + strcat(s, " and "); + } + + strcat(s, s2); + } + + strcpy(buf, s); + } + else + { + strcpy(buf, "no one"); + } +} + +void +CTFSay_Team(edict_t *who, char *msg) +{ + char outmsg[256]; + char buf[256]; + int i; + char *p; + edict_t *cl_ent; + + if (CheckFlood(who)) + { + return; + } + + outmsg[0] = 0; + + if (*msg == '\"') + { + msg[strlen(msg) - 1] = 0; + msg++; + } + + for (p = outmsg; *msg && (p - outmsg) < sizeof(outmsg) - 2; msg++) + { + if (*msg == '%') + { + switch (*++msg) + { + case 'l': + case 'L': + CTFSay_Team_Location(who, buf); + + if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 2) + { + strcpy(p, buf); + p += strlen(buf); + } + + break; + case 'a': + case 'A': + CTFSay_Team_Armor(who, buf); + + if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 2) + { + strcpy(p, buf); + p += strlen(buf); + } + + break; + case 'h': + case 'H': + CTFSay_Team_Health(who, buf); + + if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 2) + { + strcpy(p, buf); + p += strlen(buf); + } + + break; + case 't': + case 'T': + CTFSay_Team_Tech(who, buf); + + if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 2) + { + strcpy(p, buf); + p += strlen(buf); + } + + break; + case 'w': + case 'W': + CTFSay_Team_Weapon(who, buf); + + if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 2) + { + strcpy(p, buf); + p += strlen(buf); + } + + break; + + case 'n': + case 'N': + CTFSay_Team_Sight(who, buf); + + if (strlen(buf) + (p - outmsg) < sizeof(outmsg) - 2) + { + strcpy(p, buf); + p += strlen(buf); + } + + break; + + default: + *p++ = *msg; + } + } + else + { + *p++ = *msg; + } + } + + *p = 0; + + for (i = 0; i < maxclients->value; 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.cprintf(cl_ent, PRINT_CHAT, "(%s): %s\n", + who->client->pers.netname, outmsg); + } + } +} + +/*-----------------------------------------------------------------------*/ + +/* + * 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. + */ +static void +misc_ctf_banner_think(edict_t *ent) +{ + ent->s.frame = (ent->s.frame + 1) % 16; + ent->nextthink = level.time + FRAMETIME; +} + +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 & 1) /* team2 */ + { + ent->s.skinnum = 1; + } + + ent->s.frame = rand() % 16; + gi.linkentity(ent); + + ent->think = misc_ctf_banner_think; + ent->nextthink = level.time + FRAMETIME; +} + +/* + * 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 & 1) /* team2 */ + { + ent->s.skinnum = 1; + } + + ent->s.frame = rand() % 16; + gi.linkentity(ent); + + ent->think = misc_ctf_banner_think; + ent->nextthink = level.time + FRAMETIME; +} + +/*-----------------------------------------------------------------------*/ + +static void +SetLevelName(pmenu_t *p) +{ + static char levelname[33]; + + levelname[0] = '*'; + + if (g_edicts[0].message) + { + strncpy(levelname + 1, g_edicts[0].message, sizeof(levelname) - 2); + } + else + { + strncpy(levelname + 1, level.mapname, sizeof(levelname) - 2); + } + + levelname[sizeof(levelname) - 1] = 0; + p->text = levelname; +} + +/*-----------------------------------------------------------------------*/ + +/* ELECTIONS */ + +qboolean +CTFBeginElection(edict_t *ent, elect_t type, char *msg) +{ + int i; + int count; + edict_t *e; + + if (electpercentage->value == 0) + { + gi.cprintf( + ent, + PRINT_HIGH, + "Elections are disabled, only an admin can process this action.\n"); + return false; + } + + if (ctfgame.election != ELECT_NONE) + { + gi.cprintf(ent, PRINT_HIGH, "Election already in progress.\n"); + return false; + } + + /* clear votes */ + count = 0; + + for (i = 1; i <= maxclients->value; i++) + { + e = g_edicts + i; + e->client->resp.voted = false; + + if (e->inuse) + { + count++; + } + } + + if (count < 2) + { + gi.cprintf(ent, PRINT_HIGH, "Not enough players for election.\n"); + return false; + } + + ctfgame.etarget = ent; + ctfgame.election = type; + ctfgame.evotes = 0; + ctfgame.needvotes = (count * electpercentage->value) / 100; + ctfgame.electtime = level.time + 20; /* twenty seconds for election */ + strncpy(ctfgame.emsg, msg, sizeof(ctfgame.emsg) - 1); + + /* tell everyone */ + gi.bprintf(PRINT_CHAT, "%s\n", ctfgame.emsg); + gi.bprintf(PRINT_HIGH, "Type YES or NO to vote on this request.\n"); + gi.bprintf(PRINT_HIGH, "Votes: %d Needed: %d Time left: %ds\n", + ctfgame.evotes, ctfgame.needvotes, + (int)(ctfgame.electtime - level.time)); + + return true; +} + +void DoRespawn(edict_t *ent); + +void +CTFResetAllPlayers(void) +{ + int i; + edict_t *ent; + + for (i = 1; i <= maxclients->value; 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 = 0; + 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; + DoRespawn(ent); + } + } + } + + if (ctfgame.match == MATCH_SETUP) + { + ctfgame.matchtime = level.time + matchsetuptime->value * 60; + } +} + +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 = 10000 + (rand() % 90000); + + 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; + strcpy(ctfgame.ghosts[ghost].netname, ent->client->pers.netname); + ent->client->resp.ghost = ctfgame.ghosts + ghost; + gi.cprintf(ent, PRINT_CHAT, "Your ghost code is **** %d ****\n", + ctfgame.ghosts[ghost].code); + gi.cprintf(ent, PRINT_HIGH, + "If you lose connection, you can rejoin with your score " + "intact by typing \"ghost %d\".\n", + ctfgame.ghosts[ghost].code); +} + +/* + * Start a match + */ +void +CTFStartMatch(void) +{ + int i; + edict_t *ent; + + ctfgame.match = MATCH_GAME; + ctfgame.matchtime = level.time + matchtime->value * 60; + ctfgame.countdown = false; + + ctfgame.team1 = ctfgame.team2 = 0; + + memset(ctfgame.ghosts, 0, sizeof(ctfgame.ghosts)); + + for (i = 1; i <= maxclients->value; i++) + { + ent = g_edicts + i; + + if (!ent->inuse) + { + continue; + } + + ent->client->resp.score = 0; + ent->client->resp.ctf_state = 0; + ent->client->resp.ghost = NULL; + + gi.centerprintf( 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 + 1.0 + ((rand() % 30) / 10.0); + 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 = DEAD_DEAD; + ent->movetype = MOVETYPE_NOCLIP; + ent->client->ps.gunindex = 0; + gi.linkentity(ent); + } + } +} + +void +CTFEndMatch(void) +{ + ctfgame.match = MATCH_POST; + gi.bprintf(PRINT_CHAT, "MATCH COMPLETED!\n"); + + CTFCalcScores(); + + gi.bprintf(PRINT_HIGH, "RED TEAM: %d captures, %d points\n", + ctfgame.team1, ctfgame.total1); + gi.bprintf(PRINT_HIGH, "BLUE TEAM: %d captures, %d points\n", + ctfgame.team2, ctfgame.total2); + + if (ctfgame.team1 > ctfgame.team2) + { + gi.bprintf(PRINT_CHAT, + "RED team won over the BLUE team by %d CAPTURES!\n", + ctfgame.team1 - ctfgame.team2); + } + else if (ctfgame.team2 > ctfgame.team1) + { + gi.bprintf(PRINT_CHAT, + "BLUE team won over the RED team by %d CAPTURES!\n", + ctfgame.team2 - ctfgame.team1); + } + else if (ctfgame.total1 > ctfgame.total2) /* frag tie breaker */ + { + gi.bprintf(PRINT_CHAT, + "RED team won over the BLUE team by %d POINTS!\n", + ctfgame.total1 - ctfgame.total2); + } + else if (ctfgame.total2 > ctfgame.total1) + { + gi.bprintf(PRINT_CHAT, + "BLUE team won over the RED team by %d POINTS!\n", + ctfgame.total2 - ctfgame.total1); + } + else + { + gi.bprintf(PRINT_CHAT, "TIE GAME!\n"); + } + + EndDMLevel(); +} + +qboolean +CTFNextMap(void) +{ + if (ctfgame.match == MATCH_POST) + { + ctfgame.match = MATCH_SETUP; + CTFResetAllPlayers(); + return true; + } + + return false; +} + +void +CTFWinElection(void) +{ + switch (ctfgame.election) + { + case ELECT_MATCH: + + /* reset into match mode */ + if (competition->value < 3) + { + gi.cvar_set("competition", "2"); + } + + ctfgame.match = MATCH_SETUP; + CTFResetAllPlayers(); + break; + + case ELECT_ADMIN: + ctfgame.etarget->client->resp.admin = true; + gi.bprintf(PRINT_HIGH, + "%s has become an admin.\n", + ctfgame.etarget->client->pers.netname); + gi.cprintf(ctfgame.etarget, + PRINT_HIGH, + "Type 'admin' to access the adminstration menu.\n"); + break; + + case ELECT_MAP: + gi.bprintf(PRINT_HIGH, "%s is warping to level %s.\n", + ctfgame.etarget->client->pers.netname, ctfgame.elevel); + strncpy(level.forcemap, ctfgame.elevel, sizeof(level.forcemap) - 1); + EndDMLevel(); + break; + default: + break; + } + + ctfgame.election = ELECT_NONE; +} + +void +CTFVoteYes(edict_t *ent) +{ + if (ctfgame.election == ELECT_NONE) + { + gi.cprintf(ent, PRINT_HIGH, "No election is in progress.\n"); + return; + } + + if (ent->client->resp.voted) + { + gi.cprintf(ent, PRINT_HIGH, "You already voted.\n"); + return; + } + + if (ctfgame.etarget == ent) + { + gi.cprintf(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.bprintf(PRINT_HIGH, "%s\n", ctfgame.emsg); + gi.bprintf(PRINT_CHAT, + "Votes: %d Needed: %d Time left: %ds\n", + ctfgame.evotes, + ctfgame.needvotes, + (int)(ctfgame.electtime - level.time)); +} + +void +CTFVoteNo(edict_t *ent) +{ + if (ctfgame.election == ELECT_NONE) + { + gi.cprintf(ent, PRINT_HIGH, "No election is in progress.\n"); + return; + } + + if (ent->client->resp.voted) + { + gi.cprintf(ent, PRINT_HIGH, "You already voted.\n"); + return; + } + + if (ctfgame.etarget == ent) + { + gi.cprintf(ent, PRINT_HIGH, "You can't vote for yourself.\n"); + return; + } + + ent->client->resp.voted = true; + + gi.bprintf(PRINT_HIGH, "%s\n", ctfgame.emsg); + gi.bprintf(PRINT_CHAT, + "Votes: %d Needed: %d Time left: %ds\n", + ctfgame.evotes, + ctfgame.needvotes, + (int)(ctfgame.electtime - level.time)); +} + +void +CTFReady(edict_t *ent) +{ + int i, j; + edict_t *e; + int t1, t2; + + if (ent->client->resp.ctf_team == CTF_NOTEAM) + { + gi.cprintf(ent, PRINT_HIGH, "Pick a team first (hit for menu)\n"); + return; + } + + if (ctfgame.match != MATCH_SETUP) + { + gi.cprintf(ent, PRINT_HIGH, "A match is not being setup.\n"); + return; + } + + if (ent->client->resp.ready) + { + gi.cprintf(ent, PRINT_HIGH, "You have already commited.\n"); + return; + } + + ent->client->resp.ready = true; + gi.bprintf(PRINT_HIGH, "%s is ready.\n", ent->client->pers.netname); + + t1 = t2 = 0; + + for (j = 0, i = 1; i <= maxclients->value; 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.bprintf(PRINT_CHAT, "All players have commited. Match starting\n"); + ctfgame.match = MATCH_PREGAME; + ctfgame.matchtime = level.time + 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.cprintf(ent, PRINT_HIGH, "Pick a team first (hit for menu)\n"); + return; + } + + if ((ctfgame.match != MATCH_SETUP) && (ctfgame.match != MATCH_PREGAME)) + { + gi.cprintf(ent, PRINT_HIGH, "A match is not being setup.\n"); + return; + } + + if (!ent->client->resp.ready) + { + gi.cprintf(ent, PRINT_HIGH, "You haven't commited.\n"); + return; + } + + ent->client->resp.ready = false; + gi.bprintf(PRINT_HIGH, "%s is no longer ready.\n", + ent->client->pers.netname); + + if (ctfgame.match == MATCH_PREGAME) + { + gi.bprintf(PRINT_CHAT, "Match halted.\n"); + ctfgame.match = MATCH_SETUP; + ctfgame.matchtime = level.time + matchsetuptime->value * 60; + } +} + +void +CTFGhost(edict_t *ent) +{ + int i; + int n; + + if (gi.argc() < 2) + { + gi.cprintf(ent, PRINT_HIGH, "Usage: ghost \n"); + return; + } + + if (ent->client->resp.ctf_team != CTF_NOTEAM) + { + gi.cprintf(ent, PRINT_HIGH, "You are already in the game.\n"); + return; + } + + if (ctfgame.match != MATCH_GAME) + { + gi.cprintf(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.cprintf(ent, PRINT_HIGH, + "Ghost code accepted, your position has been reinstated.\n"); + ctfgame.ghosts[i].ent->client->resp.ghost = NULL; + 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 = 0; + ent->flags &= ~FL_GODMODE; + PutClientInServer(ent); + gi.bprintf(PRINT_HIGH, "%s has been reinstated to %s team.\n", + ent->client->pers.netname, + CTFTeamName(ent->client->resp.ctf_team)); + return; + } + } + + gi.cprintf(ent, PRINT_HIGH, "Invalid ghost code.\n"); +} + +qboolean +CTFMatchSetup(void) +{ + if ((ctfgame.match == MATCH_SETUP) || (ctfgame.match == MATCH_PREGAME)) + { + return true; + } + + return false; +} + +qboolean +CTFMatchOn(void) +{ + 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 CTFCredits(edict_t *ent, pmenuhnd_t *p); +void CTFReturnToMain(edict_t *ent, pmenuhnd_t *p); +void CTFChaseCam(edict_t *ent, pmenuhnd_t *p); + +pmenu_t creditsmenu[] = { + {"*Quake II", PMENU_ALIGN_CENTER, NULL}, + {"*ThreeWave Capture the Flag", PMENU_ALIGN_CENTER, NULL}, + {NULL, PMENU_ALIGN_CENTER, NULL}, + {"*Programming", PMENU_ALIGN_CENTER, NULL}, + {"Dave 'Zoid' Kirsch", PMENU_ALIGN_CENTER, NULL}, + {"*Level Design", PMENU_ALIGN_CENTER, NULL}, + {"Christian Antkow", PMENU_ALIGN_CENTER, NULL}, + {"Tim Willits", PMENU_ALIGN_CENTER, NULL}, + {"Dave 'Zoid' Kirsch", PMENU_ALIGN_CENTER, NULL}, + {"*Art", PMENU_ALIGN_CENTER, NULL}, + {"Adrian Carmack Paul Steed", PMENU_ALIGN_CENTER, NULL}, + {"Kevin Cloud", PMENU_ALIGN_CENTER, NULL}, + {"*Sound", PMENU_ALIGN_CENTER, NULL}, + {"Tom 'Bjorn' Klok", PMENU_ALIGN_CENTER, NULL}, + {"*Original CTF Art Design", PMENU_ALIGN_CENTER, NULL}, + {"Brian 'Whaleboy' Cozzens", PMENU_ALIGN_CENTER, NULL}, + {NULL, PMENU_ALIGN_CENTER, NULL}, + {"Return to Main Menu", PMENU_ALIGN_LEFT, CTFReturnToMain} +}; + +static const int jmenu_level = 2; +static const int jmenu_match = 3; +static const int jmenu_red = 5; +static const int jmenu_blue = 7; +static const int jmenu_chase = 9; +static const int jmenu_reqmatch = 11; + +pmenu_t joinmenu[] = { + {"*Quake II", PMENU_ALIGN_CENTER, NULL}, + {"*ThreeWave Capture the Flag", PMENU_ALIGN_CENTER, NULL}, + {NULL, PMENU_ALIGN_CENTER, NULL}, + {NULL, PMENU_ALIGN_CENTER, NULL}, + {NULL, PMENU_ALIGN_CENTER, NULL}, + {"Join Red Team", PMENU_ALIGN_LEFT, CTFJoinTeam1}, + {NULL, PMENU_ALIGN_LEFT, NULL}, + {"Join Blue Team", PMENU_ALIGN_LEFT, CTFJoinTeam2}, + {NULL, PMENU_ALIGN_LEFT, NULL}, + {"Chase Camera", PMENU_ALIGN_LEFT, CTFChaseCam}, + {"Credits", PMENU_ALIGN_LEFT, CTFCredits}, + {NULL, PMENU_ALIGN_LEFT, NULL}, + {NULL, PMENU_ALIGN_LEFT, NULL}, + {"Use [ and ] to move cursor", PMENU_ALIGN_LEFT, NULL}, + {"ENTER to select", PMENU_ALIGN_LEFT, NULL}, + {"ESC to Exit Menu", PMENU_ALIGN_LEFT, NULL}, + {"(TAB to Return)", PMENU_ALIGN_LEFT, NULL}, + {"v" CTF_STRING_VERSION, PMENU_ALIGN_RIGHT, NULL}, +}; + +pmenu_t nochasemenu[] = { + {"*Quake II", PMENU_ALIGN_CENTER, NULL}, + {"*ThreeWave Capture the Flag", PMENU_ALIGN_CENTER, NULL}, + {NULL, PMENU_ALIGN_CENTER, NULL}, + {NULL, PMENU_ALIGN_CENTER, NULL}, + {"No one to chase", PMENU_ALIGN_LEFT, NULL}, + {NULL, PMENU_ALIGN_CENTER, NULL}, + {"Return to Main Menu", PMENU_ALIGN_LEFT, CTFReturnToMain} +}; + +void +CTFJoinTeam(edict_t *ent, int desired_team) +{ + char *s; + + PMenu_Close(ent); + + ent->svflags &= ~SVF_NOCLIENT; + ent->client->resp.ctf_team = desired_team; + ent->client->resp.ctf_state = 0; + s = Info_ValueForKey(ent->client->pers.userinfo, "skin"); + CTFAssignSkin(ent, s); + + /* 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 = NULL; + CTFAssignGhost(ent); + } + + PutClientInServer(ent); + /* add a teleportation effect */ + ent->s.event = EV_PLAYER_TELEPORT; + /* hold in place briefly */ + ent->client->ps.pmove.pm_flags = PMF_TIME_TELEPORT; + ent->client->ps.pmove.pm_time = 14; + gi.bprintf(PRINT_HIGH, "%s joined the %s team.\n", + ent->client->pers.netname, CTFTeamName(desired_team)); + + if (ctfgame.match == MATCH_SETUP) + { + gi.centerprintf(ent, "***********************\n" + "Type \"ready\" in console\n" + "to ready up.\n" + "***********************"); + } +} + +void +CTFJoinTeam1(edict_t *ent, pmenuhnd_t *p) +{ + CTFJoinTeam(ent, CTF_TEAM1); +} + +void +CTFJoinTeam2(edict_t *ent, pmenuhnd_t *p) +{ + CTFJoinTeam(ent, CTF_TEAM2); +} + +void +CTFChaseCam(edict_t *ent, pmenuhnd_t *p) +{ + int i; + edict_t *e; + + if (ent->client->chase_target) + { + ent->client->chase_target = NULL; + ent->client->ps.pmove.pm_flags &= ~PMF_NO_PREDICTION; + PMenu_Close(ent); + return; + } + + for (i = 1; i <= maxclients->value; 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; + } + } + + SetLevelName(nochasemenu + jmenu_level); + + PMenu_Close(ent); + PMenu_Open(ent, nochasemenu, -1, + sizeof(nochasemenu) / sizeof(pmenu_t), + NULL); +} + +void +CTFReturnToMain(edict_t *ent, pmenuhnd_t *p) +{ + PMenu_Close(ent); + CTFOpenJoinMenu(ent); +} + +void +CTFRequestMatch(edict_t *ent, pmenuhnd_t *p) +{ + char text[1024]; + + PMenu_Close(ent); + + sprintf(text, "%s has requested to switch to competition mode.", + ent->client->pers.netname); + CTFBeginElection(ent, ELECT_MATCH, text); +} + +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); +} + +int +CTFUpdateJoinMenu(edict_t *ent) +{ + static char team1players[32]; + static char team2players[32]; + int num1, num2, i; + + if ((ctfgame.match >= MATCH_PREGAME) && matchlock->value) + { + joinmenu[jmenu_red].text = "MATCH IS LOCKED"; + joinmenu[jmenu_red].SelectFunc = NULL; + joinmenu[jmenu_blue].text = " (entry is not permitted)"; + joinmenu[jmenu_blue].SelectFunc = NULL; + } + else + { + if (ctfgame.match >= MATCH_PREGAME) + { + joinmenu[jmenu_red].text = "Join Red MATCH Team"; + joinmenu[jmenu_blue].text = "Join Blue MATCH Team"; + } + else + { + joinmenu[jmenu_red].text = "Join Red Team"; + joinmenu[jmenu_blue].text = "Join Blue Team"; + } + + joinmenu[jmenu_red].SelectFunc = CTFJoinTeam1; + joinmenu[jmenu_blue].SelectFunc = CTFJoinTeam2; + } + + if (ctf_forcejoin->string && *ctf_forcejoin->string) + { + if (Q_stricmp(ctf_forcejoin->string, "red") == 0) + { + joinmenu[jmenu_blue].text = NULL; + joinmenu[jmenu_blue].SelectFunc = NULL; + } + else if (Q_stricmp(ctf_forcejoin->string, "blue") == 0) + { + joinmenu[jmenu_red].text = NULL; + joinmenu[jmenu_red].SelectFunc = NULL; + } + } + + if (ent->client->chase_target) + { + joinmenu[jmenu_chase].text = "Leave Chase Camera"; + } + else + { + joinmenu[jmenu_chase].text = "Chase Camera"; + } + + SetLevelName(joinmenu + jmenu_level); + + num1 = num2 = 0; + + for (i = 0; i < maxclients->value; 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++; + } + } + + sprintf(team1players, " (%d players)", num1); + sprintf(team2players, " (%d players)", num2); + + switch (ctfgame.match) + { + case MATCH_NONE: + joinmenu[jmenu_match].text = NULL; + break; + + case MATCH_SETUP: + joinmenu[jmenu_match].text = "*MATCH SETUP IN PROGRESS"; + break; + + case MATCH_PREGAME: + joinmenu[jmenu_match].text = "*MATCH STARTING"; + break; + + case MATCH_GAME: + joinmenu[jmenu_match].text = "*MATCH IN PROGRESS"; + break; + default: + break; + } + + if (joinmenu[jmenu_red].text) + { + joinmenu[jmenu_red + 1].text = team1players; + } + else + { + joinmenu[jmenu_red + 1].text = NULL; + } + + if (joinmenu[jmenu_blue].text) + { + joinmenu[jmenu_blue + 1].text = team2players; + } + else + { + joinmenu[jmenu_blue + 1].text = NULL; + } + + joinmenu[jmenu_reqmatch].text = NULL; + joinmenu[jmenu_reqmatch].SelectFunc = NULL; + + if (competition->value && (ctfgame.match < MATCH_SETUP)) + { + joinmenu[jmenu_reqmatch].text = "Request Match"; + joinmenu[jmenu_reqmatch].SelectFunc = CTFRequestMatch; + } + + if (num1 > num2) + { + return CTF_TEAM1; + } + else if (num2 > num1) + { + return CTF_TEAM2; + } + + return (rand() & 1) ? CTF_TEAM1 : CTF_TEAM2; +} + +void +CTFOpenJoinMenu(edict_t *ent) +{ + int team; + + team = CTFUpdateJoinMenu(ent); + + if (ent->client->chase_target) + { + team = 8; + } + else if (team == CTF_TEAM1) + { + team = 4; + } + else + { + team = 6; + } + + PMenu_Open(ent, joinmenu, team, sizeof(joinmenu) / sizeof(pmenu_t), NULL); +} + +void +CTFCredits(edict_t *ent, pmenuhnd_t *p) +{ + PMenu_Close(ent); + PMenu_Open(ent, creditsmenu, -1, + sizeof(creditsmenu) / sizeof(pmenu_t), + NULL); +} + +qboolean +CTFStartClient(edict_t *ent) +{ + if (ent->client->resp.ctf_team != CTF_NOTEAM) + { + return false; + } + + if (!((int)dmflags->value & DF_CTF_FORCEJOIN) || + (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->ps.gunindex = 0; + gi.linkentity(ent); + + CTFOpenJoinMenu(ent); + return true; + } + + return false; +} + +void +CTFObserver(edict_t *ent) +{ + char userinfo[MAX_INFO_STRING]; + + /* start as 'observer' */ + if (ent->movetype == MOVETYPE_NOCLIP) + { + CTFPlayerResetGrapple(ent); + } + + CTFDeadDropFlag(ent); + CTFDeadDropTech(ent); + + ent->deadflag = DEAD_NO; + 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->resp.score = 0; + memcpy(userinfo, ent->client->pers.userinfo, sizeof(userinfo)); + InitClientPersistant(ent->client); + ClientUserinfoChanged(ent, userinfo); + gi.linkentity(ent); + CTFOpenJoinMenu(ent); +} + +qboolean +CTFInMatch(void) +{ + if (ctfgame.match > MATCH_NONE) + { + return true; + } + + return false; +} + +qboolean +CTFCheckRules(void) +{ + int t; + int i, j; + char text[64]; + edict_t *ent; + + if ((ctfgame.election != ELECT_NONE) && (ctfgame.electtime <= level.time)) + { + gi.bprintf(PRINT_CHAT, "Election timed out and has been cancelled.\n"); + ctfgame.election = ELECT_NONE; + } + + if (ctfgame.match != MATCH_NONE) + { + t = ctfgame.matchtime - level.time; + + /* 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->value < 3) + { + ctfgame.match = MATCH_NONE; + gi.cvar_set("competition", "1"); + CTFResetAllPlayers(); + } + else + { + /* reset the time */ + ctfgame.matchtime = level.time + + matchsetuptime->value * 60; + } + + 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 <= maxclients->value; i++) + { + ent = g_edicts + i; + + if (!ent->inuse) + { + continue; + } + + if ((ent->client->resp.ctf_team != CTF_NOTEAM) && + !ent->client->resp.ready) + { + j++; + } + } + + if (competition->value < 3) + { + sprintf(text, "%02d:%02d SETUP: %d not ready", + t / 60, t % 60, j); + } + else + { + sprintf(text, "SETUP: %d not ready", j); + } + + gi.configstring(CONFIG_CTF_MATCH, text); + break; + + case MATCH_PREGAME: + sprintf(text, "%02d:%02d 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: + sprintf(text, "%02d:%02d 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 == ctfgame.lasttime) + { + return false; + } + + ctfgame.lasttime = level.time; + + if (warn_unbalanced->value) + { + /* count up the team totals */ + for (i = 1; i <= maxclients->value; 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->value && + ((ctfgame.team1 >= capturelimit->value) || + (ctfgame.team2 >= capturelimit->value))) + { + gi.bprintf(PRINT_HIGH, "Capturelimit hit.\n"); + return true; + } + + return false; +} + +/*-------------------------------------------------------------------------- + * just here to help old map conversions + *--------------------------------------------------------------------------*/ + +static void +old_teleporter_touch(edict_t *self, edict_t *other, cplane_t *plane, + csurface_t *surf) +{ + edict_t *dest; + int i; + vec3_t forward; + + if (!other->client) + { + return; + } + + dest = G_Find(NULL, FOFS(targetname), self->target); + + if (!dest) + { + gi.dprintf("Couldn't find destination\n"); + return; + } + + CTFPlayerResetGrapple(other); + + /* unlink to make sure it can't possibly interfere with KillBox */ + gi.unlinkentity(other); + + VectorCopy(dest->s.origin, other->s.origin); + VectorCopy(dest->s.origin, other->s.old_origin); + + /* clear the velocity and hold them in place briefly */ + VectorClear(other->velocity); + other->client->ps.pmove.pm_time = 160 >> 3; /* 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 */ + for (i = 0; i < 3; i++) + { + other->client->ps.pmove.delta_angles[i] = ANGLE2SHORT( + dest->s.angles[i] - other->client->resp.cmd_angles[i]); + } + + other->s.angles[PITCH] = 0; + other->s.angles[YAW] = dest->s.angles[YAW]; + other->s.angles[ROLL] = 0; + VectorCopy(dest->s.angles, other->client->ps.viewangles); + VectorCopy(dest->s.angles, other->client->v_angle); + + /* give a little forward velocity */ + AngleVectors(other->client->v_angle, forward, NULL, NULL); + VectorScale(forward, 200, other->velocity); + + /* kill anything at the destination */ + if (!KillBox(other)) + { + } + + gi.linkentity(other); +} + +/* + * QUAKED trigger_teleport (0.5 0.5 0.5) ? + * Players touching this will be teleported + */ +void +SP_trigger_teleport(edict_t *ent) +{ + edict_t *s; + int i; + + if (!ent->target) + { + gi.dprintf("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_teleport_destination (0.5 0.5 0.5) (-16 -16 -24) (16 16 32) + * Point trigger_teleports at these. + */ +void +SP_info_teleport_destination(edict_t *ent) +{ + ent->s.origin[2] += 16; +} + +/*----------------------------------------------------------------------------------*/ + +/* ADMIN */ + +typedef struct admin_settings_s +{ + int matchlen; + int matchsetuplen; + int matchstartlen; + qboolean weaponsstay; + qboolean instantitems; + qboolean quaddrop; + qboolean instantweap; + qboolean matchlock; +} admin_settings_t; + +#define SETMENU_SIZE (7 + 5) + +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 = p->arg; + char st[80]; + int i; + + if (settings->matchlen != matchtime->value) + { + gi.bprintf(PRINT_HIGH, "%s changed the match length to %d 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 - matchtime->value * + 60) + settings->matchlen * 60; + } + + sprintf(st, "%d", settings->matchlen); + gi.cvar_set("matchtime", st); + } + + if (settings->matchsetuplen != matchsetuptime->value) + { + gi.bprintf(PRINT_HIGH, + "%s changed the match setup time to %d 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 - matchsetuptime->value * + 60) + settings->matchsetuplen * 60; + } + + sprintf(st, "%d", settings->matchsetuplen); + gi.cvar_set("matchsetuptime", st); + } + + if (settings->matchstartlen != matchstarttime->value) + { + gi.bprintf(PRINT_HIGH, + "%s changed the match start time to %d 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 - + matchstarttime->value) + settings->matchstartlen; + } + + sprintf(st, "%d", settings->matchstartlen); + gi.cvar_set("matchstarttime", st); + } + + if (settings->weaponsstay != !!((int)dmflags->value & DF_WEAPONS_STAY)) + { + gi.bprintf(PRINT_HIGH, "%s turned %s weapons stay.\n", + ent->client->pers.netname, settings->weaponsstay ? "on" : "off"); + i = (int)dmflags->value; + + if (settings->weaponsstay) + { + i |= DF_WEAPONS_STAY; + } + else + { + i &= ~DF_WEAPONS_STAY; + } + + sprintf(st, "%d", i); + gi.cvar_set("dmflags", st); + } + + if (settings->instantitems != !!((int)dmflags->value & DF_INSTANT_ITEMS)) + { + gi.bprintf(PRINT_HIGH, "%s turned %s instant items.\n", + ent->client->pers.netname, + settings->instantitems ? "on" : "off"); + i = (int)dmflags->value; + + if (settings->instantitems) + { + i |= DF_INSTANT_ITEMS; + } + else + { + i &= ~DF_INSTANT_ITEMS; + } + + sprintf(st, "%d", i); + gi.cvar_set("dmflags", st); + } + + if (settings->quaddrop != !!((int)dmflags->value & DF_QUAD_DROP)) + { + gi.bprintf(PRINT_HIGH, "%s turned %s quad drop.\n", + ent->client->pers.netname, settings->quaddrop ? "on" : "off"); + i = (int)dmflags->value; + + if (settings->quaddrop) + { + i |= DF_QUAD_DROP; + } + else + { + i &= ~DF_QUAD_DROP; + } + + sprintf(st, "%d", i); + gi.cvar_set("dmflags", st); + } + + if (settings->instantweap != !!((int)instantweap->value)) + { + gi.bprintf(PRINT_HIGH, "%s turned %s instant weapons.\n", + ent->client->pers.netname, settings->instantweap ? "on" : "off"); + sprintf(st, "%d", (int)settings->instantweap); + gi.cvar_set("instantweap", st); + } + + if (settings->matchlock != !!((int)matchlock->value)) + { + gi.bprintf(PRINT_HIGH, "%s turned %s match lock.\n", + ent->client->pers.netname, settings->matchlock ? "on" : "off"); + sprintf(st, "%d", (int)settings->matchlock); + gi.cvar_set("matchlock", st); + } + + 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 = 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 = 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 = 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 = p->arg; + + settings->weaponsstay = !settings->weaponsstay; + CTFAdmin_UpdateSettings(ent, p); +} + +void +CTFAdmin_ChangeInstantItems(edict_t *ent, pmenuhnd_t *p) +{ + admin_settings_t *settings = p->arg; + + settings->instantitems = !settings->instantitems; + CTFAdmin_UpdateSettings(ent, p); +} + +void +CTFAdmin_ChangeQuadDrop(edict_t *ent, pmenuhnd_t *p) +{ + admin_settings_t *settings = p->arg; + + settings->quaddrop = !settings->quaddrop; + CTFAdmin_UpdateSettings(ent, p); +} + +void +CTFAdmin_ChangeInstantWeap(edict_t *ent, pmenuhnd_t *p) +{ + admin_settings_t *settings = p->arg; + + settings->instantweap = !settings->instantweap; + CTFAdmin_UpdateSettings(ent, p); +} + +void +CTFAdmin_ChangeMatchLock(edict_t *ent, pmenuhnd_t *p) +{ + admin_settings_t *settings = p->arg; + + settings->matchlock = !settings->matchlock; + CTFAdmin_UpdateSettings(ent, p); +} + +void +CTFAdmin_UpdateSettings(edict_t *ent, pmenuhnd_t *setmenu) +{ + int i = 2; + char text[64]; + admin_settings_t *settings = setmenu->arg; + + sprintf(text, "Match Len: %2d mins", settings->matchlen); + PMenu_UpdateEntry(setmenu->entries + i, + text, + PMENU_ALIGN_LEFT, + CTFAdmin_ChangeMatchLen); + i++; + + sprintf(text, "Match Setup Len: %2d mins", settings->matchsetuplen); + PMenu_UpdateEntry(setmenu->entries + i, + text, + PMENU_ALIGN_LEFT, + CTFAdmin_ChangeMatchSetupLen); + i++; + + sprintf(text, "Match Start Len: %2d secs", settings->matchstartlen); + PMenu_UpdateEntry(setmenu->entries + i, + text, + PMENU_ALIGN_LEFT, + CTFAdmin_ChangeMatchStartLen); + i++; + + sprintf(text, "Weapons Stay: %s", settings->weaponsstay ? "Yes" : "No"); + PMenu_UpdateEntry(setmenu->entries + i, + text, + PMENU_ALIGN_LEFT, + CTFAdmin_ChangeWeapStay); + i++; + + sprintf(text, "Instant Items: %s", settings->instantitems ? "Yes" : "No"); + PMenu_UpdateEntry(setmenu->entries + i, + text, + PMENU_ALIGN_LEFT, + CTFAdmin_ChangeInstantItems); + i++; + + sprintf(text, "Quad Drop: %s", settings->quaddrop ? "Yes" : "No"); + PMenu_UpdateEntry(setmenu->entries + i, + text, + PMENU_ALIGN_LEFT, + CTFAdmin_ChangeQuadDrop); + i++; + + sprintf(text, "Instant Weapons: %s", settings->instantweap ? "Yes" : "No"); + PMenu_UpdateEntry(setmenu->entries + i, + text, + PMENU_ALIGN_LEFT, + CTFAdmin_ChangeInstantWeap); + i++; + + sprintf(text, "Match Lock: %s", settings->matchlock ? "Yes" : "No"); + PMenu_UpdateEntry(setmenu->entries + i, + text, + PMENU_ALIGN_LEFT, + CTFAdmin_ChangeMatchLock); + i++; + + PMenu_Update(ent); +} + +pmenu_t def_setmenu[] = { + {"*Settings Menu", PMENU_ALIGN_CENTER, NULL}, + {NULL, PMENU_ALIGN_CENTER, NULL}, + {NULL, PMENU_ALIGN_LEFT, NULL}, /* int matchlen; */ + {NULL, PMENU_ALIGN_LEFT, NULL}, /* int matchsetuplen; */ + {NULL, PMENU_ALIGN_LEFT, NULL}, /* int matchstartlen; */ + {NULL, PMENU_ALIGN_LEFT, NULL}, /* qboolean weaponsstay; */ + {NULL, PMENU_ALIGN_LEFT, NULL}, /* qboolean instantitems; */ + {NULL, PMENU_ALIGN_LEFT, NULL}, /* qboolean quaddrop; */ + {NULL, PMENU_ALIGN_LEFT, NULL}, /* qboolean instantweap; */ + {NULL, PMENU_ALIGN_LEFT, NULL}, /* qboolean matchlock; */ + {NULL, PMENU_ALIGN_LEFT, NULL}, + {"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 = malloc(sizeof(*settings)); + + settings->matchlen = matchtime->value; + settings->matchsetuplen = matchsetuptime->value; + settings->matchstartlen = matchstarttime->value; + settings->weaponsstay = !!((int)dmflags->value & DF_WEAPONS_STAY); + settings->instantitems = !!((int)dmflags->value & DF_INSTANT_ITEMS); + settings->quaddrop = !!((int)dmflags->value & DF_QUAD_DROP); + settings->instantweap = instantweap->value != 0; + settings->matchlock = matchlock->value != 0; + + menu = PMenu_Open(ent, def_setmenu, + -1, sizeof(def_setmenu) / sizeof(pmenu_t), + settings); + CTFAdmin_UpdateSettings(ent, menu); +} + +void +CTFAdmin_MatchSet(edict_t *ent, pmenuhnd_t *p) +{ + PMenu_Close(ent); + + if (ctfgame.match == MATCH_SETUP) + { + gi.bprintf(PRINT_CHAT, "Match has been forced to start.\n"); + ctfgame.match = MATCH_PREGAME; + ctfgame.matchtime = level.time + 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.bprintf(PRINT_CHAT, "Match has been forced to terminate.\n"); + ctfgame.match = MATCH_SETUP; + ctfgame.matchtime = level.time + matchsetuptime->value * 60; + CTFResetAllPlayers(); + } +} + +void +CTFAdmin_MatchMode(edict_t *ent, pmenuhnd_t *p) +{ + PMenu_Close(ent); + + if (ctfgame.match != MATCH_SETUP) + { + if (competition->value < 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.bprintf(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, NULL}, + {NULL, PMENU_ALIGN_CENTER, NULL}, /* blank */ + {"Settings", PMENU_ALIGN_LEFT, CTFAdmin_Settings}, + {NULL, PMENU_ALIGN_LEFT, NULL}, + {NULL, PMENU_ALIGN_LEFT, NULL}, + {"Cancel", PMENU_ALIGN_LEFT, CTFAdmin_Cancel}, + {NULL, PMENU_ALIGN_CENTER, NULL}, +}; + +void +CTFOpenAdminMenu(edict_t *ent) +{ + adminmenu[3].text = NULL; + adminmenu[3].SelectFunc = NULL; + adminmenu[4].text = NULL; + adminmenu[4].SelectFunc = NULL; + + if (ctfgame.match == MATCH_SETUP) + { + adminmenu[3].text = "Force start match"; + adminmenu[3].SelectFunc = CTFAdmin_MatchSet; + adminmenu[4].text = "Reset to pickup mode"; + adminmenu[4].SelectFunc = CTFAdmin_Reset; + } + else if ((ctfgame.match == MATCH_GAME) || (ctfgame.match == MATCH_PREGAME)) + { + adminmenu[3].text = "Cancel match"; + adminmenu[3].SelectFunc = CTFAdmin_MatchSet; + } + else if ((ctfgame.match == MATCH_NONE) && competition->value) + { + adminmenu[3].text = "Switch to match mode"; + adminmenu[3].SelectFunc = CTFAdmin_MatchMode; + } + + PMenu_Open(ent, adminmenu, -1, sizeof(adminmenu) / sizeof(pmenu_t), NULL); +} + +void +CTFAdmin(edict_t *ent) +{ + char text[1024]; + + if (!allow_admin->value) + { + gi.cprintf(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.bprintf(PRINT_HIGH, + "%s has become an admin.\n", + ent->client->pers.netname); + gi.cprintf(ent, + PRINT_HIGH, + "Type 'admin' to access the adminstration menu.\n"); + } + + if (!ent->client->resp.admin) + { + sprintf(text, "%s has requested admin rights.", + ent->client->pers.netname); + CTFBeginElection(ent, ELECT_ADMIN, text); + return; + } + + if (ent->client->menu) + { + PMenu_Close(ent); + } + + CTFOpenAdminMenu(ent); +} + +/*----------------------------------------------------------------*/ + +void +CTFStats(edict_t *ent) +{ + int i, e; + ghost_t *g; + char st[80]; + char text[1024]; + edict_t *e2; + + *text = 0; + + if (ctfgame.match == MATCH_SETUP) + { + for (i = 1; i <= maxclients->value; i++) + { + e2 = g_edicts + i; + + if (!e2->inuse) + { + continue; + } + + if (!e2->client->resp.ready && + (e2->client->resp.ctf_team != CTF_NOTEAM)) + { + sprintf(st, "%s is not ready.\n", e2->client->pers.netname); + + if (strlen(text) + strlen(st) < sizeof(text) - 50) + { + strcat(text, st); + } + } + } + } + + for (i = 0, g = ctfgame.ghosts; i < MAX_CLIENTS; i++, g++) + { + if (g->ent) + { + break; + } + } + + if (i == MAX_CLIENTS) + { + if (*text) + { + gi.cprintf(ent, PRINT_HIGH, "%s", text); + } + + gi.cprintf(ent, PRINT_HIGH, "No statistics available.\n"); + return; + } + + strcat(text, " #|Name |Score|Kills|Death|BasDf|CarDf|Effcy|\n"); + + for (i = 0, g = ctfgame.ghosts; i < MAX_CLIENTS; i++, g++) + { + if (!*g->netname) + { + continue; + } + + if (g->deaths + g->kills == 0) + { + e = 50; + } + else + { + e = g->kills * 100 / (g->kills + g->deaths); + } + + sprintf(st, "%3d|%-16.16s|%5d|%5d|%5d|%5d|%5d|%4d%%|\n", + g->number, + g->netname, + g->score, + g->kills, + g->deaths, + g->basedef, + g->carrierdef, + e); + + if (strlen(text) + strlen(st) > sizeof(text) - 50) + { + sprintf(text + strlen(text), "And more...\n"); + gi.cprintf(ent, PRINT_HIGH, "%s", text); + return; + } + + strcat(text, st); + } + + gi.cprintf(ent, PRINT_HIGH, "%s", text); +} + +void +CTFPlayerList(edict_t *ent) +{ + int i; + char st[80]; + char text[1400]; + edict_t *e2; + + /* number, name, connect time, ping, score, admin */ + + *text = 0; + + for (i = 1; i <= maxclients->value; i++) + { + e2 = g_edicts + i; + + if (!e2->inuse) + { + continue; + } + + Com_sprintf( st, sizeof(st), "%3d %-16.16s %02d:%02d %4d %3d%s%s\n", + i, e2->client->pers.netname, (level.framenum - e2->client->resp.enterframe) / 600, + ((level.framenum - e2->client->resp.enterframe) % 600) / 10, + 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 (strlen(text) + strlen(st) > sizeof(text) - 50) + { + sprintf(text + strlen(text), "And more...\n"); + gi.cprintf(ent, PRINT_HIGH, "%s", text); + return; + } + + strcat(text, st); + } + + gi.cprintf(ent, PRINT_HIGH, "%s", text); +} + +void +CTFWarp(edict_t *ent) +{ + char text[1024]; + char *mlist, *token; + static const char *seps = " \t\n\r"; + + if (gi.argc() < 2) + { + gi.cprintf(ent, PRINT_HIGH, "Where do you want to warp to?\n"); + gi.cprintf(ent, PRINT_HIGH, "Available levels are: %s\n", + warp_list->string); + return; + } + + mlist = strdup(warp_list->string); + + token = strtok(mlist, seps); + + while (token != NULL) + { + if (Q_stricmp(token, gi.argv(1)) == 0) + { + break; + } + + token = strtok(NULL, seps); + } + + if (token == NULL) + { + gi.cprintf(ent, PRINT_HIGH, "Unknown CTF level.\n"); + gi.cprintf(ent, PRINT_HIGH, "Available levels are: %s\n", + warp_list->string); + free(mlist); + return; + } + + free(mlist); + + if (ent->client->resp.admin) + { + gi.bprintf(PRINT_HIGH, "%s is warping to level %s.\n", + ent->client->pers.netname, gi.argv(1)); + strncpy(level.forcemap, gi.argv(1), sizeof(level.forcemap) - 1); + EndDMLevel(); + return; + } + + sprintf(text, "%s has requested warping to level %s.", + ent->client->pers.netname, gi.argv(1)); + + if (CTFBeginElection(ent, ELECT_MAP, text)) + { + strncpy(ctfgame.elevel, gi.argv(1), sizeof(ctfgame.elevel) - 1); + } +} + +void +CTFBoot(edict_t *ent) +{ + int i; + edict_t *targ; + char text[80]; + + if (!ent->client->resp.admin) + { + gi.cprintf(ent, PRINT_HIGH, "You are not an admin.\n"); + return; + } + + if (gi.argc() < 2) + { + gi.cprintf(ent, PRINT_HIGH, "Who do you want to kick?\n"); + return; + } + + if ((*gi.argv(1) < '0') && (*gi.argv(1) > '9')) + { + gi.cprintf(ent, PRINT_HIGH, "Specify the player number to kick.\n"); + return; + } + + i = atoi(gi.argv(1)); + + if ((i < 1) || (i > maxclients->value)) + { + gi.cprintf(ent, PRINT_HIGH, "Invalid player number.\n"); + return; + } + + targ = g_edicts + i; + + if (!targ->inuse) + { + gi.cprintf(ent, PRINT_HIGH, "That player number is not connected.\n"); + return; + } + + sprintf(text, "kick %d\n", i - 1); + gi.AddCommandString(text); +} + +void +CTFSetPowerUpEffect(edict_t *ent, int def) +{ + if (ent->client->resp.ctf_team == CTF_TEAM1) + { + ent->s.effects |= EF_PENT; /* red */ + } + else if (ent->client->resp.ctf_team == CTF_TEAM2) + { + ent->s.effects |= EF_QUAD; /* red */ + } + else + { + ent->s.effects |= def; + } +} + +#endif /* CTF */ + diff --git a/src/game/baseq2/g_ctf.h b/src/game/baseq2/g_ctf.h new file mode 100644 index 00000000..fcd6df44 --- /dev/null +++ b/src/game/baseq2/g_ctf.h @@ -0,0 +1,209 @@ +/* + * Copyright (C) 1997-2001 Id Software, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * ======================================================================= + * + * Header file for the CTF code. Included at the end of g_local.h. + * + * ======================================================================= + */ + +#ifdef CTF + +#ifndef G_CTF_H +#define G_CTF_H + +#define CTF_VERSION 1.52 +#define CTF_VSTRING2(x) # x +#define CTF_VSTRING(x) CTF_VSTRING2(x) +#define CTF_STRING_VERSION CTF_VSTRING(CTF_VERSION) + +#define STAT_CTF_TEAM1_PIC 17 +#define STAT_CTF_TEAM1_CAPS 18 +#define STAT_CTF_TEAM2_PIC 19 +#define STAT_CTF_TEAM2_CAPS 20 +#define STAT_CTF_FLAG_PIC 21 +#define STAT_CTF_JOINED_TEAM1_PIC 22 +#define STAT_CTF_JOINED_TEAM2_PIC 23 +#define STAT_CTF_TEAM1_HEADER 24 +#define STAT_CTF_TEAM2_HEADER 25 +#define STAT_CTF_TECH 26 +#define STAT_CTF_ID_VIEW 27 +#define STAT_CTF_MATCH 28 +#define STAT_CTF_ID_VIEW_COLOR 29 +#define STAT_CTF_TEAMINFO 30 + +#define CONFIG_CTF_MATCH (CS_AIRACCEL - 1) +#define CONFIG_CTF_TEAMINFO (CS_AIRACCEL - 2) + +typedef enum +{ + CTF_NOTEAM, + CTF_TEAM1, + CTF_TEAM2 +} ctfteam_t; + +typedef enum +{ + CTF_GRAPPLE_STATE_FLY, + CTF_GRAPPLE_STATE_PULL, + CTF_GRAPPLE_STATE_HANG +} ctfgrapplestate_t; + +typedef struct ghost_s +{ + char netname[16]; + int number; + + /* stats */ + int deaths; + int kills; + int caps; + int basedef; + int carrierdef; + + int code; /* ghost code */ + int team; /* team */ + int score; /* frags at time of disconnect */ + edict_t *ent; +} ghost_t; + +extern cvar_t *ctf; + +#define CTF_TEAM1_SKIN "ctf_r" +#define CTF_TEAM2_SKIN "ctf_b" + +#define DF_CTF_FORCEJOIN 131072 +#define DF_ARMOR_PROTECT 262144 +#define DF_CTF_NO_TECH 524288 + +#define CTF_CAPTURE_BONUS 15 /* what you get for capture */ +#define CTF_TEAM_BONUS 10 /* what your team gets for capture */ +#define CTF_RECOVERY_BONUS 1 /* what you get for recovery */ +#define CTF_FLAG_BONUS 0 /* what you get for picking up enemy flag */ +#define CTF_FRAG_CARRIER_BONUS 2 /* what you get for fragging enemy flag carrier */ +#define CTF_FLAG_RETURN_TIME 40 /* seconds until auto return */ + +#define CTF_CARRIER_DANGER_PROTECT_BONUS 2 /* bonus for fraggin someone who has recently hurt your flag carrier */ +#define CTF_CARRIER_PROTECT_BONUS 1 /* bonus for fraggin someone while either you or your target are near your flag carrier */ +#define CTF_FLAG_DEFENSE_BONUS 1 /* bonus for fraggin someone while either you or your target are near your flag */ +#define CTF_RETURN_FLAG_ASSIST_BONUS 1 /* awarded for returning a flag that causes a capture to happen almost immediately */ +#define CTF_FRAG_CARRIER_ASSIST_BONUS 2 /* award for fragging a flag carrier if a capture happens almost immediately */ + +#define CTF_TARGET_PROTECT_RADIUS 400 /* the radius around an object being defended where a target will be worth extra frags */ +#define CTF_ATTACKER_PROTECT_RADIUS 400 /* the radius around an object being defended where an attacker will get extra frags when making kills */ + +#define CTF_CARRIER_DANGER_PROTECT_TIMEOUT 8 +#define CTF_FRAG_CARRIER_ASSIST_TIMEOUT 10 +#define CTF_RETURN_FLAG_ASSIST_TIMEOUT 10 + +#define CTF_AUTO_FLAG_RETURN_TIMEOUT 30 /* number of seconds before dropped flag auto-returns */ + +#define CTF_TECH_TIMEOUT 60 /* seconds before techs spawn again */ + +#define CTF_GRAPPLE_SPEED 650 /* speed of grapple in flight */ +#define CTF_GRAPPLE_PULL_SPEED 650 /* speed player is pulled at */ + +void CTFInit(void); +void CTFSpawn(void); +void CTFPrecache(void); + +void SP_info_player_team1(edict_t *self); +void SP_info_player_team2(edict_t *self); + +char *CTFTeamName(int team); +char *CTFOtherTeamName(int team); +void CTFAssignSkin(edict_t *ent, char *s); +void CTFAssignTeam(gclient_t *who); +edict_t *SelectCTFSpawnPoint(edict_t *ent); +qboolean CTFPickup_Flag(edict_t *ent, edict_t *other); +void CTFDrop_Flag(edict_t *ent, gitem_t *item); +void CTFEffects(edict_t *player); +void CTFCalcScores(void); +void SetCTFStats(edict_t *ent); +void CTFDeadDropFlag(edict_t *self); +void CTFScoreboardMessage(edict_t *ent, edict_t *killer); +void CTFTeam_f(edict_t *ent); +void CTFID_f(edict_t *ent); +void CTFSay_Team(edict_t *who, char *msg); +void CTFFlagSetup(edict_t *ent); +void CTFResetFlag(int ctf_team); +void CTFFragBonuses(edict_t *targ, edict_t *inflictor, edict_t *attacker); +void CTFCheckHurtCarrier(edict_t *targ, edict_t *attacker); + +/* GRAPPLE */ +void CTFWeapon_Grapple(edict_t *ent); +void CTFPlayerResetGrapple(edict_t *ent); +void CTFGrapplePull(edict_t *self); +void CTFResetGrapple(edict_t *self); + +/* TECH */ +gitem_t *CTFWhat_Tech(edict_t *ent); +qboolean CTFPickup_Tech(edict_t *ent, edict_t *other); +void CTFDrop_Tech(edict_t *ent, gitem_t *item); +void CTFDeadDropTech(edict_t *ent); +void CTFSetupTechSpawn(void); +int CTFApplyResistance(edict_t *ent, int dmg); +int CTFApplyStrength(edict_t *ent, int dmg); +qboolean CTFApplyStrengthSound(edict_t *ent); +qboolean CTFApplyHaste(edict_t *ent); +void CTFApplyHasteSound(edict_t *ent); +void CTFApplyRegeneration(edict_t *ent); +qboolean CTFHasRegeneration(edict_t *ent); +void CTFRespawnTech(edict_t *ent); +void CTFResetTech(void); + +void CTFOpenJoinMenu(edict_t *ent); +qboolean CTFStartClient(edict_t *ent); +void CTFVoteYes(edict_t *ent); +void CTFVoteNo(edict_t *ent); +void CTFReady(edict_t *ent); +void CTFNotReady(edict_t *ent); +qboolean CTFNextMap(void); +qboolean CTFMatchSetup(void); +qboolean CTFMatchOn(void); +void CTFGhost(edict_t *ent); +void CTFAdmin(edict_t *ent); +qboolean CTFInMatch(void); +void CTFStats(edict_t *ent); +void CTFWarp(edict_t *ent); +void CTFBoot(edict_t *ent); +void CTFPlayerList(edict_t *ent); + +qboolean CTFCheckRules(void); + +void SP_misc_ctf_banner(edict_t *ent); +void SP_misc_ctf_small_banner(edict_t *ent); + +extern char *ctf_statusbar; + +void UpdateChaseCam(edict_t *ent); +void ChaseNext(edict_t *ent); +void ChasePrev(edict_t *ent); + +void CTFObserver(edict_t *ent); + +void SP_trigger_teleport(edict_t *ent); +void SP_info_teleport_destination(edict_t *ent); + +void CTFSetPowerUpEffect(edict_t *ent, int def); + +#endif /* G_CTF_H */ +#endif /* CTF */ + diff --git a/src/game/baseq2/g_local.h b/src/game/baseq2/g_local.h index b100b112..334ea89e 100644 --- a/src/game/baseq2/g_local.h +++ b/src/game/baseq2/g_local.h @@ -24,6 +24,9 @@ * ======================================================================= */ +#ifndef G_LOCAL_H +#define G_LOCAL_H + #include "q_shared.h" /* define GAME_INCLUDE so that game.h does not define the @@ -1124,3 +1127,5 @@ struct edict_s #include "g_ctf.h" #endif +#endif /* G_LOCAL_H */ + diff --git a/src/game/baseq2/game.h b/src/game/baseq2/game.h index 1d8067a3..b3f1ea63 100644 --- a/src/game/baseq2/game.h +++ b/src/game/baseq2/game.h @@ -34,6 +34,9 @@ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ +#ifndef GAME_H +#define GAME_H + #define GAME_API_VERSION 3 #define SVF_NOCLIENT 0x00000001 /* don't send entity to clients, even if it has effects */ @@ -242,3 +245,5 @@ typedef struct game_export_t *GetGameApi(game_import_t *import); +#endif /* GAME_H */ + diff --git a/src/game/baseq2/p_menu.c b/src/game/baseq2/p_menu.c new file mode 100644 index 00000000..fadbc5b5 --- /dev/null +++ b/src/game/baseq2/p_menu.c @@ -0,0 +1,415 @@ +/* + * Copyright (C) 1997-2001 Id Software, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * ======================================================================= + * + * The CTF menu. + * + * ======================================================================= + */ + +#ifdef CTF + +#include "g_local.h" + +/* + * Note that the pmenu entries are duplicated + * this is so that a static set of pmenu entries + * can be used for multiple clients and changed + * without interference. Note that arg will be + * freed when the menu is closed, it must be + * allocated memory. + */ +pmenuhnd_t * +PMenu_Open(edict_t *ent, pmenu_t *entries, int cur, int num, void *arg) +{ + pmenuhnd_t *hnd; + pmenu_t *p; + int i; + + if (!ent || !entries || !arg) + { + return NULL; + } + + if (!ent->client) + { + return NULL; + } + + if (ent->client->menu) + { + gi.dprintf("warning, ent already has a menu\n"); + PMenu_Close(ent); + } + + hnd = malloc(sizeof(*hnd)); + + hnd->arg = arg; + hnd->entries = malloc(sizeof(pmenu_t) * num); + memcpy(hnd->entries, entries, sizeof(pmenu_t) * num); + + /* duplicate the strings since they may be from static memory */ + for (i = 0; i < num; i++) + { + if (entries[i].text) + { + hnd->entries[i].text = strdup(entries[i].text); + } + } + + hnd->num = num; + + if ((cur < 0) || !entries[cur].SelectFunc) + { + for (i = 0, p = entries; i < num; i++, p++) + { + if (p->SelectFunc) + { + break; + } + } + } + else + { + i = cur; + } + + if (i >= num) + { + hnd->cur = -1; + } + else + { + hnd->cur = i; + } + + ent->client->showscores = true; + ent->client->inmenu = true; + ent->client->menu = hnd; + + PMenu_Do_Update(ent); + gi.unicast(ent, true); + + return hnd; +} + +void +PMenu_Close(edict_t *ent) +{ + int i; + pmenuhnd_t *hnd; + + if (!ent) + { + return; + } + + if (!ent->client->menu) + { + return; + } + + hnd = ent->client->menu; + + for (i = 0; i < hnd->num; i++) + { + if (hnd->entries[i].text) + { + free(hnd->entries[i].text); + } + } + + free(hnd->entries); + + if (hnd->arg) + { + free(hnd->arg); + } + + free(hnd); + ent->client->menu = NULL; + ent->client->showscores = false; +} + +/* + * only use on pmenu's that have + * been called with PMenu_Open + * */ +void +PMenu_UpdateEntry(pmenu_t *entry, const char *text, + int align, SelectFunc_t SelectFunc) +{ + if (!entry || !text) + { + return; + } + + if (entry->text) + { + free(entry->text); + } + + entry->text = strdup(text); + entry->align = align; + entry->SelectFunc = SelectFunc; +} + +void +PMenu_Do_Update(edict_t *ent) +{ + char string[1400]; + int i; + pmenu_t *p; + int x; + pmenuhnd_t *hnd; + char *t; + qboolean alt = false; + + if (!ent) + { + return; + } + + if (!ent->client->menu) + { + gi.dprintf("warning: ent has no menu\n"); + return; + } + + hnd = ent->client->menu; + + strcpy(string, "xv 32 yv 8 picn inventory "); + + for (i = 0, p = hnd->entries; i < hnd->num; i++, p++) + { + if (!p->text || !*(p->text)) + { + continue; /* blank line */ + } + + t = p->text; + + if (*t == '*') + { + alt = true; + t++; + } + + sprintf(string + strlen(string), "yv %d ", 32 + i * 8); + + if (p->align == PMENU_ALIGN_CENTER) + { + x = 196 / 2 - strlen(t) * 4 + 64; + } + else if (p->align == PMENU_ALIGN_RIGHT) + { + x = 64 + (196 - strlen(t) * 8); + } + else + { + x = 64; + } + + sprintf(string + strlen(string), "xv %d ", + x - ((hnd->cur == i) ? 8 : 0)); + + if (hnd->cur == i) + { + sprintf(string + strlen(string), "string2 \"\x0d%s\" ", t); + } + else if (alt) + { + sprintf(string + strlen(string), "string2 \"%s\" ", t); + } + else + { + sprintf(string + strlen(string), "string \"%s\" ", t); + } + + alt = false; + } + + gi.WriteByte(svc_layout); + gi.WriteString(string); +} + +void +PMenu_Update(edict_t *ent) +{ + if (!ent) + { + return; + } + + if (!ent->client->menu) + { + gi.dprintf("warning: ent has no menu\n"); + return; + } + + if (level.time - ent->client->menutime >= 1.0) + { + /* been a second or more since + last update, update now */ + PMenu_Do_Update(ent); + gi.unicast(ent, true); + ent->client->menutime = level.time; + ent->client->menudirty = false; + } + + ent->client->menutime = level.time + 0.2; + ent->client->menudirty = true; +} + +void +PMenu_Next(edict_t *ent) +{ + pmenuhnd_t *hnd; + int i; + pmenu_t *p; + + if (!ent) + { + return; + } + + if (!ent->client->menu) + { + gi.dprintf("warning: ent has no menu\n"); + return; + } + + hnd = ent->client->menu; + + if (hnd->cur < 0) + { + return; /* no selectable entries */ + } + + i = hnd->cur; + p = hnd->entries + hnd->cur; + + do + { + i++, p++; + + if (i == hnd->num) + { + i = 0, p = hnd->entries; + } + + if (p->SelectFunc) + { + break; + } + } + while (i != hnd->cur); + + hnd->cur = i; + + PMenu_Update(ent); +} + +void +PMenu_Prev(edict_t *ent) +{ + pmenuhnd_t *hnd; + int i; + pmenu_t *p; + + if (!ent) + { + return; + } + + if (!ent->client->menu) + { + gi.dprintf("warning: ent has no menu\n"); + return; + } + + hnd = ent->client->menu; + + if (hnd->cur < 0) + { + return; /* no selectable entries */ + } + + i = hnd->cur; + p = hnd->entries + hnd->cur; + + do + { + if (i == 0) + { + i = hnd->num - 1; + p = hnd->entries + i; + } + else + { + i--, p--; + } + + if (p->SelectFunc) + { + break; + } + } + while (i != hnd->cur); + + hnd->cur = i; + + PMenu_Update(ent); +} + +void +PMenu_Select(edict_t *ent) +{ + pmenuhnd_t *hnd; + pmenu_t *p; + + if (!ent) + { + return; + } + + if (!ent->client->menu) + { + gi.dprintf("warning: ent has no menu\n"); + return; + } + + hnd = ent->client->menu; + + if (hnd->cur < 0) + { + return; /* no selectable entries */ + } + + p = hnd->entries + hnd->cur; + + if (p->SelectFunc) + { + p->SelectFunc(ent, hnd); + } +} + +#endif /* CTF */ + diff --git a/src/game/baseq2/p_menu.h b/src/game/baseq2/p_menu.h new file mode 100644 index 00000000..bd52cf64 --- /dev/null +++ b/src/game/baseq2/p_menu.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 1997-2001 Id Software, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * ======================================================================= + * + * Header file for the CTF menu. + * + * ======================================================================= + */ + +#ifdef CTF + +#ifndef P_MENU_H +#define P_MENU_H + +enum +{ + PMENU_ALIGN_LEFT, + PMENU_ALIGN_CENTER, + PMENU_ALIGN_RIGHT +}; + +typedef struct pmenuhnd_s +{ + struct pmenu_s *entries; + int cur; + int num; + void *arg; +} pmenuhnd_t; + +typedef void (*SelectFunc_t)(edict_t *ent, pmenuhnd_t *hnd); + +typedef struct pmenu_s +{ + char *text; + int align; + SelectFunc_t SelectFunc; +} pmenu_t; + +pmenuhnd_t *PMenu_Open(edict_t *ent, pmenu_t *entries, int cur, + int num, void *arg); +void PMenu_Close(edict_t *ent); +void PMenu_UpdateEntry(pmenu_t *entry, const char *text, int align, + SelectFunc_t SelectFunc); +void PMenu_Do_Update(edict_t *ent); +void PMenu_Update(edict_t *ent); +void PMenu_Next(edict_t *ent); +void PMenu_Prev(edict_t *ent); +void PMenu_Select(edict_t *ent); + +#endif /* P_MENU_H */ +#endif /* CTF */ + diff --git a/src/game/baseq2/q_shared.h b/src/game/baseq2/q_shared.h index c744ab10..bb99b5cc 100644 --- a/src/game/baseq2/q_shared.h +++ b/src/game/baseq2/q_shared.h @@ -25,6 +25,9 @@ * ======================================================================= */ +#ifndef QSHARED_H +#define QSHARED_H + #include #include #include @@ -1077,3 +1080,5 @@ extern int vidref_val; size_t verify_fread(void *, size_t, size_t, FILE *); size_t verify_fwrite(void *, size_t, size_t, FILE *); +#endif /* QSHARED_H */ +