]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/survival/sv_survival.qc
85e40e8175f9f3ef912530e204b203cd03c73f05
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / gamemodes / gamemode / survival / sv_survival.qc
1 #include "sv_survival.qh"
2
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;
8
9 void nades_Clear(entity player);
10
11 void Surv_UpdateScores(bool timed_out)
12 {
13         // give players their hard-earned kills now that the round is over
14         FOREACH_CLIENT(true,
15         {
16                 it.totalfrags += it.survival_validkills;
17                 if(it.survival_validkills)
18                         GameRules_scoring_add(it, SCORE, it.survival_validkills);
19                 it.survival_validkills = 0;
20                 // player survived the round
21                 if(IS_PLAYER(it) && !IS_DEAD(it))
22                 {
23                         if(autocvar_g_survival_reward_survival && timed_out && it.survival_status == SV_STATUS_PREY)
24                                 GameRules_scoring_add(it, SCORE, 1); // reward survivors who make it to the end of the round time limit
25                         if(it.survival_status == SV_STATUS_PREY)
26                                 GameRules_scoring_add(it, SV_SURVIVALS, 1);
27                         else if(it.survival_status == SV_STATUS_HUNTER)
28                                 GameRules_scoring_add(it, SV_HUNTS, 1);
29                 }
30         });
31 }
32
33 float Surv_CheckWinner()
34 {
35         if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
36         {
37                 // if the match times out, survivors win too!
38                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_SURVIVAL_SURVIVOR_WIN);
39                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SURVIVAL_SURVIVOR_WIN);
40                 FOREACH_CLIENT(true,
41                 {
42                         if(IS_PLAYER(it))
43                                 nades_Clear(it);
44                 });
45
46                 Surv_UpdateScores(true);
47
48                 allowed_to_spawn = false;
49                 game_stopped = true;
50                 round_handler_Init(5, autocvar_g_survival_warmup, autocvar_g_survival_round_timelimit);
51                 return 1;
52         }
53
54         int survivor_count = 0, hunter_count = 0;
55         FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
56         {
57                 if(it.survival_status == SV_STATUS_PREY)
58                         survivor_count++;
59                 else if(it.survival_status == SV_STATUS_HUNTER)
60                         hunter_count++;
61         });
62         if(survivor_count > 0 && hunter_count > 0)
63         {
64                 return 0;
65         }
66
67         if(hunter_count > 0) // hunters win
68         {
69                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_SURVIVAL_HUNTER_WIN);
70                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SURVIVAL_HUNTER_WIN);
71         }
72         else if(survivor_count > 0) // survivors win
73         {
74                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_SURVIVAL_SURVIVOR_WIN);
75                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SURVIVAL_SURVIVOR_WIN);
76         }
77         else
78         {
79                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED);
80                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED);
81         }
82
83         Surv_UpdateScores(false);
84
85         allowed_to_spawn = false;
86         game_stopped = true;
87         round_handler_Init(5, autocvar_g_survival_warmup, autocvar_g_survival_round_timelimit);
88
89         FOREACH_CLIENT(true,
90         {
91                 if(IS_PLAYER(it))
92                         nades_Clear(it);
93         });
94
95         return 1;
96 }
97
98 void Surv_RoundStart()
99 {
100         allowed_to_spawn = boolean(warmup_stage);
101         int playercount = 0;
102         FOREACH_CLIENT(true,
103         {
104                 if(IS_PLAYER(it) && !IS_DEAD(it))
105                 {
106                         ++playercount;
107                         it.survival_status = SV_STATUS_PREY;
108                 }
109                 else
110                         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!
111                 it.survival_validkills = 0;
112         });
113         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
114         int total_hunters = 0;
115         FOREACH_CLIENT_RANDOM(IS_PLAYER(it) && !IS_DEAD(it),
116         {
117                 if(total_hunters >= hunter_count)
118                         break;
119                 total_hunters++;
120                 it.survival_status = SV_STATUS_HUNTER;
121         });
122
123         FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
124         {
125                 if(it.survival_status == SV_STATUS_PREY)
126                         Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_SURVIVAL_SURVIVOR);
127                 else if(it.survival_status == SV_STATUS_HUNTER)
128                         Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_SURVIVAL_HUNTER);
129         });
130 }
131
132 bool Surv_CheckPlayers()
133 {
134         static int prev_missing_players;
135         allowed_to_spawn = true;
136         int playercount = 0;
137         FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
138         {
139                 ++playercount;
140         });
141         if (playercount >= 2)
142         {
143                 if(prev_missing_players > 0)
144                         Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS);
145                 prev_missing_players = -1;
146                 return true;
147         }
148         if(playercount == 0)
149         {
150                 if(prev_missing_players > 0)
151                         Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS);
152                 prev_missing_players = -1;
153                 return false;
154         }
155         // if we get here, only 1 player is missing
156         if(prev_missing_players != 1)
157         {
158                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_PLAYERS, 1);
159                 prev_missing_players = 1;
160         }
161         return false;
162 }
163
164 bool surv_isEliminated(entity e)
165 {
166         if(INGAME_JOINED(e) && (IS_DEAD(e) || e.frags == FRAGS_PLAYER_OUT_OF_GAME))
167                 return true;
168         if(INGAME_JOINING(e))
169                 return true;
170         return false;
171 }
172
173 void surv_Initialize() // run at the start of a match, initiates game mode
174 {
175         GameRules_scoring(0, SFL_SORT_PRIO_PRIMARY, 0, {
176                 field(SP_SV_SURVIVALS, "survivals", 0);
177                 field(SP_SV_HUNTS, "hunts", SFL_SORT_PRIO_SECONDARY);
178         });
179
180         allowed_to_spawn = true;
181         round_handler_Spawn(Surv_CheckPlayers, Surv_CheckWinner, Surv_RoundStart);
182         round_handler_Init(5, autocvar_g_survival_warmup, autocvar_g_survival_round_timelimit);
183         EliminatedPlayers_Init(surv_isEliminated);
184 }
185
186
187 // ==============
188 // Hook Functions
189 // ==============
190
191 MUTATOR_HOOKFUNCTION(sv, ClientObituary)
192 {
193         // in survival, announcing a frag would tell everyone who the hunter is
194         entity frag_attacker = M_ARGV(1, entity);
195         entity frag_target = M_ARGV(2, entity);
196         if(IS_PLAYER(frag_attacker) && frag_attacker != frag_target)
197         {
198                 float frag_deathtype = M_ARGV(3, float);
199                 entity wep_ent = M_ARGV(4, entity);
200                 // "team" kill, a point is awarded to the player by default so we must take it away plus an extra one
201                 // unless the player is going to be punished for suicide, in which case just remove one
202                 if(frag_attacker.survival_status == frag_target.survival_status)
203                         GiveFrags(frag_attacker, frag_target, ((autocvar_g_survival_punish_teamkill) ? -1 : -2), frag_deathtype, wep_ent.weaponentity_fld);
204         }
205
206         if(frag_attacker.survival_status == SV_STATUS_HUNTER)
207                 M_ARGV(5, bool) = true; // anonymous attacker
208 }
209
210 MUTATOR_HOOKFUNCTION(sv, PlayerPreThink)
211 {
212         entity player = M_ARGV(0, entity);
213
214         if(IS_PLAYER(player) || INGAME_JOINED(player))
215         {
216                 // update the scoreboard colour display to out the real killer at the end of the round
217                 // running this every frame to avoid cheats
218                 int plcolor = SV_COLOR_PREY;
219                 if(player.survival_status == SV_STATUS_HUNTER && game_stopped)
220                         plcolor = SV_COLOR_HUNTER;
221                 setcolor(player, plcolor);
222         }
223 }
224
225 MUTATOR_HOOKFUNCTION(sv, PlayerSpawn)
226 {
227         entity player = M_ARGV(0, entity);
228
229         player.survival_status = 0;
230         player.survival_validkills = 0;
231         INGAME_STATUS_SET(player, INGAME_STATUS_JOINED);
232         if (!warmup_stage)
233                 eliminatedPlayers.SendFlags |= 1;
234 }
235
236 MUTATOR_HOOKFUNCTION(sv, ForbidSpawn)
237 {
238         entity player = M_ARGV(0, entity);
239
240         // spectators / observers that weren't playing can join; they are
241         // immediately forced to observe in the PutClientInServer hook
242         // this way they are put in a team and can play in the next round
243         if (!allowed_to_spawn && INGAME(player))
244                 return true;
245         return false;
246 }
247
248 MUTATOR_HOOKFUNCTION(sv, PutClientInServer)
249 {
250         entity player = M_ARGV(0, entity);
251
252         if (!allowed_to_spawn && IS_PLAYER(player)) // this is true even when player is trying to join
253         {
254                 TRANSMUTE(Observer, player);
255                 if (CS(player).jointime != time && !INGAME(player)) // not when connecting
256                 {
257                         INGAME_STATUS_SET(player, INGAME_STATUS_JOINING);
258                         Send_Notification(NOTIF_ONE_ONLY, player, MSG_INFO, INFO_CA_JOIN_LATE);
259                 }
260         }
261 }
262
263 MUTATOR_HOOKFUNCTION(sv, reset_map_players)
264 {
265         FOREACH_CLIENT(true, {
266                 CS(it).killcount = 0;
267                 it.survival_status = 0;
268                 if (INGAME(it) || IS_BOT_CLIENT(it))
269                 {
270                         TRANSMUTE(Player, it);
271                         INGAME_STATUS_SET(it, INGAME_STATUS_JOINED);
272                         PutClientInServer(it);
273                 }
274         });
275         bot_relinkplayerlist();
276         return true;
277 }
278
279 MUTATOR_HOOKFUNCTION(sv, reset_map_global)
280 {
281         allowed_to_spawn = true;
282         return true;
283 }
284
285 entity surv_LastPlayerForTeam(entity this)
286 {
287         entity last_pl = NULL;
288         FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
289                 if (!IS_DEAD(it) && this.survival_status == it.survival_status)
290                 {
291                         if (!last_pl)
292                                 last_pl = it;
293                         else
294                                 return NULL;
295                 }
296         });
297         return last_pl;
298 }
299
300 void surv_LastPlayerForTeam_Notify(entity this)
301 {
302         if (!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted())
303         {
304                 entity pl = surv_LastPlayerForTeam(this);
305                 if (pl)
306                         Send_Notification(NOTIF_ONE_ONLY, pl, MSG_CENTER, CENTER_ALONE);
307         }
308 }
309
310 MUTATOR_HOOKFUNCTION(sv, PlayerDies)
311 {
312         entity frag_attacker = M_ARGV(1, entity);
313         entity frag_target = M_ARGV(2, entity);
314         float frag_deathtype = M_ARGV(3, float);
315
316         surv_LastPlayerForTeam_Notify(frag_target);
317         if (!allowed_to_spawn)
318         {
319                 frag_target.respawn_flags = RESPAWN_SILENT;
320                 // prevent unwanted sudden rejoin as spectator and movement of spectator camera
321                 frag_target.respawn_time = time + 2;
322         }
323         frag_target.respawn_flags |= RESPAWN_FORCE;
324         if (!warmup_stage)
325         {
326                 eliminatedPlayers.SendFlags |= 1;
327                 if (IS_BOT_CLIENT(frag_target))
328                         bot_clearqueue(frag_target);
329         }
330
331         // killed an ally! punishment is death
332         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))
333         if(!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted()) // don't autokill if the round hasn't
334                 Damage(frag_attacker, frag_attacker, frag_attacker, 100000, DEATH_MIRRORDAMAGE.m_id, DMG_NOWEP, frag_attacker.origin, '0 0 0');
335         return true;
336 }
337
338 MUTATOR_HOOKFUNCTION(sv, ClientDisconnect)
339 {
340         entity player = M_ARGV(0, entity);
341
342         if (IS_PLAYER(player) && !IS_DEAD(player))
343                 surv_LastPlayerForTeam_Notify(player);
344         return true;
345 }
346
347 MUTATOR_HOOKFUNCTION(sv, MakePlayerObserver)
348 {
349         entity player = M_ARGV(0, entity);
350         bool is_forced = M_ARGV(1, bool);
351         if (is_forced && INGAME(player))
352                 INGAME_STATUS_CLEAR(player);
353
354         if (IS_PLAYER(player) && !IS_DEAD(player))
355                 surv_LastPlayerForTeam_Notify(player);
356         if (player.killindicator_teamchange == -2) // player wants to spectate
357                 INGAME_STATUS_CLEAR(player);
358         if (INGAME(player))
359                 player.frags = FRAGS_PLAYER_OUT_OF_GAME;
360         if (!warmup_stage)
361                 eliminatedPlayers.SendFlags |= 1;
362         if (!INGAME(player))
363         {
364                 player.survival_validkills = 0;
365                 player.survival_status = 0;
366                 return false;  // allow team reset
367         }
368         return true;  // prevent team reset
369 }
370
371 MUTATOR_HOOKFUNCTION(sv, Scores_CountFragsRemaining)
372 {
373         // announce remaining frags?
374         return true;
375 }
376
377 MUTATOR_HOOKFUNCTION(sv, GiveFragsForKill, CBC_ORDER_FIRST)
378 {
379         entity frag_attacker = M_ARGV(0, entity);
380         if(!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted())
381                 frag_attacker.survival_validkills += M_ARGV(2, float);
382         M_ARGV(2, float) = 0; // score will be given to the winner when the round ends
383         return true;
384 }
385
386 MUTATOR_HOOKFUNCTION(sv, AddPlayerScore)
387 {
388         entity scorefield = M_ARGV(0, entity);
389         if(scorefield == SP_KILLS || scorefield == SP_DEATHS || scorefield == SP_SUICIDES || scorefield == SP_DMG || scorefield == SP_DMGTAKEN)
390                 M_ARGV(1, float) = 0; // don't report that the player has killed or been killed, that would out them as a hunter!
391 }
392
393 MUTATOR_HOOKFUNCTION(sv, CalculateRespawnTime)
394 {
395         // no respawn calculations needed, player is forced to spectate anyway
396         return true;
397 }
398
399 MUTATOR_HOOKFUNCTION(sv, Bot_FixCount, CBC_ORDER_EXCLUSIVE)
400 {
401         FOREACH_CLIENT(IS_REAL_CLIENT(it), {
402                 if (IS_PLAYER(it) || INGAME_JOINED(it))
403                         ++M_ARGV(0, int);
404                 ++M_ARGV(1, int);
405         });
406         return true;
407 }
408
409 MUTATOR_HOOKFUNCTION(sv, ClientCommand_Spectate)
410 {
411         entity player = M_ARGV(0, entity);
412
413         if (INGAME(player))
414         {
415                 // they're going to spec, we can do other checks
416                 if (autocvar_sv_spectate && (IS_SPEC(player) || IS_OBSERVER(player)))
417                         Send_Notification(NOTIF_ONE_ONLY, player, MSG_INFO, INFO_CA_LEAVE);
418                 return MUT_SPECCMD_FORCE;
419         }
420
421         return MUT_SPECCMD_CONTINUE;
422 }
423
424 MUTATOR_HOOKFUNCTION(sv, BotShouldAttack)
425 {
426         entity bot = M_ARGV(0, entity);
427         entity targ = M_ARGV(1, entity);
428
429         if(targ.survival_status == bot.survival_status)
430                 return true;
431 }