--- /dev/null
+#include "gamemode_survival.qh"
+
+#include <common/mutators/mutator/overkill/hmg.qh>
+#include <common/mutators/mutator/overkill/rpc.qh>
+
+//============================ Constants ======================================
+
+const int SURVIVAL_TEAM_1_BIT = BIT(0); ///< Bitmask of the first team.
+const int SURVIVAL_TEAM_2_BIT = BIT(1); ///< Bitmask of the second team.
+
+/// \brief Used when bitfield team count is requested.
+const int SURVIVAL_TEAM_BITS = 3;
+
+enum
+{
+ SURVIVAL_TYPE_COOP, ///< All humans are on the defender team.
+ SURVIVAL_TYPE_VERSUS ///< Humans take turns between attackers and defenders.
+};
+
+const string SURVIVAL_TYPE_COOP_VALUE = "coop"; ///< Cvar value for coop.
+const string SURVIVAL_TYPE_VERSUS_VALUE = "versus"; ///< Cvar value for versus.
+
+enum
+{
+ /// \brief First round where there is timelimit set by the server.
+ SURVIVAL_ROUND_FIRST,
+ /// \brief Second round where defender team tries to survive for the first
+ /// round's time.
+ SURVIVAL_ROUND_SECOND
+};
+
+enum
+{
+ /// \brief Player is spectating and has no intention of playing.
+ SURVIVAL_STATE_NOT_PLAYING,
+ /// \brief Player is playing the game.
+ SURVIVAL_STATE_PLAYING = 1
+};
+
+enum
+{
+ SURVIVAL_ROLE_NONE, ///< Player is not playing.
+ SURVIVAL_ROLE_PLAYER, ///< Player is an attacker or defender.
+ SURVIVAL_ROLE_CANNON_FODDER ///< Player is a cannon fodder.
+};
+
+SOUND(SURV_3_FRAGS_LEFT, "announcer/default/3fragsleft");
+SOUND(SURV_2_FRAGS_LEFT, "announcer/default/2fragsleft");
+SOUND(SURV_1_FRAG_LEFT, "announcer/default/1fragleft");
+
+SOUND(SURV_RED_SCORES, "ctf/red_capture");
+SOUND(SURV_BLUE_SCORES, "ctf/blue_capture");
+
+//======================= Global variables ====================================
+
+float autocvar_g_surv_warmup; ///< Warmup time in seconds.
+float autocvar_g_surv_round_timelimit; ///< First round time limit in seconds.
+
+int autocvar_g_surv_point_limit; ///< Maximum number of points.
+int autocvar_g_surv_point_leadlimit; ///< Maximum lead of a single team.
+
+/// \brief How much players are allowed in teams (excluding cannon fodder).
+int autocvar_g_surv_team_size;
+/// \brief If set, defenders will not be shown on the radar.
+int autocvar_g_surv_stealth;
+/// \brief Whether to allow spectating enemy players while dead.
+bool autocvar_g_surv_spectate_enemies;
+
+/// \brief Whether to force overkill player models for attackers.
+int autocvar_g_surv_attacker_force_overkill_models;
+/// \brief Whether to force overkill player models for defenders.
+int autocvar_g_surv_defender_force_overkill_models;
+/// \brief Whether to force overkill player models for cannon fodder.
+int autocvar_g_surv_cannon_fodder_force_overkill_models;
+
+/// \brief How much score attackers gain per 1 point of damage.
+float autocvar_g_surv_attacker_damage_score;
+
+/// \brief How much score attackers get for fragging defenders.
+float autocvar_g_surv_attacker_frag_score;
+
+/// \brief How much health do defenders get when they frag an attacker.
+int autocvar_g_surv_defender_attacker_frag_health;
+/// \brief How much armor do defenders get when they frag an attacker.
+int autocvar_g_surv_defender_attacker_frag_armor;
+/// \brief How many shells do defenders get when they frag an attacker.
+int autocvar_g_surv_defender_attacker_frag_shells;
+/// \brief How many bullets do defenders get when they frag an attacker.
+int autocvar_g_surv_defender_attacker_frag_bullets;
+/// \brief How many rockets do defenders get when they frag an attacker.
+int autocvar_g_surv_defender_attacker_frag_rockets;
+/// \brief How many cells do defenders get when they frag an attacker.
+int autocvar_g_surv_defender_attacker_frag_cells;
+/// \brief How much plasma do defenders get when they frag an attacker.
+int autocvar_g_surv_defender_attacker_frag_plasma;
+/// \brief How much fuel do defenders get when they frag an attacker.
+int autocvar_g_surv_defender_attacker_frag_fuel;
+/// \brief How much health do defenders get when they frag cannon fodder.
+int autocvar_g_surv_defender_cannon_fodder_frag_health;
+/// \brief How much armor do defenders get when they frag cannon fodder.
+int autocvar_g_surv_defender_cannon_fodder_frag_armor;
+/// \brief How many shells do defenders get when they frag cannon fodder.
+int autocvar_g_surv_defender_cannon_fodder_frag_shells;
+/// \brief How many bullets do defenders get when they frag cannon fodder.
+int autocvar_g_surv_defender_cannon_fodder_frag_bullets;
+/// \brief How many rockets do defenders get when they frag cannon fodder.
+int autocvar_g_surv_defender_cannon_fodder_frag_rockets;
+/// \brief How many cells do defenders get when they frag cannon fodder.
+int autocvar_g_surv_defender_cannon_fodder_frag_cells;
+/// \brief How much plasma do defenders get when they frag cannon fodder.
+int autocvar_g_surv_defender_cannon_fodder_frag_plasma;
+/// \brief How much fuel do defenders get when they frag cannon fodder.
+int autocvar_g_surv_defender_cannon_fodder_frag_fuel;
+
+/// \brief A stat that is used to track the time left in the round.
+.float surv_round_time_stat = _STAT(SURV_ROUND_TIME);
+/// \brief A stat that is used to track defender team.
+.int surv_defender_team_stat = _STAT(SURV_DEFENDER_TEAM);
+/// \brief A stat that is used to track number of defenders alive.
+.int surv_defenders_alive_stat = _STAT(SURV_DEFENDERS_ALIVE);
+/// \brief A stat that is used to track the total health of defenders.
+.float surv_defender_health_stat = _STAT(SURV_DEFENDER_HEALTH);
+
+/// \brief Holds the state of the player. See SURVIVAL_STATE constants.
+.int surv_state;
+/// \brief Holds the role of the player. See SURVIVAL_ROLE constants.
+.int surv_role;
+.string surv_savedplayermodel; ///< Initial player model.
+/// \brief Player state used during replacement of bot player with real player.
+.entity surv_savedplayerstate;
+.string surv_playermodel; ///< Player model forced by the game.
+
+.entity surv_attack_sprite; ///< Holds the sprite telling attackers to attack.
+.entity surv_defend_sprite; ///< Holds the sprite telling defenders to defend.
+
+int surv_type; ///< Holds the type of survival. See SURVIVAL_TYPE constants.
+bool surv_warmup; ///< Holds whether warmup is active.
+/// \brief Holds the type of the current round. See SURVIVAL_ROUND constants.
+int surv_roundtype;
+bool surv_isroundactive; ///< Holds whether the round is active.
+float surv_roundstarttime; ///< Holds the time of the round start.
+/// \brief Holds the time needed to survive in the second round.
+float surv_timetobeat;
+
+int surv_attackerteam; ///< Holds the attacker team.
+int surv_defenderteam; ///< Holds the defender team.
+
+int surv_attackerteambit; ///< Hols the attacker team bitmask.
+int surv_defenderteambit; ///< Holds the defender team bitmask.
+
+int surv_numattackers; ///< Holds the number of players in attacker team.
+int surv_numdefenders; ///< Holds the number of players in defender team.
+
+/// \brief Holds the number of humans in attacker team.
+int surv_numattackerhumans;
+/// \brief Holds the number of humans in defender team.
+int surv_numdefenderhumans;
+
+/// \brief Holds the number of attacker players that are alive.
+int surv_numattackersalive;
+/// \brief Holds the number of defender players that are alive.
+int surv_numdefendersalive;
+
+bool surv_autobalance; ///< Holds whether autobalance is active.
+bool surv_announcefrags; ///< Holds whether remaining frags must be announced.
+bool surv_allowed_to_spawn; ///< Holds whether players are allowed to spawn.
+
+//====================== Forward declarations =================================
+
+/// \brief Determines whether the round can start.
+/// \return True if the round can start, false otherwise.
+bool Surv_CanRoundStart();
+
+/// \brief Determines whether the round can end.
+/// \return True if the round can end, false otherwise.
+bool Surv_CanRoundEnd();
+
+/// \brief Called when the round starts.
+/// \return No return.
+void Surv_RoundStart();
+
+/// \brief Returns whether player has been eliminated.
+/// \param[in] player Player to check.
+/// \return True if player is eliminated, false otherwise.
+bool Surv_IsEliminated(entity player);
+
+/// \brief Updates stats of team count on HUD.
+/// \return No return.
+void Surv_UpdateTeamStats();
+
+/// \brief Updates stats of alive players on HUD.
+/// \return No return.
+void Surv_UpdateAliveStats();
+
+/// \brief Updates defender health on the HUD.
+/// \return No return.
+void Surv_UpdateDefenderHealthStat();
+
+/// \brief Updates the health of defender sprite.
+/// \param[in,out] player Player that has the sprite.
+/// \return No return.
+void Surv_UpdateWaypointSpriteHealth(entity player);
+
+//========================= Free functions ====================================
+
+void Surv_Initialize()
+{
+ switch (cvar_string("g_surv_type"))
+ {
+ case SURVIVAL_TYPE_COOP_VALUE:
+ {
+ surv_type = SURVIVAL_TYPE_COOP;
+ break;
+ }
+ case SURVIVAL_TYPE_VERSUS_VALUE:
+ {
+ surv_type = SURVIVAL_TYPE_VERSUS;
+ break;
+ }
+ default:
+ {
+ error("Invalid survival type.");
+ }
+ }
+ surv_roundtype = SURVIVAL_ROUND_FIRST;
+ surv_isroundactive = false;
+ surv_roundstarttime = time;
+ surv_timetobeat = autocvar_g_surv_round_timelimit;
+ if (random() < 0.5)
+ {
+ surv_attackerteam = NUM_TEAM_1;
+ surv_defenderteam = NUM_TEAM_2;
+ surv_attackerteambit = SURVIVAL_TEAM_1_BIT;
+ surv_defenderteambit = SURVIVAL_TEAM_2_BIT;
+ }
+ else
+ {
+ surv_attackerteam = NUM_TEAM_2;
+ surv_defenderteam = NUM_TEAM_1;
+ surv_attackerteambit = SURVIVAL_TEAM_2_BIT;
+ surv_defenderteambit = SURVIVAL_TEAM_1_BIT;
+ }
+ surv_numattackers = 0;
+ surv_numdefenders = 0;
+ surv_numattackerhumans = 0;
+ surv_numdefenderhumans = 0;
+ surv_numattackersalive = 0;
+ surv_numdefendersalive = 0;
+ surv_autobalance = true;
+ surv_announcefrags = true;
+ surv_allowed_to_spawn = true;
+ precache_all_playermodels("models/ok_player/*.dpm");
+ ScoreRules_basics(SURVIVAL_TEAM_BITS, SFL_SORT_PRIO_PRIMARY, 0, true);
+ ScoreInfo_SetLabel_TeamScore(1, "rounds", SFL_SORT_PRIO_PRIMARY);
+ ScoreRules_basics_end();
+ round_handler_Spawn(Surv_CanRoundStart, Surv_CanRoundEnd, Surv_RoundStart);
+ round_handler_Init(5, autocvar_g_surv_warmup, surv_timetobeat);
+ EliminatedPlayers_Init(Surv_IsEliminated);
+ GameRules_teams(true);
+ GameRules_limit_score(autocvar_g_surv_point_limit);
+ GameRules_limit_lead(autocvar_g_surv_point_leadlimit);
+}
+
+/// \brief Returns the name of the template of the given player.
+/// \param[in] player Player to inspect.
+/// \return Name of the template of the given player.
+string Surv_GetPlayerTemplate(entity player)
+{
+ switch (player.team)
+ {
+ case surv_attackerteam:
+ {
+ switch (player.surv_role)
+ {
+ case SURVIVAL_ROLE_NONE:
+ case SURVIVAL_ROLE_PLAYER:
+ {
+ return cvar_string("g_surv_attacker_template");
+ }
+ case SURVIVAL_ROLE_CANNON_FODDER:
+ {
+ return cvar_string("g_surv_cannon_fodder_template");
+ }
+ }
+ }
+ case surv_defenderteam:
+ {
+ return cvar_string("g_surv_defender_template");
+ }
+ }
+ return "default";
+}
+
+/// \brief Saves the player state. Used to seamlessly swap bots with humans.
+/// \param[in] player Player to save the state of.
+/// \return Entity containing the player state.
+entity Surv_SavePlayerState(entity player)
+{
+ entity state = spawn();
+ state.origin = player.origin;
+ state.velocity = player.velocity;
+ state.angles = player.angles;
+ state.health = player.health;
+ state.armorvalue = player.armorvalue;
+ state.ammo_shells = player.ammo_shells;
+ state.ammo_nails = player.ammo_nails;
+ state.ammo_rockets = player.ammo_rockets;
+ state.ammo_cells = player.ammo_cells;
+ state.weapons = player.weapons;
+ state.items = player.items;
+ state.superweapons_finished = player.superweapons_finished;
+ return state;
+}
+
+/// \brief Restores a saved player state.
+/// \param[in] player Player to restore the state of.
+/// \param[in] st State to restore.
+/// \return No return.
+void Surv_RestorePlayerState(entity player, entity st)
+{
+ player.origin = st.origin;
+ player.velocity = st.velocity;
+ player.angles = st.angles;
+ player.health = st.health;
+ player.armorvalue = st.armorvalue;
+ player.ammo_shells = st.ammo_shells;
+ player.ammo_nails = st.ammo_nails;
+ player.ammo_rockets = st.ammo_rockets;
+ player.ammo_cells = st.ammo_cells;
+ player.weapons = st.weapons;
+ player.items = st.items;
+ player.superweapons_finished = st.superweapons_finished;
+}
+
+/// \brief Returns the attacker with the lowest score.
+/// \param[in] bot Whether to search only for bots.
+/// \return Attacker with the lowest score or NULL if not found.
+entity Surv_FindLowestAttacker(bool bot)
+{
+ entity player = NULL;
+ float score = FLOAT_MAX;
+ FOREACH_CLIENT(bot ? IS_BOT_CLIENT(it) : true,
+ {
+ if ((it.team == surv_attackerteam) && (it.surv_role ==
+ SURVIVAL_ROLE_PLAYER))
+ {
+ float tempscore = PlayerScore_Get(it, SP_SCORE);
+ if (tempscore < score)
+ {
+ player = it;
+ score = tempscore;
+ }
+ }
+ });
+ return player;
+}
+
+/// \brief Returns the defender with the lowest score.
+/// \param[in] bot Whether to search only for bots.
+/// \param[in] alive Whether to search only for alive players.
+/// \return Defender with the lowest score or NULL if not found.
+entity Surv_FindLowestDefender(bool bot, bool alive)
+{
+ entity player = NULL;
+ float score = FLOAT_MAX;
+ FOREACH_CLIENT(bot ? IS_BOT_CLIENT(it) : true,
+ {
+ if ((it.team == surv_defenderteam) && (alive ? !IS_DEAD(it) : true))
+ {
+ float tempscore = PlayerScore_Get(it, SP_SCORE);
+ if (tempscore < score)
+ {
+ player = it;
+ score = tempscore;
+ }
+ }
+ });
+ return player;
+}
+
+/// \brief Returns the cannon fodder.
+/// \return Cannon fodder or NULL if not found.
+entity Surv_FindCannonFodder()
+{
+ FOREACH_CLIENT(IS_BOT_CLIENT(it),
+ {
+ if (it.surv_role == SURVIVAL_ROLE_CANNON_FODDER)
+ {
+ return it;
+ }
+ });
+ return NULL;
+}
+
+/// \brief Changes the number of players in a team.
+/// \param[in] teamnum Team to adjust.
+/// \param[in] delta Amount to adjust by.
+/// \return No return.
+void Surv_ChangeNumberOfPlayers(int teamnum, int delta)
+{
+ switch (teamnum)
+ {
+ case surv_attackerteam:
+ {
+ surv_numattackers += delta;
+ LOG_TRACE("Number of attackers = ", ftos(surv_numattackers),
+ " was = ", ftos(surv_numattackers - delta));
+ Surv_UpdateTeamStats();
+ return;
+ }
+ case surv_defenderteam:
+ {
+ surv_numdefenders += delta;
+ LOG_TRACE("Number of defenders = ", ftos(surv_numdefenders),
+ " was = ", ftos(surv_numdefenders - delta));
+ Surv_UpdateTeamStats();
+ return;
+ }
+ }
+}
+
+/// \brief Changes the number of alive players in a team.
+/// \param[in] teamnum Team to adjust.
+/// \param[in] delta Amount to adjust by.
+/// \return No return.
+void Surv_ChangeNumberOfAlivePlayers(int teamnum, int delta)
+{
+ switch (teamnum)
+ {
+ case surv_attackerteam:
+ {
+ surv_numattackersalive += delta;
+ LOG_TRACE("Number of alive attackers = ", ftos(
+ surv_numattackersalive), " was = ", ftos(surv_numattackersalive
+ - delta));
+ break;
+ }
+ case surv_defenderteam:
+ {
+ surv_numdefendersalive += delta;
+ LOG_TRACE("Number of alive defenders = ", ftos(
+ surv_numdefendersalive), " was = ", ftos(surv_numdefendersalive
+ - delta));
+ break;
+ }
+ }
+ Surv_UpdateAliveStats();
+ eliminatedPlayers.SendFlags |= 1;
+}
+
+/// \brief Sets the player role.
+/// \param[in,out] player Player to adjust.
+/// \param[in] role Role to set.
+/// \return No return.
+void Surv_SetPlayerRole(entity player, int role)
+{
+ if (player.surv_role == role)
+ {
+ return;
+ }
+ player.surv_role = role;
+ switch (role)
+ {
+ case SURVIVAL_ROLE_NONE:
+ {
+ LOG_TRACE(player.netname, " now has no role.");
+ break;
+ }
+ case SURVIVAL_ROLE_PLAYER:
+ {
+ LOG_TRACE(player.netname, " is now a player.");
+ break;
+ }
+ case SURVIVAL_ROLE_CANNON_FODDER:
+ {
+ LOG_TRACE(player.netname, " is now a cannon fodder.");
+ break;
+ }
+ }
+}
+
+/// \brief Adds player to team. Handles bookkeeping information.
+/// \param[in] player Player to add.
+/// \param[in] teamnum Team to add to.
+/// \return True on success, false otherwise.
+bool Surv_AddPlayerToTeam(entity player, int teamnum)
+{
+ LOG_TRACE("Survival: AddPlayerToTeam, player: ", player.netname);
+ switch (teamnum)
+ {
+ case surv_attackerteam:
+ {
+ LOG_TRACE("Attacker team");
+ if (player.surv_role == SURVIVAL_ROLE_CANNON_FODDER)
+ {
+ LOG_TRACE("Cannon fodder is switching team");
+ return true;
+ }
+ if (IS_BOT_CLIENT(player))
+ {
+ LOG_TRACE("Client is bot");
+ LOG_TRACE("Attackers = ", ftos(surv_numattackers));
+ if (surv_numattackers < autocvar_g_surv_team_size)
+ {
+ Surv_SetPlayerRole(player, SURVIVAL_ROLE_PLAYER);
+ Surv_ChangeNumberOfPlayers(teamnum, +1);
+ return true;
+ }
+ Surv_SetPlayerRole(player, SURVIVAL_ROLE_CANNON_FODDER);
+ return true;
+ }
+ LOG_TRACE("Client is not a bot");
+ LOG_TRACE("Attackers = ", ftos(surv_numattackers));
+ if (surv_numattackers >= autocvar_g_surv_team_size)
+ {
+ LOG_TRACE("Removing bot");
+ // Remove bot to make space for human.
+ entity bot = Surv_FindLowestAttacker(true);
+ if (bot == NULL)
+ {
+ LOG_TRACE("No valid bot to remove");
+ // No space in team, denying team change.
+ TRANSMUTE(Spectator, player);
+ return false;
+ }
+ LOG_TRACE("Changing ", bot.netname,
+ " from attacker to cannon fodder.");
+ Surv_SetPlayerRole(bot, SURVIVAL_ROLE_CANNON_FODDER);
+ if (!IS_DEAD(bot))
+ {
+ Surv_ChangeNumberOfAlivePlayers(teamnum, -1);
+ }
+ Surv_ChangeNumberOfPlayers(teamnum, -1);
+ LOG_TRACE("Removed bot");
+ }
+ Surv_SetPlayerRole(player, SURVIVAL_ROLE_PLAYER);
+ Surv_ChangeNumberOfPlayers(teamnum, +1);
+ ++surv_numattackerhumans;
+ LOG_TRACE("Human attackers = ", ftos(surv_numattackerhumans));
+ if ((surv_autobalance == false) || (surv_numattackers -
+ surv_numdefenders) < 2)
+ {
+ return true;
+ }
+ entity lowestplayer = Surv_FindLowestAttacker(true);
+ if (lowestplayer != NULL)
+ {
+ bool savedautobalance = surv_autobalance;
+ surv_autobalance = false;
+ SetPlayerTeamSimple(lowestplayer, surv_defenderteam);
+ surv_autobalance = savedautobalance;
+ return true;
+ }
+ lowestplayer = Surv_FindLowestAttacker(false);
+ if (lowestplayer != NULL)
+ {
+ bool savedautobalance = surv_autobalance;
+ surv_autobalance = false;
+ SetPlayerTeamSimple(lowestplayer, surv_defenderteam);
+ surv_autobalance = savedautobalance;
+ }
+ return true;
+ }
+ case surv_defenderteam:
+ {
+ LOG_TRACE("Defender team");
+ if (IS_BOT_CLIENT(player))
+ {
+ LOG_TRACE("Client is bot");
+ LOG_TRACE("Defenders = ", ftos(surv_numdefenders));
+ if (surv_numdefenders < autocvar_g_surv_team_size)
+ {
+ Surv_SetPlayerRole(player, SURVIVAL_ROLE_PLAYER);
+ Surv_ChangeNumberOfPlayers(teamnum, +1);
+ return true;
+ }
+ LOG_TRACE("No space for defender, switching to attacker");
+ SetPlayerTeamSimple(player, surv_attackerteam);
+ return false;
+ }
+ LOG_TRACE("Client is not a bot");
+ LOG_TRACE("Defenders = ", ftos(surv_numdefenders));
+ if (surv_numdefenders >= autocvar_g_surv_team_size)
+ {
+ LOG_TRACE("Removing bot");
+ // Remove bot to make space for human.
+ entity bot = Surv_FindLowestDefender(true, true);
+ if (bot == NULL)
+ {
+ bot = Surv_FindLowestDefender(true, false);
+ }
+ if (bot == NULL)
+ {
+ LOG_TRACE("No valid bot to remove");
+ // No space in team, denying team change.
+ TRANSMUTE(Spectator, player);
+ return false;
+ }
+ LOG_TRACE("Changing ", bot.netname,
+ " from defender to cannon fodder.");
+ if (!IS_DEAD(bot))
+ {
+ player.surv_savedplayerstate = Surv_SavePlayerState(bot);
+ }
+ bool savedautobalance = surv_autobalance;
+ surv_autobalance = false;
+ surv_announcefrags = false;
+ SetPlayerTeamSimple(bot, surv_attackerteam);
+ surv_autobalance = savedautobalance;
+ surv_announcefrags = true;
+ LOG_TRACE("Removed bot");
+ }
+ Surv_SetPlayerRole(player, SURVIVAL_ROLE_PLAYER);
+ Surv_ChangeNumberOfPlayers(teamnum, +1);
+ ++surv_numdefenderhumans;
+ LOG_TRACE("Human defenders = ", ftos(surv_numdefenderhumans));
+ if ((surv_autobalance == false) || (surv_numdefenders -
+ surv_numattackers) < 2)
+ {
+ return true;
+ }
+ entity lowestplayer = Surv_FindLowestDefender(true, false);
+ if (lowestplayer != NULL)
+ {
+ bool savedautobalance = surv_autobalance;
+ surv_autobalance = false;
+ SetPlayerTeamSimple(lowestplayer, surv_attackerteam);
+ surv_autobalance = savedautobalance;
+ return true;
+ }
+ lowestplayer = Surv_FindLowestDefender(false, false);
+ if (lowestplayer != NULL)
+ {
+ bool savedautobalance = surv_autobalance;
+ surv_autobalance = false;
+ SetPlayerTeamSimple(lowestplayer, surv_attackerteam);
+ surv_autobalance = savedautobalance;
+ }
+ return true;
+ }
+ case -1:
+ {
+ LOG_TRACE("Spectator team");
+ player.surv_role = SURVIVAL_ROLE_NONE;
+ return false;
+ }
+ }
+ LOG_TRACE("Invalid team");
+ player.surv_role = SURVIVAL_ROLE_NONE;
+ return false;
+}
+
+/// \brief Removes player from team. Handles bookkeeping information.
+/// \param[in] player Player to remove.
+/// \param[in] Team to remove from.
+/// \return No return.
+void Surv_RemovePlayerFromTeam(entity player, int teamnum)
+{
+ LOG_TRACE("Survival: RemovePlayerFromTeam, player: ", player.netname);
+ switch (teamnum)
+ {
+ case surv_attackerteam:
+ {
+ LOG_TRACE("Attacker team");
+ if (player.surv_role == SURVIVAL_ROLE_NONE)
+ {
+ string message = strcat("RemovePlayerFromTeam: ",
+ player.netname, " has invalid role.");
+ DebugPrintToChatAll(message);
+ return;
+ }
+ if (player.surv_role == SURVIVAL_ROLE_CANNON_FODDER)
+ {
+ Surv_SetPlayerRole(player, SURVIVAL_ROLE_NONE);
+ return;
+ }
+ Surv_SetPlayerRole(player, SURVIVAL_ROLE_NONE);
+ Surv_ChangeNumberOfPlayers(teamnum, -1);
+ if (!IS_BOT_CLIENT(player))
+ {
+ --surv_numattackerhumans;
+ }
+ if ((surv_autobalance == false) || (surv_numattackers >=
+ surv_numdefenders))
+ {
+ return;
+ }
+ // Add bot to keep teams balanced.
+ entity lowestplayer = Surv_FindCannonFodder();
+ if (lowestplayer != NULL)
+ {
+ LOG_TRACE("Changing ", lowestplayer.netname,
+ " from cannon fodder to attacker.");
+ Surv_SetPlayerRole(lowestplayer, SURVIVAL_ROLE_PLAYER);
+ Surv_ChangeNumberOfPlayers(teamnum, +1);
+ if (!IS_DEAD(lowestplayer))
+ {
+ Surv_ChangeNumberOfAlivePlayers(teamnum, +1);
+ }
+ return;
+ }
+ lowestplayer = Surv_FindLowestDefender(true, false);
+ if (lowestplayer != NULL)
+ {
+ bool savedautobalance = surv_autobalance;
+ surv_autobalance = false;
+ SetPlayerTeamSimple(lowestplayer, surv_attackerteam);
+ surv_autobalance = savedautobalance;
+ return;
+ }
+ lowestplayer = Surv_FindLowestDefender(false, false);
+ if (lowestplayer == NULL)
+ {
+ return;
+ }
+ bool savedautobalance = surv_autobalance;
+ surv_autobalance = false;
+ SetPlayerTeamSimple(lowestplayer, surv_attackerteam);
+ surv_autobalance = savedautobalance;
+ return;
+ }
+ case surv_defenderteam:
+ {
+ LOG_TRACE("Defender team");
+ if (player.surv_role == SURVIVAL_ROLE_CANNON_FODDER)
+ {
+ // This happens during team switch. We don't need to change
+ // anything.
+ LOG_TRACE("Cannon fodder. Assuming team switch");
+ return;
+ }
+ if (player.surv_role != SURVIVAL_ROLE_PLAYER)
+ {
+ string message = strcat("RemovePlayerFromTeam: ",
+ player.netname, " has invalid role.");
+ DebugPrintToChatAll(message);
+ return;
+ }
+ Surv_SetPlayerRole(player, SURVIVAL_ROLE_NONE);
+ Surv_ChangeNumberOfPlayers(teamnum, -1);
+ if (!IS_BOT_CLIENT(player))
+ {
+ --surv_numdefenderhumans;
+ }
+ if ((surv_autobalance == false) || (surv_numdefenders >=
+ surv_numattackers))
+ {
+ return;
+ }
+ // Add bot to keep teams balanced.
+ entity lowestplayer = Surv_FindCannonFodder();
+ if (lowestplayer != NULL)
+ {
+ LOG_TRACE("Changing ", lowestplayer.netname,
+ " from cannon fodder to defender.");
+ if (!IS_DEAD(player))
+ {
+ lowestplayer.surv_savedplayerstate =
+ Surv_SavePlayerState(player);
+ }
+ bool savedautobalance = surv_autobalance;
+ surv_autobalance = false;
+ surv_announcefrags = false;
+ SetPlayerTeamSimple(lowestplayer, surv_defenderteam);
+ surv_autobalance = savedautobalance;
+ surv_announcefrags = true;
+ return;
+ }
+ lowestplayer = Surv_FindLowestAttacker(true);
+ if (lowestplayer != NULL)
+ {
+ bool savedautobalance = surv_autobalance;
+ surv_autobalance = false;
+ surv_announcefrags = false;
+ SetPlayerTeamSimple(lowestplayer, surv_defenderteam);
+ surv_autobalance = savedautobalance;
+ surv_announcefrags = true;
+ return;
+ }
+ lowestplayer = Surv_FindLowestAttacker(false);
+ if (lowestplayer == NULL)
+ {
+ return;
+ }
+ bool savedautobalance = surv_autobalance;
+ surv_autobalance = false;
+ surv_announcefrags = false;
+ SetPlayerTeamSimple(lowestplayer, surv_defenderteam);
+ surv_autobalance = savedautobalance;
+ surv_announcefrags = true;
+ return;
+ }
+ case -1:
+ {
+ LOG_TRACE("Spectator team");
+ return;
+ }
+ default:
+ {
+ LOG_TRACE("Invalid team");
+ return;
+ }
+ }
+}
+
+/// \brief Updates stats of team count on HUD.
+/// \return No return.
+void Surv_UpdateTeamStats()
+{
+ // Debug stuff
+ if (surv_attackerteam == NUM_TEAM_1)
+ {
+ yellowalive = surv_numattackers;
+ pinkalive = surv_numdefenders;
+ }
+ else
+ {
+ pinkalive = surv_numattackers;
+ yellowalive = surv_numdefenders;
+ }
+ FOREACH_CLIENT(IS_REAL_CLIENT(it),
+ {
+ it.yellowalive_stat = yellowalive;
+ it.pinkalive_stat = pinkalive;
+ });
+}
+
+/// \brief Adds player to alive list. Handles bookkeeping information.
+/// \param[in] player Player to add.
+/// \param[in] teamnum Team of the player.
+/// \return No return.
+void Surv_AddPlayerToAliveList(entity player, int teamnum)
+{
+ switch (teamnum)
+ {
+ case surv_attackerteam:
+ {
+ if (player.surv_role == SURVIVAL_ROLE_PLAYER)
+ {
+ Surv_ChangeNumberOfAlivePlayers(teamnum, +1);
+ }
+ return;
+ }
+ case surv_defenderteam:
+ {
+ Surv_ChangeNumberOfAlivePlayers(teamnum, +1);
+ return;
+ }
+ }
+}
+
+/// \brief Removes player from alive list. Handles bookkeeping information.
+/// \param[in] player Player to remove.
+/// \param[in] teamnum Team of the player.
+/// \return No return.
+void Surv_RemovePlayerFromAliveList(entity player, int teamnum)
+{
+ if (player.surv_attack_sprite)
+ {
+ WaypointSprite_Kill(player.surv_attack_sprite);
+ }
+ if (player.surv_defend_sprite)
+ {
+ WaypointSprite_Kill(player.surv_defend_sprite);
+ }
+ switch (teamnum)
+ {
+ case surv_attackerteam:
+ {
+ if (player.surv_role == SURVIVAL_ROLE_PLAYER)
+ {
+ Surv_ChangeNumberOfAlivePlayers(teamnum, -1);
+ }
+ return;
+ }
+ case surv_defenderteam:
+ {
+ if (player.surv_role == SURVIVAL_ROLE_CANNON_FODDER)
+ {
+ // This happens during team switch. We don't need to change
+ // anything.
+ return;
+ }
+ Surv_ChangeNumberOfAlivePlayers(teamnum, -1);
+ if (warmup_stage || surv_allowed_to_spawn || !surv_announcefrags)
+ {
+ return;
+ }
+ switch (surv_numdefendersalive)
+ {
+ case 1:
+ {
+ sound(NULL, CH_TRIGGER, SND_SURV_1_FRAG_LEFT,
+ VOL_BASE, ATTEN_NONE);
+ FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
+ {
+ if (it.team == surv_defenderteam)
+ {
+ Send_Notification(NOTIF_ONE, it, MSG_CENTER,
+ CENTER_ALONE);
+ return;
+ }
+ });
+ return;
+ }
+ case 2:
+ {
+ sound(NULL, CH_TRIGGER, SND_SURV_2_FRAGS_LEFT,
+ VOL_BASE, ATTEN_NONE);
+ return;
+ }
+ case 3:
+ {
+ sound(NULL, CH_TRIGGER, SND_SURV_3_FRAGS_LEFT,
+ VOL_BASE, ATTEN_NONE);
+ return;
+ }
+ }
+ return;
+ }
+ }
+}
+
+/// \brief Counts alive players.
+/// \return No return.
+/// \note In a perfect world this function shouldn't exist. However, since QC
+/// code is so bad and spurious mutators can really mess with your code, this
+/// function is called as a last resort.
+void Surv_CountAlivePlayers()
+{
+ int savednumdefenders = surv_numdefendersalive;
+ surv_numattackersalive = 0;
+ surv_numdefendersalive = 0;
+ FOREACH_CLIENT(IS_PLAYER(it),
+ {
+ switch (it.team)
+ {
+ case surv_attackerteam:
+ {
+ if ((it.surv_role == SURVIVAL_ROLE_PLAYER) && !IS_DEAD(it))
+ {
+ ++surv_numattackersalive;
+ }
+ break;
+ }
+ case surv_defenderteam:
+ {
+ if ((it.surv_role == SURVIVAL_ROLE_PLAYER) && !IS_DEAD(it))
+ {
+ ++surv_numdefendersalive;
+ }
+ break;
+ }
+ }
+ });
+ Surv_UpdateAliveStats();
+ eliminatedPlayers.SendFlags |= 1;
+ if (warmup_stage || surv_allowed_to_spawn || (savednumdefenders <=
+ surv_numdefendersalive))
+ {
+ return;
+ }
+ switch (surv_numdefendersalive)
+ {
+ case 1:
+ {
+ sound(NULL, CH_TRIGGER, SND_SURV_1_FRAG_LEFT, VOL_BASE, ATTEN_NONE);
+ FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
+ {
+ if (it.team == surv_defenderteam)
+ {
+ Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_ALONE);
+ return;
+ }
+ });
+ return;
+ }
+ case 2:
+ {
+ sound(NULL, CH_TRIGGER, SND_SURV_2_FRAGS_LEFT, VOL_BASE,
+ ATTEN_NONE);
+ return;
+ }
+ case 3:
+ {
+ sound(NULL, CH_TRIGGER, SND_SURV_3_FRAGS_LEFT, VOL_BASE,
+ ATTEN_NONE);
+ return;
+ }
+ }
+}
+
+/// \brief Updates stats of alive players on HUD.
+/// \return No return.
+void Surv_UpdateAliveStats()
+{
+ // Debug stuff
+ if (surv_attackerteam == NUM_TEAM_1)
+ {
+ redalive = surv_numattackersalive;
+ bluealive = surv_numdefendersalive;
+ }
+ else
+ {
+ bluealive = surv_numattackersalive;
+ redalive = surv_numdefendersalive;
+ }
+ FOREACH_CLIENT(IS_REAL_CLIENT(it),
+ {
+ it.surv_defenders_alive_stat = surv_numdefendersalive;
+ it.redalive_stat = redalive;
+ it.bluealive_stat = bluealive;
+ });
+ Surv_UpdateDefenderHealthStat();
+}
+
+/// \brief Updates defender health on the HUD.
+/// \return No return.
+void Surv_UpdateDefenderHealthStat()
+{
+ float maxhealth;
+ float totalhealth = 0;
+ if (autocvar_g_instagib == 1)
+ {
+ maxhealth = surv_numdefenders * (PlayerTemplate_GetFloatValue(
+ "surv_defender", "start_armor") + 1);
+ FOREACH_CLIENT(IS_PLAYER(it),
+ {
+ if (it.team == surv_defenderteam)
+ {
+ totalhealth += it.armorvalue + 1;
+ }
+ });
+ }
+ else
+ {
+ maxhealth = surv_numdefenders * (PlayerTemplate_GetFloatValue(
+ "surv_defender", "start_health") + PlayerTemplate_GetFloatValue(
+ "surv_defender", "start_armor"));
+ FOREACH_CLIENT(IS_PLAYER(it),
+ {
+ if (it.team == surv_defenderteam)
+ {
+ totalhealth += it.health;
+ totalhealth += it.armorvalue;
+ }
+ });
+ }
+ float healthratio;
+ if (maxhealth == 0)
+ {
+ healthratio = 0;
+ }
+ else
+ {
+ healthratio = totalhealth / maxhealth;
+ }
+ FOREACH_CLIENT(IS_REAL_CLIENT(it),
+ {
+ it.surv_defender_health_stat = healthratio;
+ });
+}
+
+/// \brief Returns whether the player can spawn.
+/// \param[in] player Player to check.
+/// \return True if the player can spawn, false otherwise.
+bool Surv_CanPlayerSpawn(entity player)
+{
+ if ((player.team == surv_attackerteam) ||
+ (player.surv_savedplayerstate != NULL))
+ {
+ return true;
+ }
+ return surv_allowed_to_spawn;
+}
+
+/// \brief Switches the round type.
+/// \return No return.
+void Surv_SwitchRoundType()
+{
+ switch (surv_roundtype)
+ {
+ case SURVIVAL_ROUND_FIRST:
+ {
+ surv_roundtype = SURVIVAL_ROUND_SECOND;
+ return;
+ }
+ case SURVIVAL_ROUND_SECOND:
+ {
+ surv_roundtype = SURVIVAL_ROUND_FIRST;
+ return;
+ }
+ }
+}
+
+/// \brief Cleans up the mess after the round has finished.
+/// \return No return.
+void Surv_RoundCleanup()
+{
+ surv_allowed_to_spawn = false;
+ surv_isroundactive = false;
+ game_stopped = true;
+ FOREACH_CLIENT(true,
+ {
+ if (it.surv_attack_sprite)
+ {
+ WaypointSprite_Kill(it.surv_attack_sprite);
+ }
+ if (it.surv_defend_sprite)
+ {
+ WaypointSprite_Kill(it.surv_defend_sprite);
+ }
+ if (it.surv_savedplayerstate)
+ {
+ delete(it.surv_savedplayerstate);
+ it.surv_savedplayerstate = NULL;
+ }
+ });
+ if (surv_type == SURVIVAL_TYPE_VERSUS)
+ {
+ Surv_SwitchRoundType();
+ round_handler_Init(5, autocvar_g_surv_warmup, surv_timetobeat);
+ return;
+ }
+ round_handler_Init(5, autocvar_g_surv_warmup,
+ autocvar_g_surv_round_timelimit);
+}
+
+/// \brief Swaps attacker and defender teams.
+/// \return No return.
+void Surv_SwapTeams()
+{
+ int temp = surv_attackerteam;
+ surv_attackerteam = surv_defenderteam;
+ surv_defenderteam = temp;
+ temp = surv_attackerteambit;
+ surv_attackerteambit = surv_defenderteambit;
+ surv_defenderteambit = temp;
+ temp = surv_numattackers;
+ surv_numattackers = surv_numdefenders;
+ surv_numdefenders = temp;
+ temp = surv_numattackerhumans;
+ surv_numattackerhumans = surv_numdefenderhumans;
+ surv_numdefenderhumans = temp;
+ FOREACH_CLIENT(true,
+ {
+ if ((it.team == surv_defenderteam) && (it.surv_role ==
+ SURVIVAL_ROLE_CANNON_FODDER))
+ {
+ SetPlayerTeamSimple(it, surv_attackerteam);
+ }
+ });
+ FOREACH_CLIENT(IS_REAL_CLIENT(it),
+ {
+ it.surv_defender_team_stat = Team_TeamToNumber(surv_defenderteam);
+ });
+}
+
+/// \brief Forces the overkill model for specific player.
+/// \param[in,out] player Player to force the model of.
+/// \return No return.
+void Surv_ForceOverkillPlayerModel(entity player)
+{
+ switch (player.team)
+ {
+ case NUM_TEAM_1:
+ {
+ switch (floor(random() * 4))
+ {
+ case 0:
+ {
+ player.surv_playermodel = "models/ok_player/okrobot1.dpm";
+ return;
+ }
+ case 1:
+ {
+ player.surv_playermodel = "models/ok_player/okrobot2.dpm";
+ return;
+ }
+ case 2:
+ {
+ player.surv_playermodel = "models/ok_player/okrobot3.dpm";
+ return;
+ }
+ case 3:
+ {
+ player.surv_playermodel = "models/ok_player/okrobot4.dpm";
+ return;
+ }
+ }
+ return;
+ }
+ case NUM_TEAM_2:
+ {
+ switch (floor(random() * 4))
+ {
+ case 0:
+ {
+ player.surv_playermodel = "models/ok_player/okmale1.dpm";
+ return;
+ }
+ case 1:
+ {
+ player.surv_playermodel = "models/ok_player/okmale2.dpm";
+ return;
+ }
+ case 2:
+ {
+ player.surv_playermodel = "models/ok_player/okmale3.dpm";
+ return;
+ }
+ case 3:
+ {
+ player.surv_playermodel = "models/ok_player/okmale4.dpm";
+ return;
+ }
+ }
+ return;
+ }
+ }
+}
+
+/// \brief Determines the player model to the one configured for the gamemode.
+/// \param[in,out] player Player to determine the model of.
+/// \return No return.
+void Surv_DeterminePlayerModel(entity player)
+{
+ switch (player.team)
+ {
+ case surv_attackerteam:
+ {
+ switch (player.surv_role)
+ {
+ case SURVIVAL_ROLE_PLAYER:
+ {
+ if (!autocvar_g_surv_attacker_force_overkill_models)
+ {
+ player.surv_playermodel = player.surv_savedplayermodel;
+ return;
+ }
+ Surv_ForceOverkillPlayerModel(player);
+ return;
+ }
+ case SURVIVAL_ROLE_CANNON_FODDER:
+ {
+ if (!autocvar_g_surv_cannon_fodder_force_overkill_models)
+ {
+ player.surv_playermodel = player.surv_savedplayermodel;
+ return;
+ }
+ Surv_ForceOverkillPlayerModel(player);
+ return;
+ }
+ }
+ }
+ case surv_defenderteam:
+ {
+ if (!autocvar_g_surv_defender_force_overkill_models)
+ {
+ player.surv_playermodel = player.surv_savedplayermodel;
+ return;
+ }
+ Surv_ForceOverkillPlayerModel(player);
+ return;
+ }
+ }
+}
+
+/// \brief Setups a waypoint sprite used to track defenders.
+/// \param[in] player Player to attach sprite too.
+/// \return No return.
+void Surv_SetupWaypointSprite(entity player)
+{
+ WaypointSprite_Spawn(WP_SurvivalKill, 0, 0, player, '0 0 64', NULL,
+ surv_attackerteam, player, surv_attack_sprite, false,
+ RADARICON_OBJECTIVE);
+ WaypointSprite_Spawn(WP_SurvivalDefend, 0, 0, player, '0 0 64', NULL,
+ surv_defenderteam, player, surv_defend_sprite, false,
+ RADARICON_OBJECTIVE);
+ //player.surv_attack_sprite.colormod = colormapPaletteColor(player.team - 1,
+ // false);
+ //player.surv_defend_sprite.colormod = colormapPaletteColor(player.team - 1,
+ // false);
+ float max_hp;
+ if (autocvar_g_instagib == 1)
+ {
+ max_hp = PlayerTemplate_GetFloatValue(Surv_GetPlayerTemplate(player),
+ "start_armor") + 1;
+ WaypointSprite_UpdateMaxHealth(player.surv_attack_sprite, max_hp);
+ WaypointSprite_UpdateMaxHealth(player.surv_defend_sprite, max_hp);
+ Surv_UpdateWaypointSpriteHealth(player);
+ return;
+ }
+ max_hp = PlayerTemplate_GetFloatValue(Surv_GetPlayerTemplate(player),
+ "start_health") + PlayerTemplate_GetFloatValue(Surv_GetPlayerTemplate(
+ player), "start_armor");
+ WaypointSprite_UpdateMaxHealth(player.surv_attack_sprite, max_hp);
+ WaypointSprite_UpdateMaxHealth(player.surv_defend_sprite, max_hp);
+ Surv_UpdateWaypointSpriteHealth(player);
+}
+
+void Surv_UpdateWaypointSpriteHealth(entity player)
+{
+ float hp;
+ if (autocvar_g_instagib == 1)
+ {
+ hp = player.armorvalue + 1;
+ }
+ else
+ {
+ hp = player.health + player.armorvalue;
+ }
+ WaypointSprite_UpdateHealth(player.surv_attack_sprite, hp);
+ WaypointSprite_UpdateHealth(player.surv_defend_sprite, hp);
+}
+
+//=============================== Callbacks ===================================
+
+bool Surv_CanRoundStart()
+{
+ return (surv_numattackersalive > 0) && (surv_numdefendersalive > 0);
+}
+
+bool Surv_CanRoundEnd()
+{
+ if (warmup_stage)
+ {
+ return false;
+ }
+ if((round_handler_GetEndTime() > 0) && (round_handler_GetEndTime() -
+ time <= 0))
+ {
+ if (surv_roundtype == SURVIVAL_ROUND_FIRST)
+ {
+ surv_timetobeat = time - surv_roundstarttime;
+ Send_Notification(NOTIF_ALL, NULL, MSG_CENTER,
+ CENTER_SURVIVAL_DEFENDERS_SURVIVED);
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO,
+ INFO_SURVIVAL_DEFENDERS_SURVIVED);
+ Surv_RoundCleanup();
+ return true;
+ }
+ surv_timetobeat = autocvar_g_surv_round_timelimit;
+ Send_Notification(NOTIF_ALL, NULL, MSG_CENTER,
+ CENTER_SURVIVAL_DEFENDERS_SURVIVED);
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO,
+ INFO_SURVIVAL_DEFENDERS_SURVIVED);
+ switch (surv_defenderteam)
+ {
+ case NUM_TEAM_1:
+ {
+ sound(NULL, CH_TRIGGER, SND_SURV_RED_SCORES, VOL_BASE,
+ ATTEN_NONE);
+ break;
+ }
+ case NUM_TEAM_2:
+ {
+ sound(NULL, CH_TRIGGER, SND_SURV_BLUE_SCORES, VOL_BASE,
+ ATTEN_NONE);
+ break;
+ }
+ }
+ TeamScore_AddToTeam(surv_defenderteam, 1, 1);
+ Surv_RoundCleanup();
+ return true;
+ }
+ if (surv_numdefendersalive > 0)
+ {
+ return false;
+ }
+ if (surv_roundtype == SURVIVAL_ROUND_FIRST)
+ {
+ surv_timetobeat = time - surv_roundstarttime;
+ Send_Notification(NOTIF_ALL, NULL, MSG_CENTER,
+ CENTER_SURVIVAL_DEFENDERS_ELIMINATED_IN, surv_timetobeat);
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO,
+ INFO_SURVIVAL_DEFENDERS_ELIMINATED_IN, surv_timetobeat);
+ Surv_RoundCleanup();
+ return true;
+ }
+ surv_timetobeat = autocvar_g_surv_round_timelimit;
+ Send_Notification(NOTIF_ALL, NULL, MSG_CENTER,
+ CENTER_SURVIVAL_DEFENDERS_ELIMINATED);
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO,
+ INFO_SURVIVAL_DEFENDERS_ELIMINATED);
+ switch (surv_attackerteam)
+ {
+ case NUM_TEAM_1:
+ {
+ sound(NULL, CH_TRIGGER, SND_SURV_RED_SCORES, VOL_BASE,
+ ATTEN_NONE);
+ break;
+ }
+ case NUM_TEAM_2:
+ {
+ sound(NULL, CH_TRIGGER, SND_SURV_BLUE_SCORES, VOL_BASE,
+ ATTEN_NONE);
+ break;
+ }
+ }
+ TeamScore_AddToTeam(surv_attackerteam, 1, 1);
+ Surv_RoundCleanup();
+ return true;
+}
+
+void Surv_RoundStart()
+{
+ if (warmup_stage)
+ {
+ surv_allowed_to_spawn = true;
+ return;
+ }
+ surv_isroundactive = true;
+ surv_roundstarttime = time;
+ surv_allowed_to_spawn = false;
+ switch (surv_roundtype)
+ {
+ case SURVIVAL_ROUND_FIRST:
+ {
+ FOREACH_CLIENT(IS_PLAYER(it),
+ {
+ if (it.team == surv_attackerteam)
+ {
+ Send_Notification(NOTIF_TEAM, it, MSG_CENTER,
+ CENTER_SURVIVAL_1ST_ROUND_ATTACKER);
+ Send_Notification(NOTIF_TEAM, it, MSG_INFO,
+ INFO_SURVIVAL_1ST_ROUND_ATTACKER);
+ break;
+ }
+ });
+ FOREACH_CLIENT(IS_PLAYER(it),
+ {
+ if (it.team == surv_defenderteam)
+ {
+ if (surv_type == SURVIVAL_TYPE_COOP)
+ {
+ Send_Notification(NOTIF_TEAM, it, MSG_CENTER,
+ CENTER_SURVIVAL_COOP_DEFENDER);
+ Send_Notification(NOTIF_TEAM, it, MSG_INFO,
+ INFO_SURVIVAL_COOP_DEFENDER);
+ break;
+ }
+ Send_Notification(NOTIF_TEAM, it, MSG_CENTER,
+ CENTER_SURVIVAL_1ST_ROUND_DEFENDER);
+ Send_Notification(NOTIF_TEAM, it, MSG_INFO,
+ INFO_SURVIVAL_1ST_ROUND_DEFENDER);
+ break;
+ }
+ });
+ break;
+ }
+ case SURVIVAL_ROUND_SECOND:
+ {
+ FOREACH_CLIENT(IS_PLAYER(it),
+ {
+ if (it.team == surv_attackerteam)
+ {
+ Send_Notification(NOTIF_TEAM, it, MSG_CENTER,
+ CENTER_SURVIVAL_2ND_ROUND_ATTACKER, surv_timetobeat);
+ Send_Notification(NOTIF_TEAM, it, MSG_INFO,
+ INFO_SURVIVAL_2ND_ROUND_ATTACKER, surv_timetobeat);
+ break;
+ }
+ });
+ FOREACH_CLIENT(IS_PLAYER(it),
+ {
+ if (it.team == surv_defenderteam)
+ {
+ Send_Notification(NOTIF_TEAM, it, MSG_CENTER,
+ CENTER_SURVIVAL_2ND_ROUND_DEFENDER, surv_timetobeat);
+ Send_Notification(NOTIF_TEAM, it, MSG_INFO,
+ INFO_SURVIVAL_2ND_ROUND_DEFENDER, surv_timetobeat);
+ break;
+ }
+ });
+ break;
+ }
+ }
+ if (autocvar_g_surv_stealth)
+ {
+ return;
+ }
+ FOREACH_CLIENT(IS_PLAYER(it),
+ {
+ switch (it.team)
+ {
+ case surv_defenderteam:
+ {
+ if (it.surv_role == SURVIVAL_ROLE_PLAYER)
+ {
+ Surv_SetupWaypointSprite(it);
+ }
+ break;
+ }
+ }
+ });
+}
+
+bool Surv_IsEliminated(entity player)
+{
+ switch (player.surv_state)
+ {
+ case SURVIVAL_STATE_NOT_PLAYING:
+ {
+ return true;
+ }
+ case SURVIVAL_STATE_PLAYING:
+ {
+ if (player.surv_role == SURVIVAL_ROLE_CANNON_FODDER)
+ {
+ // A hack until proper scoreboard is done.
+ return true;
+ }
+ if ((player.team == surv_defenderteam) && (IS_DEAD(player) ||
+ IS_OBSERVER(player)))
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+ // Should never reach here
+ return true;
+}
+
+//============================= Hooks ========================================
+
+/// \brief Hook that is called to determine general rules of the game.
+MUTATOR_HOOKFUNCTION(surv, ReadLevelCvars)
+{
+ surv_warmup = warmup_stage;
+}
+
+/// \brief Hook that is called to determine if there is a weapon arena.
+MUTATOR_HOOKFUNCTION(surv, SetWeaponArena)
+{
+ // Removing any weapon arena.
+ M_ARGV(0, string) = "off";
+}
+
+/// \brief Hook that is called to determine start items of all players.
+MUTATOR_HOOKFUNCTION(surv, SetStartItems)
+{
+ if (autocvar_g_instagib == 1)
+ {
+ return;
+ }
+ start_weapons = WEPSET(Null);
+ warmup_start_weapons = WEPSET(Null);
+}
+
+/// \brief Hook that is called on every frame.
+MUTATOR_HOOKFUNCTION(surv, SV_StartFrame)
+{
+ if (game_stopped || !surv_isroundactive)
+ {
+ return;
+ }
+ float roundtime = 0;
+ switch (surv_roundtype)
+ {
+ case SURVIVAL_ROUND_FIRST:
+ {
+ roundtime = time - surv_roundstarttime;
+ break;
+ }
+ case SURVIVAL_ROUND_SECOND:
+ {
+ roundtime = round_handler_GetEndTime() - time;
+ break;
+ }
+ }
+ FOREACH_CLIENT(IS_REAL_CLIENT(it),
+ {
+ it.surv_round_time_stat = roundtime;
+ });
+}
+
+/// \brief Hook that determines which team player can join. This is called
+/// before ClientConnect.
+MUTATOR_HOOKFUNCTION(surv, CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
+{
+ entity player = M_ARGV(2, entity);
+ LOG_TRACE("Survival: CheckAllowedTeams, player = ", player.netname);
+ if (player == NULL)
+ {
+ return SURVIVAL_TEAM_BITS;
+ }
+ if (IS_BOT_CLIENT(player))
+ {
+ int teambits = surv_attackerteambit;
+ if ((player.team == surv_defenderteam) || (surv_numdefenders <
+ autocvar_g_surv_team_size))
+ {
+ teambits |= surv_defenderteambit;
+ }
+ M_ARGV(0, float) = teambits;
+ return;
+ }
+ if (surv_type == SURVIVAL_TYPE_COOP)
+ {
+ if (surv_numdefenderhumans < autocvar_g_surv_team_size)
+ {
+ M_ARGV(0, float) = surv_defenderteambit;
+ return;
+ }
+ M_ARGV(0, float) = 0;
+ return;
+ }
+ int teambits = 0;
+ if (surv_numattackerhumans < autocvar_g_surv_team_size)
+ {
+ LOG_TRACE("Player can join attackers");
+ teambits |= surv_attackerteambit;
+ }
+ if (surv_numdefenderhumans < autocvar_g_surv_team_size)
+ {
+ LOG_TRACE("Player can join defenders");
+ teambits |= surv_defenderteambit;
+ }
+ M_ARGV(0, float) = teambits;
+ return;
+}
+
+/// \brief Hook that override team counts.
+MUTATOR_HOOKFUNCTION(surv, GetTeamCounts, CBC_ORDER_EXCLUSIVE)
+{
+ return true;
+}
+
+/// \brief Hook that sets the team count.
+MUTATOR_HOOKFUNCTION(surv, GetTeamCount, CBC_ORDER_EXCLUSIVE)
+{
+ float teamnum = M_ARGV(0, float);
+ entity ignore = M_ARGV(1, entity);
+ switch (teamnum)
+ {
+ case surv_attackerteam:
+ {
+ M_ARGV(2, float) = surv_numattackers;
+ M_ARGV(3, float) = surv_numattackers - surv_numattackerhumans;
+ if (ignore.team == surv_attackerteam)
+ {
+ --M_ARGV(2, float);
+ if (IS_BOT_CLIENT(ignore))
+ {
+ --M_ARGV(3, float);
+ }
+ }
+ entity lowestplayer = NULL;
+ float lowestplayerscore = FLOAT_MAX;
+ entity lowestbot = NULL;
+ float lowestbotscore = FLOAT_MAX;
+ FOREACH_CLIENT((it.team == surv_attackerteam) && (it.surv_role ==
+ SURVIVAL_ROLE_PLAYER),
+ {
+ if (it == ignore)
+ {
+ continue;
+ }
+ if (IS_BOT_CLIENT(it))
+ {
+ float tempscore = PlayerScore_Get(it, SP_SCORE);
+ if (tempscore < lowestbotscore)
+ {
+ lowestbot = it;
+ lowestbotscore = tempscore;
+ continue;
+ }
+ }
+ float tempscore = PlayerScore_Get(it, SP_SCORE);
+ if (tempscore < lowestplayerscore)
+ {
+ lowestplayer = it;
+ lowestplayerscore = tempscore;
+ }
+ });
+ M_ARGV(4, entity) = lowestplayer;
+ M_ARGV(5, entity) = lowestbot;
+ break;
+ }
+ case surv_defenderteam:
+ {
+ M_ARGV(2, float) = surv_numdefenders;
+ M_ARGV(3, float) = surv_numdefenders - surv_numdefenderhumans;
+ if (ignore.team == surv_defenderteam)
+ {
+ --M_ARGV(2, float);
+ if (IS_BOT_CLIENT(ignore))
+ {
+ --M_ARGV(3, float);
+ }
+ }
+ entity lowestplayer = NULL;
+ float lowestplayerscore = FLOAT_MAX;
+ entity lowestbot = NULL;
+ float lowestbotscore = FLOAT_MAX;
+ FOREACH_CLIENT((it.team == surv_defenderteam),
+ {
+ if (it == ignore)
+ {
+ continue;
+ }
+ if (IS_BOT_CLIENT(it))
+ {
+ float tempscore = PlayerScore_Get(it, SP_SCORE);
+ if (tempscore < lowestbotscore)
+ {
+ lowestbot = it;
+ lowestbotscore = tempscore;
+ continue;
+ }
+ }
+ float tempscore = PlayerScore_Get(it, SP_SCORE);
+ if (tempscore < lowestplayerscore)
+ {
+ lowestplayer = it;
+ lowestplayerscore = tempscore;
+ }
+ });
+ M_ARGV(4, entity) = lowestplayer;
+ M_ARGV(5, entity) = lowestbot;
+ break;
+ }
+ }
+ return true;
+}
+
+/// \brief Hook that determines the best teams for the player to join.
+MUTATOR_HOOKFUNCTION(surv, FindBestTeams, CBC_ORDER_EXCLUSIVE)
+{
+ if (surv_type == SURVIVAL_TYPE_COOP)
+ {
+ return false;
+ }
+ entity player = M_ARGV(0, entity);
+ if (IS_BOT_CLIENT(player))
+ {
+ return false;
+ }
+ int numattackerhumans = surv_numattackerhumans;
+ int numdefenderhumans = surv_numdefenderhumans;
+ if (player.team == surv_attackerteam)
+ {
+ --numattackerhumans;
+ }
+ else if (player.team == surv_defenderteam)
+ {
+ --numdefenderhumans;
+ }
+ if (numattackerhumans < numdefenderhumans)
+ {
+ M_ARGV(1, float) = BIT(Team_TeamToNumber(surv_attackerteam) - 1);
+ return true;
+ }
+ if (numattackerhumans > numdefenderhumans)
+ {
+ M_ARGV(1, float) = BIT(Team_TeamToNumber(surv_defenderteam) - 1);
+ return true;
+ }
+ M_ARGV(1, float) = SURVIVAL_TEAM_BITS;
+ return true;
+}
+
+/// \brief Hook that is called when player has changed the team.
+MUTATOR_HOOKFUNCTION(surv, Player_ChangedTeam)
+{
+ entity player = M_ARGV(0, entity);
+ int oldteam = M_ARGV(1, float);
+ int newteam = M_ARGV(2, float);
+ string message = strcat("Survival: Player_ChangedTeam, ", player.netname,
+ ", old team = ", ftos(oldteam), " new team = ", ftos(newteam));
+ LOG_TRACE(message);
+ DebugPrintToChatAll(message);
+ if ((oldteam != -1) && IS_PLAYER(player) && !IS_DEAD(player))
+ {
+ Surv_RemovePlayerFromAliveList(player, oldteam);
+ }
+ Surv_RemovePlayerFromTeam(player, oldteam);
+ if (Surv_AddPlayerToTeam(player, newteam) == false)
+ {
+ return;
+ }
+ //Surv_CountAlivePlayers();
+ if ((oldteam != -1) && IS_PLAYER(player) && !IS_DEAD(player))
+ {
+ Surv_AddPlayerToAliveList(player, newteam);
+ }
+}
+
+/// \brief Hook that is called when player connects to the server.
+MUTATOR_HOOKFUNCTION(surv, ClientConnect)
+{
+ entity player = M_ARGV(0, entity);
+ LOG_TRACE("Survival: ClientConnect, player = ", player.netname);
+ player.surv_savedplayermodel = player.playermodel;
+ if (IS_REAL_CLIENT(player))
+ {
+ player.surv_defender_team_stat = Team_TeamToNumber(surv_defenderteam);
+ player.surv_defenders_alive_stat = surv_numdefendersalive;
+ player.redalive_stat = redalive;
+ player.bluealive_stat = bluealive;
+ player.yellowalive_stat = yellowalive;
+ player.pinkalive_stat = pinkalive;
+ }
+ if (player.surv_role == SURVIVAL_ROLE_NONE)
+ {
+ Surv_AddPlayerToTeam(player, player.team);
+ }
+ return true;
+}
+
+/// \brief Hook that is called when player disconnects from the server.
+MUTATOR_HOOKFUNCTION(surv, ClientDisconnect)
+{
+ entity player = M_ARGV(0, entity);
+ if (!IS_DEAD(player))
+ {
+ Surv_RemovePlayerFromAliveList(player, player.team);
+ }
+ Surv_RemovePlayerFromTeam(player, player.team);
+ //Surv_CountAlivePlayers();
+}
+
+/// \brief Hook that determines whether player can spawn. It is not called for
+/// players who have joined the team and are dead.
+MUTATOR_HOOKFUNCTION(surv, ForbidSpawn)
+{
+ entity player = M_ARGV(0, entity);
+ LOG_TRACE("Survival: ForbidSpawn, player = ", player.netname);
+ if (player.surv_state == SURVIVAL_STATE_NOT_PLAYING)
+ {
+ return false;
+ }
+ return !Surv_CanPlayerSpawn(player);
+}
+
+MUTATOR_HOOKFUNCTION(surv, PutClientInServer)
+{
+ entity player = M_ARGV(0, entity);
+ LOG_TRACE("Survival: PutClientInServer, player = ", player.netname);
+ if (!Surv_CanPlayerSpawn(player) && IS_PLAYER(player))
+ {
+ LOG_TRACE("Transmuting to observer");
+ TRANSMUTE(Observer, player);
+ }
+}
+
+MUTATOR_HOOKFUNCTION(surv, MakePlayerObserver)
+{
+ entity player = M_ARGV(0, entity);
+ LOG_TRACE("Survival: MakePlayerObserver, player = ", player.netname);
+ if (player.killindicator_teamchange == -2) // player wants to spectate
+ {
+ LOG_TRACE("killindicator_teamchange == -2");
+ player.surv_state = SURVIVAL_STATE_NOT_PLAYING;
+ }
+ if (player.surv_state == SURVIVAL_STATE_NOT_PLAYING)
+ {
+ return false; // allow team reset
+ }
+ return true; // prevent team reset
+}
+
+MUTATOR_HOOKFUNCTION(surv, reset_map_global)
+{
+ LOG_TRACE("Survival: reset_map_global");
+ surv_allowed_to_spawn = true;
+ if (surv_roundtype == SURVIVAL_ROUND_FIRST)
+ {
+ FOREACH_CLIENT(IS_REAL_CLIENT(it),
+ {
+ it.surv_round_time_stat = 0;
+ });
+ }
+ return true;
+}
+
+MUTATOR_HOOKFUNCTION(surv, reset_map_players)
+{
+ LOG_TRACE("Survival: reset_map_players");
+ surv_numattackersalive = 0;
+ surv_numdefendersalive = 0;
+ if (surv_warmup)
+ {
+ surv_warmup = false;
+ }
+ else if (surv_type == SURVIVAL_TYPE_VERSUS)
+ {
+ Surv_SwapTeams();
+ }
+ FOREACH_CLIENT(true,
+ {
+ it.killcount = 0;
+ if ((it.surv_state == SURVIVAL_STATE_NOT_PLAYING) && IS_BOT_CLIENT(it))
+ {
+ it.team = -1;
+ it.surv_state = SURVIVAL_STATE_PLAYING;
+ }
+ if (it.surv_state == SURVIVAL_STATE_PLAYING)
+ {
+ TRANSMUTE(Player, it);
+ it.surv_state = SURVIVAL_STATE_PLAYING;
+ PutClientInServer(it);
+ }
+ });
+ bot_relinkplayerlist();
+ return true;
+}
+
+/// \brief Hook that is called when player spawns.
+MUTATOR_HOOKFUNCTION(surv, PlayerSpawn)
+{
+ entity player = M_ARGV(0, entity);
+ LOG_TRACE("Survival: PlayerSpawn, player = ", player.netname);
+ player.surv_state = SURVIVAL_STATE_PLAYING;
+ Surv_DeterminePlayerModel(player);
+ if (player.surv_savedplayerstate != NULL)
+ {
+ Surv_RestorePlayerState(player, player.surv_savedplayerstate);
+ delete(player.surv_savedplayerstate);
+ player.surv_savedplayerstate = NULL;
+ }
+ else
+ {
+ PlayerTemplateHook_PlayerSpawn(player, Surv_GetPlayerTemplate(player));
+ }
+ switch (player.team)
+ {
+ case surv_attackerteam:
+ {
+ if (player.surv_role == SURVIVAL_ROLE_PLAYER)
+ {
+ Send_Notification(NOTIF_ONE, player, MSG_CENTER,
+ CENTER_ASSAULT_ATTACKING);
+ }
+ break;
+ }
+ case surv_defenderteam:
+ {
+ Send_Notification(NOTIF_ONE, player, MSG_CENTER,
+ CENTER_ASSAULT_DEFENDING);
+ break;
+ }
+ }
+ //Surv_CountAlivePlayers();
+ Surv_AddPlayerToAliveList(player, player.team);
+}
+
+/// \brief UGLY HACK. This is called every frame to keep player model correct.
+MUTATOR_HOOKFUNCTION(surv, FixPlayermodel)
+{
+ entity player = M_ARGV(2, entity);
+ M_ARGV(0, string) = player.surv_playermodel;
+}
+
+/// \brief Hook which is called when the player tries to throw their weapon.
+MUTATOR_HOOKFUNCTION(surv, ForbidThrowCurrentWeapon)
+{
+ entity player = M_ARGV(0, entity);
+ return PlayerTemplateHook_ForbidThrowCurrentWeapon(
+ Surv_GetPlayerTemplate(player));
+}
+
+/// \brief Hook that is called every frame to determine how player health should
+/// regenerate.
+MUTATOR_HOOKFUNCTION(surv, PlayerRegen)
+{
+ entity player = M_ARGV(0, entity);
+ if (player.team == surv_defenderteam)
+ {
+ return true;
+ }
+ return PlayerTemplateHook_PlayerRegen(player,
+ Surv_GetPlayerTemplate(player));
+}
+
+/// \brief Hook that is called to determine if balance messages will appear.
+MUTATOR_HOOKFUNCTION(surv, HideTeamNagger)
+{
+ return true;
+}
+
+/// \brief Hook that is called when player touches an item.
+MUTATOR_HOOKFUNCTION(surv, ItemTouch)
+{
+ entity item = M_ARGV(0, entity);
+ entity player = M_ARGV(1, entity);
+ switch (player.team)
+ {
+ case surv_attackerteam:
+ {
+ return PlayerTemplateHook_ItemTouch(player, item,
+ Surv_GetPlayerTemplate(player));
+ }
+ case surv_defenderteam:
+ {
+ switch (item.classname)
+ {
+ case "item_strength":
+ {
+ W_GiveWeapon(player, WEP_HMG.m_id);
+ player.superweapons_finished = max(
+ player.superweapons_finished, time) +
+ autocvar_g_balance_superweapons_time;
+ Item_ScheduleRespawn(item);
+ sound(player, CH_TRIGGER, SND_Strength, VOL_BASE,
+ ATTEN_NORM);
+ return MUT_ITEMTOUCH_RETURN;
+ }
+ case "item_invincible":
+ {
+ W_GiveWeapon(player, WEP_RPC.m_id);
+ player.superweapons_finished = max(
+ player.superweapons_finished, time) +
+ autocvar_g_balance_superweapons_time;
+ Item_ScheduleRespawn(item);
+ sound(player, CH_TRIGGER, SND_Shield, VOL_BASE, ATTEN_NORM);
+ return MUT_ITEMTOUCH_RETURN;
+ }
+ default:
+ {
+ return PlayerTemplateHook_ItemTouch(player, item,
+ Surv_GetPlayerTemplate(player));
+ }
+ }
+ DebugPrintToChat(player, item.classname);
+ return MUT_ITEMTOUCH_RETURN;
+ }
+ }
+ return MUT_ITEMTOUCH_CONTINUE;
+}
+
+/// \brief Hook which is called when the damage amount must be determined.
+MUTATOR_HOOKFUNCTION(surv, Damage_Calculate)
+{
+ entity frag_attacker = M_ARGV(1, entity);
+ entity frag_target = M_ARGV(2, entity);
+ float deathtype = M_ARGV(3, float);
+ float damage = M_ARGV(4, float);
+ M_ARGV(4, float) = PlayerTemplateHook_Damage_Calculate(frag_attacker,
+ Surv_GetPlayerTemplate(frag_attacker), frag_target,
+ Surv_GetPlayerTemplate(frag_target), deathtype, damage);
+}
+
+/// \brief Hook which is called when the player was damaged.
+MUTATOR_HOOKFUNCTION(surv, PlayerDamaged)
+{
+ entity target = M_ARGV(1, entity);
+ if (target.team != surv_defenderteam)
+ {
+ return;
+ }
+ Surv_UpdateDefenderHealthStat();
+ entity attacker = M_ARGV(0, entity);
+ if ((attacker.team == surv_attackerteam) && (attacker.surv_role ==
+ SURVIVAL_ROLE_PLAYER))
+ {
+ float health = M_ARGV(2, float);
+ float armor = M_ARGV(3, float);
+ float score = (health + armor) * autocvar_g_surv_attacker_damage_score;
+ GameRules_scoring_add(attacker, SCORE, score);
+ }
+ if (autocvar_g_surv_stealth)
+ {
+ return;
+ }
+ if (target.health < 1)
+ {
+ WaypointSprite_Kill(target.surv_attack_sprite);
+ WaypointSprite_Kill(target.surv_defend_sprite);
+ }
+ else
+ {
+ Surv_UpdateWaypointSpriteHealth(target);
+ }
+}
+
+/// \brief Hook which is called when the player dies.
+MUTATOR_HOOKFUNCTION(surv, PlayerDies, CBC_ORDER_FIRST)
+{
+ //DebugPrintToChatAll("PlayerDies");
+ entity attacker = M_ARGV(1, entity);
+ entity victim = M_ARGV(2, entity);
+ PlayerTemplateHook_PlayerDies(victim, Surv_GetPlayerTemplate(victim));
+ if ((attacker.team == surv_defenderteam) &&
+ (victim.team == surv_attackerteam))
+ {
+ switch (victim.surv_role)
+ {
+ case SURVIVAL_ROLE_PLAYER:
+ {
+ GiveResource(attacker, RESOURCE_HEALTH,
+ autocvar_g_surv_defender_attacker_frag_health);
+ GiveResource(attacker, RESOURCE_ARMOR,
+ autocvar_g_surv_defender_attacker_frag_armor);
+ GiveResource(attacker, RESOURCE_SHELLS,
+ autocvar_g_surv_defender_attacker_frag_shells);
+ GiveResource(attacker, RESOURCE_BULLETS,
+ autocvar_g_surv_defender_attacker_frag_bullets);
+ GiveResource(attacker, RESOURCE_ROCKETS,
+ autocvar_g_surv_defender_attacker_frag_rockets);
+ GiveResource(attacker, RESOURCE_CELLS,
+ autocvar_g_surv_defender_attacker_frag_cells);
+ GiveResource(attacker, RESOURCE_PLASMA,
+ autocvar_g_surv_defender_attacker_frag_plasma);
+ GiveResource(attacker, RESOURCE_FUEL,
+ autocvar_g_surv_defender_attacker_frag_fuel);
+ break;
+ }
+ case SURVIVAL_ROLE_CANNON_FODDER:
+ {
+ GiveResource(attacker, RESOURCE_HEALTH,
+ autocvar_g_surv_defender_cannon_fodder_frag_health);
+ GiveResource(attacker, RESOURCE_ARMOR,
+ autocvar_g_surv_defender_cannon_fodder_frag_armor);
+ GiveResource(attacker, RESOURCE_SHELLS,
+ autocvar_g_surv_defender_cannon_fodder_frag_shells);
+ GiveResource(attacker, RESOURCE_BULLETS,
+ autocvar_g_surv_defender_cannon_fodder_frag_bullets);
+ GiveResource(attacker, RESOURCE_ROCKETS,
+ autocvar_g_surv_defender_cannon_fodder_frag_rockets);
+ GiveResource(attacker, RESOURCE_CELLS,
+ autocvar_g_surv_defender_cannon_fodder_frag_cells);
+ GiveResource(attacker, RESOURCE_PLASMA,
+ autocvar_g_surv_defender_cannon_fodder_frag_plasma);
+ GiveResource(attacker, RESOURCE_FUEL,
+ autocvar_g_surv_defender_cannon_fodder_frag_fuel);
+ break;
+ }
+ }
+ }
+ if (!Surv_CanPlayerSpawn(victim))
+ {
+ victim.respawn_flags = RESPAWN_SILENT;
+ if (IS_BOT_CLIENT(victim))
+ {
+ bot_clear(victim);
+ }
+ }
+ return true;
+}
+
+/// \brief Hook which is called after the player died.
+MUTATOR_HOOKFUNCTION(surv, PlayerDied)
+{
+ //DebugPrintToChatAll("PlayerDied");
+ entity player = M_ARGV(0, entity);
+ Surv_RemovePlayerFromAliveList(player, player.team);
+ //Surv_CountAlivePlayers();
+}
+
+/// \brief Hook which is called when player has scored a frag.
+MUTATOR_HOOKFUNCTION(surv, GiveFragsForKill, CBC_ORDER_FIRST)
+{
+ if (surv_type == SURVIVAL_TYPE_COOP)
+ {
+ return true;
+ }
+ entity attacker = M_ARGV(0, entity);
+ if ((attacker.team == surv_defenderteam) || (attacker.surv_role ==
+ SURVIVAL_ROLE_CANNON_FODDER))
+ {
+ M_ARGV(2, float) = 0;
+ return true;
+ }
+ entity target = M_ARGV(1, entity);
+ if ((attacker.surv_role == SURVIVAL_ROLE_PLAYER) && (target.team ==
+ surv_defenderteam))
+ {
+ M_ARGV(2, float) = autocvar_g_surv_attacker_frag_score;
+ }
+ return true;
+}
+
+MUTATOR_HOOKFUNCTION(surv, SpectateSet)
+{
+ entity client = M_ARGV(0, entity);
+ entity targ = M_ARGV(1, entity);
+
+ if (!autocvar_g_surv_spectate_enemies &&
+ (client.surv_state == SURVIVAL_STATE_PLAYING) &&
+ DIFF_TEAM(targ, client))
+ {
+ return true;
+ }
+}
+
+MUTATOR_HOOKFUNCTION(surv, SpectateNext)
+{
+ entity client = M_ARGV(0, entity);
+
+ if (!autocvar_g_surv_spectate_enemies &&
+ (client.surv_state == SURVIVAL_STATE_PLAYING))
+ {
+ entity targ = M_ARGV(1, entity);
+ M_ARGV(1, entity) = CA_SpectateNext(client, targ);
+ return true;
+ }
+}
+
+MUTATOR_HOOKFUNCTION(surv, SpectatePrev)
+{
+ entity client = M_ARGV(0, entity);
+ entity targ = M_ARGV(1, entity);
+ entity first = M_ARGV(2, entity);
+
+ if (!autocvar_g_surv_spectate_enemies &&
+ (client.surv_state == SURVIVAL_STATE_PLAYING))
+ {
+ do
+ {
+ targ = targ.chain;
+ }
+ while (targ && DIFF_TEAM(targ, client));
+ if (!targ)
+ {
+ for (targ = first; targ && DIFF_TEAM(targ, client);
+ targ = targ.chain);
+
+ if (targ == client.enemy)
+ {
+ return MUT_SPECPREV_RETURN;
+ }
+ }
+ }
+ M_ARGV(1, entity) = targ;
+ return MUT_SPECPREV_FOUND;
+}
+
+/// \brief I'm not sure exactly what this function does but it is very
+/// important. Without it bots are completely broken. Is it a hack? Of course.
+MUTATOR_HOOKFUNCTION(surv, Bot_FixCount, CBC_ORDER_EXCLUSIVE)
+{
+ FOREACH_CLIENT(IS_REAL_CLIENT(it),
+ {
+ if (IS_PLAYER(it) || (it.surv_state == SURVIVAL_STATE_PLAYING))
+ {
+ ++M_ARGV(0, int);
+ }
+ ++M_ARGV(1, int);
+ });
+ return true;
+}
+
+MUTATOR_HOOKFUNCTION(surv, Scores_CountFragsRemaining)
+{
+ // Don't announce remaining frags
+ return false;
+}
#endif
}
-void SetPlayerColors(entity pl, float _color)
+void SetPlayerColors(entity player, float _color)
{
- /*string s;
- s = ftos(cl);
- stuffcmd(pl, strcat("color ", s, " ", s, "\n") );
- pl.team = cl + 1;
- //pl.clientcolors = pl.clientcolors - (pl.clientcolors & 15) + cl;
- pl.clientcolors = 16*cl + cl;*/
-
- float pants, shirt;
- pants = _color & 0x0F;
- shirt = _color & 0xF0;
-
-
- if(teamplay) {
- setcolor(pl, 16*pants + pants);
- } else {
- setcolor(pl, shirt + pants);
+ float pants = _color & 0x0F;
+ float shirt = _color & 0xF0;
+ if (teamplay)
+ {
+ setcolor(player, 16 * pants + pants);
+ }
+ else
+ {
+ setcolor(player, shirt + pants);
}
}
-void SetPlayerTeam(entity pl, float t, float s, float noprint)
+void KillPlayerForTeamChange(entity player)
{
- float _color;
-
- if(t == 4)
- _color = NUM_TEAM_4 - 1;
- else if(t == 3)
- _color = NUM_TEAM_3 - 1;
- else if(t == 2)
- _color = NUM_TEAM_2 - 1;
- else
- _color = NUM_TEAM_1 - 1;
-
- SetPlayerColors(pl,_color);
-
- if(t != s) {
- LogTeamchange(pl.playerid, pl.team, 3); // log manual team join
+ if (IS_DEAD(player))
+ {
+ return;
+ }
+ if (MUTATOR_CALLHOOK(Player_ChangeTeamKill, player) == true)
+ {
+ return;
+ }
+ Damage(player, player, player, 100000, DEATH_TEAMCHANGE.m_id, player.origin,
+ '0 0 0');
+}
- if(!noprint)
- bprint(playername(pl, false), "^7 has changed from ", Team_NumberToColoredFullName(s), "^7 to ", Team_NumberToColoredFullName(t), "\n");
+bool SetPlayerTeamSimple(entity player, int team_num)
+{
+ if (player.team == team_num)
+ {
+ // This is important when players join the game and one of their color
+ // matches the team color while other doesn't. For example [BOT]Lion.
+ SetPlayerColors(player, team_num - 1);
+ return true;
}
+ if (MUTATOR_CALLHOOK(Player_ChangeTeam, player, Team_TeamToNumber(
+ player.team), Team_TeamToNumber(team_num)) == true)
+ {
+ // Mutator has blocked team change.
+ return false;
+ }
+ int old_team = player.team;
+ SetPlayerColors(player, team_num - 1);
+ MUTATOR_CALLHOOK(Player_ChangedTeam, player, old_team, player.team);
+ return true;
+}
+bool SetPlayerTeam(entity player, int destination_team, int source_team,
+ bool no_print)
+{
+ int team_num = Team_NumberToTeam(destination_team);
+ if (!SetPlayerTeamSimple(player, team_num))
+ {
+ return false;
+ }
+ LogTeamchange(player.playerid, player.team, 3); // log manual team join
+ if (no_print)
+ {
+ return true;
+ }
+ bprint(playername(player, false), "^7 has changed from ", Team_NumberToColoredFullName(source_team), "^7 to ", Team_NumberToColoredFullName(destination_team), "\n");
+ return true;
}
// set c1...c4 to show what teams are allowed
-void CheckAllowedTeams (entity for_whom)
+void CheckAllowedTeams(entity for_whom)
{
int teams_mask = 0;
c1 = c2 = c3 = c4 = -1;
- cb1 = cb2 = cb3 = cb4 = 0;
+ num_bots_team1 = num_bots_team2 = num_bots_team3 = num_bots_team4 = 0;
string teament_name = string_null;
// teams that are allowed will now have their player counts stored in c1...c4
void GetTeamCounts(entity ignore)
{
- float value, bvalue;
- // now count how many players are on each team already
-
- // FIXME: also find and memorize the lowest-scoring bot on each team (in case players must be shuffled around)
- // also remember the lowest-scoring player
-
- FOREACH_CLIENT(true, {
- float t;
- if(IS_PLAYER(it) || it.caplayer)
- t = it.team;
- else if(it.team_forced > 0)
- t = it.team_forced; // reserve the spot
- else
- continue;
- if(it != ignore)// && it.netname != "")
+ if (MUTATOR_CALLHOOK(GetTeamCounts) == true)
+ {
+ if (c1 >= 0)
+ {
+ MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_1, ignore, c1,
+ num_bots_team1, lowest_human_team1, lowest_bot_team1);
+ c1 = M_ARGV(2, float);
+ num_bots_team1 = M_ARGV(3, float);
+ lowest_human_team1 = M_ARGV(4, entity);
+ lowest_bot_team1 = M_ARGV(5, entity);
+ }
+ if (c2 >= 0)
+ {
+ MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_2, ignore, c2,
+ num_bots_team2, lowest_human_team2, lowest_bot_team2);
+ c2 = M_ARGV(2, float);
+ num_bots_team2 = M_ARGV(3, float);
+ lowest_human_team2 = M_ARGV(4, entity);
+ lowest_bot_team2 = M_ARGV(5, entity);
+ }
+ if (c3 >= 0)
+ {
+ MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_3, ignore, c3,
+ num_bots_team3, lowest_human_team3, lowest_bot_team3);
+ c3 = M_ARGV(2, float);
+ num_bots_team3 = M_ARGV(3, float);
+ lowest_human_team3 = M_ARGV(4, entity);
+ lowest_bot_team3 = M_ARGV(5, entity);
+ }
+ if (c4 >= 0)
+ {
+ MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_4, ignore,
+ c4, num_bots_team4, lowest_human_team4, lowest_bot_team4);
+ c4 = M_ARGV(2, float);
+ num_bots_team4 = M_ARGV(3, float);
+ lowest_human_team4 = M_ARGV(4, entity);
+ lowest_bot_team4 = M_ARGV(5, entity);
+ }
+ }
+ else
+ {
+ float value, bvalue;
+ // now count how many players are on each team already
+ float lowest_human_score1 = FLOAT_MAX;
+ float lowest_bot_score1 = FLOAT_MAX;
+ float lowest_human_score2 = FLOAT_MAX;
+ float lowest_bot_score2 = FLOAT_MAX;
+ float lowest_human_score3 = FLOAT_MAX;
+ float lowest_bot_score3 = FLOAT_MAX;
+ float lowest_human_score4 = FLOAT_MAX;
+ float lowest_bot_score4 = FLOAT_MAX;
+ FOREACH_CLIENT(true,
{
+ float t;
+ if (IS_PLAYER(it) || it.caplayer)
+ {
+ t = it.team;
+ }
+ else if (it.team_forced > 0)
+ {
+ t = it.team_forced; // reserve the spot
+ }
+ else
+ {
+ continue;
+ }
+ if (it == ignore)
+ {
+ continue;
+ }
value = PlayerValue(it);
- if(IS_BOT_CLIENT(it))
+ if (IS_BOT_CLIENT(it))
+ {
bvalue = value;
+ }
else
+ {
bvalue = 0;
- if(t == NUM_TEAM_1)
+ }
+ if (value == 0)
{
- if(c1 >= 0)
- {
- c1 = c1 + value;
- cb1 = cb1 + bvalue;
- }
+ continue;
}
- else if(t == NUM_TEAM_2)
+ switch (t)
{
- if(c2 >= 0)
+ case NUM_TEAM_1:
{
- c2 = c2 + value;
- cb2 = cb2 + bvalue;
+ if (c1 < 0)
+ {
+ break;
+ }
+ c1 += value;
+ num_bots_team1 += bvalue;
+ float temp_score = PlayerScore_Get(it, SP_SCORE);
+ if (!bvalue)
+ {
+ if (temp_score < lowest_human_score1)
+ {
+ lowest_human_team1 = it;
+ lowest_human_score1 = temp_score;
+ }
+ break;
+ }
+ if (temp_score < lowest_bot_score1)
+ {
+ lowest_bot_team1 = it;
+ lowest_bot_score1 = temp_score;
+ }
+ break;
}
- }
- else if(t == NUM_TEAM_3)
- {
- if(c3 >= 0)
+ case NUM_TEAM_2:
{
- c3 = c3 + value;
- cb3 = cb3 + bvalue;
+ if (c2 < 0)
+ {
+ break;
+ }
+ c2 += value;
+ num_bots_team2 += bvalue;
+ float temp_score = PlayerScore_Get(it, SP_SCORE);
+ if (!bvalue)
+ {
+ if (temp_score < lowest_human_score2)
+ {
+ lowest_human_team2 = it;
+ lowest_human_score2 = temp_score;
+ }
+ break;
+ }
+ if (temp_score < lowest_bot_score2)
+ {
+ lowest_bot_team2 = it;
+ lowest_bot_score2 = temp_score;
+ }
+ break;
}
- }
- else if(t == NUM_TEAM_4)
- {
- if(c4 >= 0)
+ case NUM_TEAM_3:
+ {
+ if (c3 < 0)
+ {
+ break;
+ }
+ c3 += value;
+ num_bots_team3 += bvalue;
+ float temp_score = PlayerScore_Get(it, SP_SCORE);
+ if (!bvalue)
+ {
+ if (temp_score < lowest_human_score3)
+ {
+ lowest_human_team3 = it;
+ lowest_human_score3 = temp_score;
+ }
+ break;
+ }
+ if (temp_score < lowest_bot_score3)
+ {
+ lowest_bot_team3 = it;
+ lowest_bot_score3 = temp_score;
+ }
+ break;
+ }
+ case NUM_TEAM_4:
{
- c4 = c4 + value;
- cb4 = cb4 + bvalue;
+ if (c4 < 0)
+ {
+ break;
+ }
+ c4 += value;
+ num_bots_team4 += bvalue;
+ float temp_score = PlayerScore_Get(it, SP_SCORE);
+ if (!bvalue)
+ {
+ if (temp_score < lowest_human_score4)
+ {
+ lowest_human_team4 = it;
+ lowest_human_score4 = temp_score;
+ }
+ break;
+ }
+ if (temp_score < lowest_bot_score4)
+ {
+ lowest_bot_team4 = it;
+ lowest_bot_score4 = temp_score;
+ }
+ break;
}
}
- }
- });
+ });
+ }
// if the player who has a forced team has not joined yet, reserve the spot
if(autocvar_g_campaign)
{
switch(autocvar_g_campaign_forceteam)
{
- case 1: if(c1 == cb1) ++c1; break;
- case 2: if(c2 == cb2) ++c2; break;
- case 3: if(c3 == cb3) ++c3; break;
- case 4: if(c4 == cb4) ++c4; break;
+ case 1: if(c1 == num_bots_team1) ++c1; break;
+ case 2: if(c2 == num_bots_team2) ++c2; break;
+ case 3: if(c3 == num_bots_team3) ++c3; break;
+ case 4: if(c4 == num_bots_team4) ++c4; break;
}
}
}
-float TeamSmallerEqThanTeam(float ta, float tb, entity e)
+bool IsTeamSmallerThanTeam(int team_a, int team_b, entity player,
+ bool use_score)
{
+ if (team_a == team_b)
+ {
+ return false;
+ }
// we assume that CheckAllowedTeams and GetTeamCounts have already been called
- float f;
- float ca = -1, cb = -1, cba = 0, cbb = 0, sa = 0, sb = 0;
-
- switch(ta)
+ int num_players_team_a = -1, num_players_team_b = -1;
+ int num_bots_team_a = 0, num_bots_team_b = 0;
+ float score_team_a = 0, score_team_b = 0;
+ switch (team_a)
{
- case 1: ca = c1; cba = cb1; sa = team1_score; break;
- case 2: ca = c2; cba = cb2; sa = team2_score; break;
- case 3: ca = c3; cba = cb3; sa = team3_score; break;
- case 4: ca = c4; cba = cb4; sa = team4_score; break;
+ case 1:
+ {
+ num_players_team_a = c1;
+ num_bots_team_a = num_bots_team1;
+ score_team_a = team1_score;
+ break;
+ }
+ case 2:
+ {
+ num_players_team_a = c2;
+ num_bots_team_a = num_bots_team2;
+ score_team_a = team2_score;
+ break;
+ }
+ case 3:
+ {
+ num_players_team_a = c3;
+ num_bots_team_a = num_bots_team3;
+ score_team_a = team3_score;
+ break;
+ }
+ case 4:
+ {
+ num_players_team_a = c4;
+ num_bots_team_a = num_bots_team4;
+ score_team_a = team4_score;
+ break;
+ }
}
- switch(tb)
+ switch (team_b)
{
- case 1: cb = c1; cbb = cb1; sb = team1_score; break;
- case 2: cb = c2; cbb = cb2; sb = team2_score; break;
- case 3: cb = c3; cbb = cb3; sb = team3_score; break;
- case 4: cb = c4; cbb = cb4; sb = team4_score; break;
+ case 1:
+ {
+ num_players_team_b = c1;
+ num_bots_team_b = num_bots_team1;
+ score_team_b = team1_score;
+ break;
+ }
+ case 2:
+ {
+ num_players_team_b = c2;
+ num_bots_team_b = num_bots_team2;
+ score_team_b = team2_score;
+ break;
+ }
+ case 3:
+ {
+ num_players_team_b = c3;
+ num_bots_team_b = num_bots_team3;
+ score_team_b = team3_score;
+ break;
+ }
+ case 4:
+ {
+ num_players_team_b = c4;
+ num_bots_team_b = num_bots_team4;
+ score_team_b = team4_score;
+ break;
+ }
}
-
// invalid
- if(ca < 0 || cb < 0)
+ if (num_players_team_a < 0 || num_players_team_b < 0)
+ {
return false;
-
- // equal
- if(ta == tb)
+ }
+ if (IS_REAL_CLIENT(player) && bots_would_leave)
+ {
+ num_players_team_a -= num_bots_team_a;
+ num_players_team_b -= num_bots_team_b;
+ }
+ if (!use_score)
+ {
+ return num_players_team_a < num_players_team_b;
+ }
+ if (num_players_team_a < num_players_team_b)
+ {
return true;
+ }
+ if (num_players_team_a > num_players_team_b)
+ {
+ return false;
+ }
+ return score_team_a < score_team_b;
+}
- if(IS_REAL_CLIENT(e))
+bool IsTeamEqualToTeam(int team_a, int team_b, entity player, bool use_score)
+{
+ if (team_a == team_b)
{
- if(bots_would_leave)
+ return true;
+ }
+ // we assume that CheckAllowedTeams and GetTeamCounts have already been called
+ int num_players_team_a = -1, num_players_team_b = -1;
+ int num_bots_team_a = 0, num_bots_team_b = 0;
+ float score_team_a = 0, score_team_b = 0;
+ switch (team_a)
+ {
+ case 1:
{
- ca -= cba * 0.999;
- cb -= cbb * 0.999;
+ num_players_team_a = c1;
+ num_bots_team_a = num_bots_team1;
+ score_team_a = team1_score;
+ break;
+ }
+ case 2:
+ {
+ num_players_team_a = c2;
+ num_bots_team_a = num_bots_team2;
+ score_team_a = team2_score;
+ break;
+ }
+ case 3:
+ {
+ num_players_team_a = c3;
+ num_bots_team_a = num_bots_team3;
+ score_team_a = team3_score;
+ break;
+ }
+ case 4:
+ {
+ num_players_team_a = c4;
+ num_bots_team_a = num_bots_team4;
+ score_team_a = team4_score;
+ break;
}
}
+ switch (team_b)
+ {
+ case 1:
+ {
+ num_players_team_b = c1;
+ num_bots_team_b = num_bots_team1;
+ score_team_b = team1_score;
+ break;
+ }
+ case 2:
+ {
+ num_players_team_b = c2;
+ num_bots_team_b = num_bots_team2;
+ score_team_b = team2_score;
+ break;
+ }
+ case 3:
+ {
+ num_players_team_b = c3;
+ num_bots_team_b = num_bots_team3;
+ score_team_b = team3_score;
+ break;
+ }
+ case 4:
+ {
+ num_players_team_b = c4;
+ num_bots_team_b = num_bots_team4;
+ score_team_b = team4_score;
+ break;
+ }
+ }
+ // invalid
+ if (num_players_team_a < 0 || num_players_team_b < 0)
+ return false;
- // keep teams alive (teams of size 0 always count as smaller, ignoring score)
- if(ca < 1)
- if(cb >= 1)
- return true;
- if(ca >= 1)
- if(cb < 1)
- return false;
-
- // first, normalize
- f = max(ca, cb, 1);
- ca /= f;
- cb /= f;
- f = max(sa, sb, 1);
- sa /= f;
- sb /= f;
-
- // the more we're at the end of the match, the more take scores into account
- f = bound(0, game_completion_ratio * autocvar_g_balance_teams_scorefactor, 1);
- ca += (sa - ca) * f;
- cb += (sb - cb) * f;
+ if (IS_REAL_CLIENT(player) && bots_would_leave)
+ {
+ num_players_team_a -= num_bots_team_a;
+ num_players_team_b -= num_bots_team_b;
+ }
+ if (!use_score)
+ {
+ return num_players_team_a == num_players_team_b;
+ }
+ if (num_players_team_a != num_players_team_b)
+ {
+ return false;
+ }
+ return score_team_a == score_team_b;
+}
- return ca <= cb;
+int FindBestTeams(entity player, bool use_score)
+{
+ if (MUTATOR_CALLHOOK(FindBestTeams, player) == true)
+ {
+ return M_ARGV(1, float);
+ }
+ int team_bits = 0;
+ int previous_team = 0;
+ if (c1 >= 0)
+ {
+ team_bits = BIT(0);
+ previous_team = 1;
+ }
+ if (c2 >= 0)
+ {
+ if (previous_team == 0)
+ {
+ team_bits = BIT(1);
+ previous_team = 2;
+ }
+ else if (IsTeamSmallerThanTeam(2, previous_team, player, use_score))
+ {
+ team_bits = BIT(1);
+ previous_team = 2;
+ }
+ else if (IsTeamEqualToTeam(2, previous_team, player, use_score))
+ {
+ team_bits |= BIT(1);
+ previous_team = 2;
+ }
+ }
+ if (c3 >= 0)
+ {
+ if (previous_team == 0)
+ {
+ team_bits = BIT(2);
+ previous_team = 3;
+ }
+ else if (IsTeamSmallerThanTeam(3, previous_team, player, use_score))
+ {
+ team_bits = BIT(2);
+ previous_team = 3;
+ }
+ else if (IsTeamEqualToTeam(3, previous_team, player, use_score))
+ {
+ team_bits |= BIT(2);
+ previous_team = 3;
+ }
+ }
+ if (c4 >= 0)
+ {
+ if (previous_team == 0)
+ {
+ team_bits = BIT(3);
+ }
+ else if (IsTeamSmallerThanTeam(4, previous_team, player, use_score))
+ {
+ team_bits = BIT(3);
+ }
+ else if (IsTeamEqualToTeam(4, previous_team, player, use_score))
+ {
+ team_bits |= BIT(3);
+ }
+ }
+ return team_bits;
}
// returns # of smallest team (1, 2, 3, 4)
// NOTE: Assumes CheckAllowedTeams has already been called!
-float FindSmallestTeam(entity pl, float ignore_pl)
+int FindSmallestTeam(entity player, float ignore_player)
{
- int totalteams = 0;
- int t = 1; // initialize with a random team?
- if(c4 >= 0) t = 4;
- if(c3 >= 0) t = 3;
- if(c2 >= 0) t = 2;
- if(c1 >= 0) t = 1;
-
- // find out what teams are available
- //CheckAllowedTeams();
-
- // make sure there are at least 2 teams to join
- if(c1 >= 0)
- totalteams = totalteams + 1;
- if(c2 >= 0)
- totalteams = totalteams + 1;
- if(c3 >= 0)
- totalteams = totalteams + 1;
- if(c4 >= 0)
- totalteams = totalteams + 1;
-
- if((autocvar_bot_vs_human || pl.team_forced > 0) && totalteams == 1)
- totalteams += 1;
-
- if(totalteams <= 1)
+ // count how many players are in each team
+ if (ignore_player)
{
- if(autocvar_g_campaign && pl && IS_REAL_CLIENT(pl))
- return 1; // special case for campaign and player joining
- else if(totalteams == 1) // single team
- LOG_TRACEF("Only 1 team available for %s, you may need to fix your map", MapInfo_Type_ToString(MapInfo_CurrentGametype()));
- else // no teams, major no no
- error(sprintf("No teams available for %s\n", MapInfo_Type_ToString(MapInfo_CurrentGametype())));
+ GetTeamCounts(player);
}
-
- // count how many players are in each team
- if(ignore_pl)
- GetTeamCounts(pl);
else
+ {
GetTeamCounts(NULL);
-
+ }
+ int team_bits = FindBestTeams(player, true);
+ if (team_bits == 0)
+ {
+ error(sprintf("No teams available for %s\n", MapInfo_Type_ToString(MapInfo_CurrentGametype())));
+ }
RandomSelection_Init();
-
- if(TeamSmallerEqThanTeam(1, t, pl))
- t = 1;
- if(TeamSmallerEqThanTeam(2, t, pl))
- t = 2;
- if(TeamSmallerEqThanTeam(3, t, pl))
- t = 3;
- if(TeamSmallerEqThanTeam(4, t, pl))
- t = 4;
-
- // now t is the minimum, or A minimum!
- if(t == 1 || TeamSmallerEqThanTeam(1, t, pl))
+ if ((team_bits & BIT(0)) != 0)
+ {
RandomSelection_AddFloat(1, 1, 1);
- if(t == 2 || TeamSmallerEqThanTeam(2, t, pl))
+ }
+ if ((team_bits & BIT(1)) != 0)
+ {
RandomSelection_AddFloat(2, 1, 1);
- if(t == 3 || TeamSmallerEqThanTeam(3, t, pl))
+ }
+ if ((team_bits & BIT(2)) != 0)
+ {
RandomSelection_AddFloat(3, 1, 1);
- if(t == 4 || TeamSmallerEqThanTeam(4, t, pl))
+ }
+ if ((team_bits & BIT(3)) != 0)
+ {
RandomSelection_AddFloat(4, 1, 1);
-
+ }
return RandomSelection_chosen_float;
}
-int JoinBestTeam(entity this, bool only_return_best, bool forcebestteam)
+int JoinBestTeam(entity this, bool only_return_best, bool force_best_team)
{
- float smallest, selectedteam;
-
// don't join a team if we're not playing a team game
- if(!teamplay)
+ if (!teamplay)
+ {
return 0;
+ }
// find out what teams are available
CheckAllowedTeams(this);
// if we don't care what team he ends up on, put him on whatever team he entered as.
// if he's not on a valid team, then let other code put him on the smallest team
- if(!forcebestteam)
+ if (!force_best_team)
{
+ int selected_team;
if( c1 >= 0 && this.team == NUM_TEAM_1)
- selectedteam = this.team;
+ selected_team = this.team;
else if(c2 >= 0 && this.team == NUM_TEAM_2)
- selectedteam = this.team;
+ selected_team = this.team;
else if(c3 >= 0 && this.team == NUM_TEAM_3)
- selectedteam = this.team;
+ selected_team = this.team;
else if(c4 >= 0 && this.team == NUM_TEAM_4)
- selectedteam = this.team;
+ selected_team = this.team;
else
- selectedteam = -1;
+ selected_team = -1;
- if(selectedteam > 0)
+ if (selected_team > 0)
{
- if(!only_return_best)
+ if (!only_return_best)
{
- SetPlayerColors(this, selectedteam - 1);
+ SetPlayerTeamSimple(this, selected_team);
// when JoinBestTeam is called by client.qc/ClientKill_Now_TeamChange the players team is -1 and thus skipped
// when JoinBestTeam is called by client.qc/ClientConnect the player_id is 0 the log attempt is rejected
LogTeamchange(this.playerid, this.team, 99);
}
- return selectedteam;
+ return selected_team;
}
// otherwise end up on the smallest team (handled below)
}
- smallest = FindSmallestTeam(this, true);
-
- if(!only_return_best && !this.bot_forced_team)
+ int best_team = FindSmallestTeam(this, true);
+ if (only_return_best || this.bot_forced_team)
{
- TeamchangeFrags(this);
- if(smallest == 1)
- {
- SetPlayerColors(this, NUM_TEAM_1 - 1);
- }
- else if(smallest == 2)
- {
- SetPlayerColors(this, NUM_TEAM_2 - 1);
- }
- else if(smallest == 3)
- {
- SetPlayerColors(this, NUM_TEAM_3 - 1);
- }
- else if(smallest == 4)
- {
- SetPlayerColors(this, NUM_TEAM_4 - 1);
- }
- else
- {
- error("smallest team: invalid team\n");
- }
-
- LogTeamchange(this.playerid, this.team, 2); // log auto join
-
- if(!IS_DEAD(this))
- Damage(this, this, this, 100000, DEATH_TEAMCHANGE.m_id, this.origin, '0 0 0');
+ return best_team;
}
-
- return smallest;
+ best_team = Team_NumberToTeam(best_team);
+ if (best_team == -1)
+ {
+ error("JoinBestTeam: invalid team\n");
+ }
+ int old_team = Team_TeamToNumber(this.team);
+ TeamchangeFrags(this);
+ SetPlayerTeamSimple(this, best_team);
+ LogTeamchange(this.playerid, this.team, 2); // log auto join
+ if (!IS_BOT_CLIENT(this))
+ {
+ AutoBalanceBots(old_team, Team_TeamToNumber(best_team));
+ }
+ KillPlayerForTeamChange(this);
+ return best_team;
}
-//void() ctf_playerchanged;
void SV_ChangeTeam(entity this, float _color)
{
- float scolor, dcolor, steam, dteam; //, dbotcount, scount, dcount;
+ float source_color, destination_color, source_team, destination_team;
// in normal deathmatch we can just apply the color and we're done
if(!teamplay)
if(!teamplay)
return;
- scolor = this.clientcolors & 0x0F;
- dcolor = _color & 0x0F;
-
- if(scolor == NUM_TEAM_1 - 1)
- steam = 1;
- else if(scolor == NUM_TEAM_2 - 1)
- steam = 2;
- else if(scolor == NUM_TEAM_3 - 1)
- steam = 3;
- else // if(scolor == NUM_TEAM_4 - 1)
- steam = 4;
- if(dcolor == NUM_TEAM_1 - 1)
- dteam = 1;
- else if(dcolor == NUM_TEAM_2 - 1)
- dteam = 2;
- else if(dcolor == NUM_TEAM_3 - 1)
- dteam = 3;
- else // if(dcolor == NUM_TEAM_4 - 1)
- dteam = 4;
+ source_color = this.clientcolors & 0x0F;
+ destination_color = _color & 0x0F;
+
+ source_team = Team_TeamToNumber(source_color + 1);
+ destination_team = Team_TeamToNumber(destination_color + 1);
CheckAllowedTeams(this);
- if(dteam == 1 && c1 < 0) dteam = 4;
- if(dteam == 4 && c4 < 0) dteam = 3;
- if(dteam == 3 && c3 < 0) dteam = 2;
- if(dteam == 2 && c2 < 0) dteam = 1;
+ if (destination_team == 1 && c1 < 0) destination_team = 4;
+ if (destination_team == 4 && c4 < 0) destination_team = 3;
+ if (destination_team == 3 && c3 < 0) destination_team = 2;
+ if (destination_team == 2 && c2 < 0) destination_team = 1;
// not changing teams
- if(scolor == dcolor)
+ if (source_color == destination_color)
{
- //bprint("same team change\n");
- SetPlayerTeam(this, dteam, steam, true);
+ SetPlayerTeam(this, destination_team, source_team, true);
return;
}
}
// autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless
- if(autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
+ if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
{
GetTeamCounts(this);
- if(!TeamSmallerEqThanTeam(dteam, steam, this))
+ if ((BIT(destination_team - 1) & FindBestTeams(this, false)) == 0)
{
Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
return;
}
}
-
-// bprint("allow change teams from ", ftos(steam), " to ", ftos(dteam), "\n");
-
- if(IS_PLAYER(this) && steam != dteam)
+ if(IS_PLAYER(this) && source_team != destination_team)
{
// reduce frags during a team change
TeamchangeFrags(this);
}
-
- MUTATOR_CALLHOOK(Player_ChangeTeam, this, steam, dteam);
-
- SetPlayerTeam(this, dteam, steam, !IS_CLIENT(this));
-
- if(IS_PLAYER(this) && steam != dteam)
+ if (!SetPlayerTeam(this, destination_team, source_team, !IS_CLIENT(this)))
+ {
+ return;
+ }
+ AutoBalanceBots(source_team, destination_team);
+ if (!IS_PLAYER(this) || (source_team == destination_team))
{
- // kill player when changing teams
- if(!IS_DEAD(this))
- Damage(this, this, this, 100000, DEATH_TEAMCHANGE.m_id, this.origin, '0 0 0');
+ return;
}
+ KillPlayerForTeamChange(this);
}
-void ShufflePlayerOutOfTeam (float source_team)
+void AutoBalanceBots(int source_team, int destination_team)
{
- float smallestteam, smallestteam_count, steam;
- float lowest_bot_score, lowest_player_score;
- entity lowest_bot, lowest_player, selected;
-
- smallestteam = 0;
- smallestteam_count = 999999999;
-
- if(c1 >= 0 && c1 < smallestteam_count)
- {
- smallestteam = 1;
- smallestteam_count = c1;
- }
- if(c2 >= 0 && c2 < smallestteam_count)
- {
- smallestteam = 2;
- smallestteam_count = c2;
- }
- if(c3 >= 0 && c3 < smallestteam_count)
+ if ((source_team == -1) || (destination_team == -1))
{
- smallestteam = 3;
- smallestteam_count = c3;
+ return;
}
- if(c4 >= 0 && c4 < smallestteam_count)
+ if (!autocvar_g_balance_teams ||
+ !autocvar_g_balance_teams_prevent_imbalance)
{
- smallestteam = 4;
- smallestteam_count = c4;
- }
-
- if(!smallestteam)
- {
- bprint("warning: no smallest team\n");
return;
}
-
- if(source_team == 1)
- steam = NUM_TEAM_1;
- else if(source_team == 2)
- steam = NUM_TEAM_2;
- else if(source_team == 3)
- steam = NUM_TEAM_3;
- else // if(source_team == 4)
- steam = NUM_TEAM_4;
-
- lowest_bot = NULL;
- lowest_bot_score = 999999999;
- lowest_player = NULL;
- lowest_player_score = 999999999;
-
- // find the lowest-scoring player & bot of that team
- FOREACH_CLIENT(IS_PLAYER(it) && it.team == steam, {
- if(it.isbot)
+ int num_players_source_team = 0;
+ int num_players_destination_team = 0;
+ entity lowest_bot_destination_team = NULL;
+ switch (source_team)
+ {
+ case 1:
{
- if(it.totalfrags < lowest_bot_score)
- {
- lowest_bot = it;
- lowest_bot_score = it.totalfrags;
- }
+ num_players_source_team = c1;
+ break;
}
- else
+ case 2:
{
- if(it.totalfrags < lowest_player_score)
- {
- lowest_player = it;
- lowest_player_score = it.totalfrags;
- }
+ num_players_source_team = c2;
+ break;
+ }
+ case 3:
+ {
+ num_players_source_team = c3;
+ break;
+ }
+ case 4:
+ {
+ num_players_source_team = c4;
+ break;
}
- });
-
- // prefers to move a bot...
- if(lowest_bot != NULL)
- selected = lowest_bot;
- // but it will move a player if it has to
- else
- selected = lowest_player;
- // don't do anything if it couldn't find anyone
- if(!selected)
- {
- bprint("warning: couldn't find a player to move from team\n");
- return;
- }
-
- // smallest team gains a member
- if(smallestteam == 1)
- {
- c1 = c1 + 1;
- }
- else if(smallestteam == 2)
- {
- c2 = c2 + 1;
- }
- else if(smallestteam == 3)
- {
- c3 = c3 + 1;
- }
- else if(smallestteam == 4)
- {
- c4 = c4 + 1;
- }
- else
- {
- bprint("warning: destination team invalid\n");
- return;
- }
- // source team loses a member
- if(source_team == 1)
- {
- c1 = c1 + 1;
- }
- else if(source_team == 2)
- {
- c2 = c2 + 2;
- }
- else if(source_team == 3)
- {
- c3 = c3 + 3;
}
- else if(source_team == 4)
+ switch (destination_team)
{
- c4 = c4 + 4;
+ case 1:
+ {
+ num_players_destination_team = c1;
+ lowest_bot_destination_team = lowest_bot_team1;
+ break;
+ }
+ case 2:
+ {
+ num_players_destination_team = c2;
+ lowest_bot_destination_team = lowest_bot_team2;
+ break;
+ }
+ case 3:
+ {
+ num_players_destination_team = c3;
+ lowest_bot_destination_team = lowest_bot_team3;
+ break;
+ }
+ case 4:
+ {
+ num_players_destination_team = c4;
+ lowest_bot_destination_team = lowest_bot_team4;
+ break;
+ }
}
- else
+ if ((num_players_destination_team <= num_players_source_team) ||
+ (lowest_bot_destination_team == NULL))
{
- bprint("warning: source team invalid\n");
return;
}
-
- // move the player to the new team
- TeamchangeFrags(selected);
- SetPlayerTeam(selected, smallestteam, source_team, false);
-
- if(!IS_DEAD(selected))
- Damage(selected, selected, selected, 100000, DEATH_AUTOTEAMCHANGE.m_id, selected.origin, '0 0 0');
- Send_Notification(NOTIF_ONE, selected, MSG_CENTER, CENTER_DEATH_SELF_AUTOTEAMCHANGE, selected.team);
+ SetPlayerTeamSimple(lowest_bot_destination_team,
+ Team_NumberToTeam(source_team));
+ KillPlayerForTeamChange(lowest_bot_destination_team);
}