1 #include "sv_survival.qh"
3 float autocvar_g_survival_hunter_count = 0.25;
4 float autocvar_g_survival_round_timelimit = 120;
5 float autocvar_g_survival_warmup = 10;
6 bool autocvar_g_survival_punish_teamkill = true;
7 bool autocvar_g_survival_reward_survival = true;
9 void nades_Clear(entity player);
11 entity survivalStatuses;
12 void SurvivalStatuses_Init();
14 void SurvivalStatuses_Send()
16 // SendFlags can be set to anything != 0, SurvivalStatuses_SendEntity won't use its value
17 survivalStatuses.SendFlags = 1;
20 bool SurvivalStatuses_SendEntity(entity this, entity dest, float sendflags)
22 Stream out = MSG_ENTITY;
23 WriteHeader(out, ENT_CLIENT_SURVIVALSTATUSES);
25 sendflags = BIT(0) | BIT(1);
27 // send hunter statuses only to hunters
28 bool send_hunter_statuses = (dest.survival_status == SURV_STATUS_HUNTER || STAT(GAME_STOPPED));
29 if (!send_hunter_statuses)
32 serialize(byte, out, sendflags);
33 if (sendflags & BIT(0)) {
34 for (int i = 1; i <= maxclients; i += 8) {
36 entity e = edict_num(i);
37 for (int b = 0; b < 8; ++b, e = nextent(e)) {
38 bool is_hunter = (IS_PLAYER(e) && e.survival_status == SURV_STATUS_HUNTER);
42 serialize(byte, out, f);
48 void SurvivalStatuses_Init()
52 backtrace("Can't spawn survivalStatuses again!");
55 Net_LinkEntity(survivalStatuses = new_pure(survivalStatuses), false, 0, SurvivalStatuses_SendEntity);
58 void Surv_UpdateScores(bool timed_out)
60 // give players their hard-earned kills now that the round is over
63 it.totalfrags += it.survival_validkills;
64 if(it.survival_validkills)
65 GameRules_scoring_add(it, SCORE, it.survival_validkills);
66 it.survival_validkills = 0;
67 // player survived the round
68 if(IS_PLAYER(it) && !IS_DEAD(it))
70 if(autocvar_g_survival_reward_survival && timed_out && it.survival_status == SURV_STATUS_PREY)
71 GameRules_scoring_add(it, SCORE, 1); // reward survivors who make it to the end of the round time limit
72 if(it.survival_status == SURV_STATUS_PREY)
73 GameRules_scoring_add(it, SURV_SURVIVALS, 1);
74 else if(it.survival_status == SURV_STATUS_HUNTER)
75 GameRules_scoring_add(it, SURV_HUNTS, 1);
80 float Surv_CheckWinner()
82 if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
84 // if the match times out, survivors win too!
85 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_SURVIVAL_SURVIVOR_WIN);
86 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SURVIVAL_SURVIVOR_WIN);
93 Surv_UpdateScores(true);
95 allowed_to_spawn = false;
97 round_handler_Init(5, autocvar_g_survival_warmup, autocvar_g_survival_round_timelimit);
101 int survivor_count = 0, hunter_count = 0;
102 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
104 if(it.survival_status == SURV_STATUS_PREY)
106 else if(it.survival_status == SURV_STATUS_HUNTER)
109 if(survivor_count > 0 && hunter_count > 0)
114 if(hunter_count > 0) // hunters win
116 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_SURVIVAL_HUNTER_WIN);
117 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SURVIVAL_HUNTER_WIN);
119 else if(survivor_count > 0) // survivors win
121 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_SURVIVAL_SURVIVOR_WIN);
122 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SURVIVAL_SURVIVOR_WIN);
126 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED);
127 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED);
130 Surv_UpdateScores(false);
132 allowed_to_spawn = false;
134 round_handler_Init(5, autocvar_g_survival_warmup, autocvar_g_survival_round_timelimit);
145 void Surv_RoundStart()
147 allowed_to_spawn = boolean(warmup_stage);
151 if(IS_PLAYER(it) && !IS_DEAD(it))
154 it.survival_status = SURV_STATUS_PREY;
157 it.survival_status = 0; // this is mostly a safety check; if a client manages to somehow maintain a survival status, clear it before the round starts!
158 it.survival_validkills = 0;
160 int hunter_count = bound(1, ((autocvar_g_survival_hunter_count >= 1) ? autocvar_g_survival_hunter_count : floor(playercount * autocvar_g_survival_hunter_count)), playercount - 1); // 20%, but ensure at least 1 and less than total
161 int total_hunters = 0;
162 FOREACH_CLIENT_RANDOM(IS_PLAYER(it) && !IS_DEAD(it),
164 if(total_hunters >= hunter_count)
167 it.survival_status = SURV_STATUS_HUNTER;
169 SurvivalStatuses_Send();
171 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
173 if(it.survival_status == SURV_STATUS_PREY)
174 Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_SURVIVAL_SURVIVOR);
175 else if(it.survival_status == SURV_STATUS_HUNTER)
176 Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_SURVIVAL_HUNTER);
180 bool Surv_CheckPlayers()
182 static int prev_missing_players;
183 allowed_to_spawn = true;
185 FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
189 if (playercount >= 2)
191 if(prev_missing_players > 0)
192 Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS);
193 prev_missing_players = -1;
198 if(prev_missing_players > 0)
199 Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS);
200 prev_missing_players = -1;
203 // if we get here, only 1 player is missing
204 if(prev_missing_players != 1)
206 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_PLAYERS, 1);
207 prev_missing_players = 1;
212 bool surv_isEliminated(entity e)
214 if(INGAME_JOINED(e) && (IS_DEAD(e) || e.frags == FRAGS_PLAYER_OUT_OF_GAME))
216 if(INGAME_JOINING(e))
221 void surv_Initialize() // run at the start of a match, initiates game mode
223 GameRules_scoring(0, SFL_SORT_PRIO_PRIMARY, 0, {
224 field(SP_SURV_SURVIVALS, "survivals", 0);
225 field(SP_SURV_HUNTS, "hunts", SFL_SORT_PRIO_SECONDARY);
228 allowed_to_spawn = true;
229 round_handler_Spawn(Surv_CheckPlayers, Surv_CheckWinner, Surv_RoundStart);
230 round_handler_Init(5, autocvar_g_survival_warmup, autocvar_g_survival_round_timelimit);
231 EliminatedPlayers_Init(surv_isEliminated);
232 SurvivalStatuses_Init();
240 MUTATOR_HOOKFUNCTION(surv, ClientObituary)
242 // in survival, announcing a frag would tell everyone who the hunter is
243 entity frag_attacker = M_ARGV(1, entity);
244 entity frag_target = M_ARGV(2, entity);
245 if(IS_PLAYER(frag_attacker) && frag_attacker != frag_target)
247 float frag_deathtype = M_ARGV(3, float);
248 entity wep_ent = M_ARGV(4, entity);
249 // "team" kill, a point is awarded to the player by default so we must take it away plus an extra one
250 // unless the player is going to be punished for suicide, in which case just remove one
251 if(frag_attacker.survival_status == frag_target.survival_status)
252 GiveFrags(frag_attacker, frag_target, ((autocvar_g_survival_punish_teamkill) ? -1 : -2), frag_deathtype, wep_ent.weaponentity_fld);
255 if(frag_attacker.survival_status == SURV_STATUS_HUNTER)
256 M_ARGV(5, bool) = true; // anonymous attacker
259 MUTATOR_HOOKFUNCTION(surv, PlayerPreThink)
261 entity player = M_ARGV(0, entity);
263 if(IS_PLAYER(player) || INGAME(player))
265 // update the scoreboard colour display to out the real killer at the end of the round
266 // running this every frame to avoid cheats
267 int plcolor = SURV_COLOR_PREY;
268 if(player.survival_status == SURV_STATUS_HUNTER && game_stopped)
269 plcolor = SURV_COLOR_HUNTER;
270 setcolor(player, plcolor);
274 MUTATOR_HOOKFUNCTION(surv, PlayerSpawn)
276 entity player = M_ARGV(0, entity);
278 player.survival_status = 0;
279 player.survival_validkills = 0;
280 INGAME_STATUS_SET(player, INGAME_STATUS_JOINED);
282 eliminatedPlayers.SendFlags |= 1;
285 MUTATOR_HOOKFUNCTION(surv, ForbidSpawn)
287 entity player = M_ARGV(0, entity);
289 // spectators / observers that weren't playing can join; they are
290 // immediately forced to observe in the PutClientInServer hook
291 // this way they are put in a team and can play in the next round
292 if (!allowed_to_spawn && INGAME(player))
297 MUTATOR_HOOKFUNCTION(surv, PutClientInServer)
299 entity player = M_ARGV(0, entity);
301 if (!allowed_to_spawn && IS_PLAYER(player)) // this is true even when player is trying to join
303 TRANSMUTE(Observer, player);
304 if (CS(player).jointime != time && !INGAME(player)) // not when connecting
306 INGAME_STATUS_SET(player, INGAME_STATUS_JOINING);
307 Send_Notification(NOTIF_ONE_ONLY, player, MSG_INFO, INFO_CA_JOIN_LATE);
312 eliminatedPlayers.SendFlags |= 1;
315 MUTATOR_HOOKFUNCTION(surv, reset_map_players)
317 FOREACH_CLIENT(true, {
318 CS(it).killcount = 0;
319 it.survival_status = 0;
320 if (INGAME(it) || IS_BOT_CLIENT(it))
322 TRANSMUTE(Player, it);
323 INGAME_STATUS_SET(it, INGAME_STATUS_JOINED);
324 PutClientInServer(it);
327 bot_relinkplayerlist();
328 SurvivalStatuses_Send();
332 MUTATOR_HOOKFUNCTION(surv, reset_map_global)
334 allowed_to_spawn = true;
338 entity surv_LastPlayerForTeam(entity this)
340 entity last_pl = NULL;
341 FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
342 if (!IS_DEAD(it) && this.survival_status == it.survival_status)
353 void surv_LastPlayerForTeam_Notify(entity this)
355 if (!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted())
357 entity pl = surv_LastPlayerForTeam(this);
359 Send_Notification(NOTIF_ONE_ONLY, pl, MSG_CENTER, CENTER_ALONE);
363 MUTATOR_HOOKFUNCTION(surv, PlayerDies)
365 entity frag_attacker = M_ARGV(1, entity);
366 entity frag_target = M_ARGV(2, entity);
367 float frag_deathtype = M_ARGV(3, float);
369 surv_LastPlayerForTeam_Notify(frag_target);
370 if (!allowed_to_spawn)
372 frag_target.respawn_flags = RESPAWN_SILENT;
373 // prevent unwanted sudden rejoin as spectator and movement of spectator camera
374 frag_target.respawn_time = time + 2;
376 frag_target.respawn_flags |= RESPAWN_FORCE;
379 eliminatedPlayers.SendFlags |= 1;
380 if (IS_BOT_CLIENT(frag_target))
381 bot_clearqueue(frag_target);
384 // killed an ally! punishment is death
385 if(autocvar_g_survival_punish_teamkill && frag_attacker != frag_target && IS_PLAYER(frag_attacker) && IS_PLAYER(frag_target) && frag_attacker.survival_status == frag_target.survival_status && !ITEM_DAMAGE_NEEDKILL(frag_deathtype))
386 if(!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted()) // don't autokill if the round hasn't
387 Damage(frag_attacker, frag_attacker, frag_attacker, 100000, DEATH_MIRRORDAMAGE.m_id, DMG_NOWEP, frag_attacker.origin, '0 0 0');
391 MUTATOR_HOOKFUNCTION(surv, ClientDisconnect)
393 entity player = M_ARGV(0, entity);
395 if (IS_PLAYER(player) && !IS_DEAD(player))
396 surv_LastPlayerForTeam_Notify(player);
400 MUTATOR_HOOKFUNCTION(surv, MakePlayerObserver)
402 entity player = M_ARGV(0, entity);
403 bool is_forced = M_ARGV(1, bool);
404 if (is_forced && INGAME(player))
405 INGAME_STATUS_CLEAR(player);
407 if (IS_PLAYER(player) && !IS_DEAD(player))
408 surv_LastPlayerForTeam_Notify(player);
409 if (player.killindicator_teamchange == -2) // player wants to spectate
410 INGAME_STATUS_CLEAR(player);
412 player.frags = FRAGS_PLAYER_OUT_OF_GAME;
414 eliminatedPlayers.SendFlags |= 1;
417 player.survival_validkills = 0;
418 player.survival_status = 0;
419 return false; // allow team reset
421 return true; // prevent team reset
424 MUTATOR_HOOKFUNCTION(surv, Scores_CountFragsRemaining)
426 // announce remaining frags?
430 MUTATOR_HOOKFUNCTION(surv, GiveFragsForKill, CBC_ORDER_FIRST)
432 entity frag_attacker = M_ARGV(0, entity);
433 if(!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted())
434 frag_attacker.survival_validkills += M_ARGV(2, float);
435 M_ARGV(2, float) = 0; // score will be given to the winner when the round ends
439 MUTATOR_HOOKFUNCTION(surv, AddPlayerScore)
441 entity scorefield = M_ARGV(0, entity);
442 if(scorefield == SP_KILLS || scorefield == SP_DEATHS || scorefield == SP_SUICIDES || scorefield == SP_DMG || scorefield == SP_DMGTAKEN)
443 M_ARGV(1, float) = 0; // don't report that the player has killed or been killed, that would out them as a hunter!
446 MUTATOR_HOOKFUNCTION(surv, CalculateRespawnTime)
448 // no respawn calculations needed, player is forced to spectate anyway
452 MUTATOR_HOOKFUNCTION(surv, Bot_FixCount, CBC_ORDER_EXCLUSIVE)
454 FOREACH_CLIENT(IS_REAL_CLIENT(it), {
455 if (IS_PLAYER(it) || INGAME_JOINED(it))
462 MUTATOR_HOOKFUNCTION(surv, ClientCommand_Spectate)
464 entity player = M_ARGV(0, entity);
468 // they're going to spec, we can do other checks
469 if (autocvar_sv_spectate && (IS_SPEC(player) || IS_OBSERVER(player)))
470 Send_Notification(NOTIF_ONE_ONLY, player, MSG_INFO, INFO_CA_LEAVE);
471 return MUT_SPECCMD_FORCE;
474 return MUT_SPECCMD_CONTINUE;
477 MUTATOR_HOOKFUNCTION(surv, BotShouldAttack)
479 entity bot = M_ARGV(0, entity);
480 entity targ = M_ARGV(1, entity);
482 if(targ.survival_status == bot.survival_status)