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