]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/survival/sv_survival.qc
Merge branch 'terencehill/survival_fixes' into 'master'
[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 entity survivalStatuses;
12 void SurvivalStatuses_Init();
13
14 void SurvivalStatuses_Send()
15 {
16         // SendFlags can be set to anything != 0, SurvivalStatuses_SendEntity won't use its value
17         survivalStatuses.SendFlags = 1;
18 }
19
20 bool SurvivalStatuses_SendEntity(entity this, entity dest, float sendflags)
21 {
22         Stream out = MSG_ENTITY;
23         WriteHeader(out, ENT_CLIENT_SURVIVALSTATUSES);
24
25         sendflags = BIT(0) | BIT(1);
26
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)
30                 sendflags &= !BIT(0);
31
32         serialize(byte, out, sendflags);
33         if (sendflags & BIT(0)) {
34                 for (int i = 1; i <= maxclients; i += 8) {
35                         int f = 0;
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);
39                                 if (is_hunter)
40                                         f |= BIT(b);
41                         }
42                         serialize(byte, out, f);
43                 }
44         }
45         return true;
46 }
47
48 void SurvivalStatuses_Init()
49 {
50         if(survivalStatuses)
51         {
52                 backtrace("Can't spawn survivalStatuses again!");
53                 return;
54         }
55         Net_LinkEntity(survivalStatuses = new_pure(survivalStatuses), false, 0, SurvivalStatuses_SendEntity);
56 }
57
58 void Surv_UpdateScores(bool timed_out)
59 {
60         // give players their hard-earned kills now that the round is over
61         FOREACH_CLIENT(true,
62         {
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))
69                 {
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);
76                 }
77         });
78 }
79
80 float Surv_CheckWinner()
81 {
82         if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
83         {
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);
87                 FOREACH_CLIENT(true,
88                 {
89                         if(IS_PLAYER(it))
90                                 nades_Clear(it);
91                 });
92
93                 Surv_UpdateScores(true);
94
95                 allowed_to_spawn = false;
96                 game_stopped = true;
97                 round_handler_Init(5, autocvar_g_survival_warmup, autocvar_g_survival_round_timelimit);
98                 return 1;
99         }
100
101         int survivor_count = 0, hunter_count = 0;
102         FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
103         {
104                 if(it.survival_status == SURV_STATUS_PREY)
105                         survivor_count++;
106                 else if(it.survival_status == SURV_STATUS_HUNTER)
107                         hunter_count++;
108         });
109         if(survivor_count > 0 && hunter_count > 0)
110         {
111                 return 0;
112         }
113
114         if(hunter_count > 0) // hunters win
115         {
116                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_SURVIVAL_HUNTER_WIN);
117                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SURVIVAL_HUNTER_WIN);
118         }
119         else if(survivor_count > 0) // survivors win
120         {
121                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_SURVIVAL_SURVIVOR_WIN);
122                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_SURVIVAL_SURVIVOR_WIN);
123         }
124         else
125         {
126                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED);
127                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED);
128         }
129
130         Surv_UpdateScores(false);
131
132         allowed_to_spawn = false;
133         game_stopped = true;
134         round_handler_Init(5, autocvar_g_survival_warmup, autocvar_g_survival_round_timelimit);
135
136         FOREACH_CLIENT(true,
137         {
138                 if(IS_PLAYER(it))
139                         nades_Clear(it);
140         });
141
142         return 1;
143 }
144
145 void Surv_RoundStart()
146 {
147         allowed_to_spawn = boolean(warmup_stage);
148         int playercount = 0;
149         FOREACH_CLIENT(true,
150         {
151                 if(IS_PLAYER(it) && !IS_DEAD(it))
152                 {
153                         ++playercount;
154                         it.survival_status = SURV_STATUS_PREY;
155                 }
156                 else
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;
159         });
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),
163         {
164                 if(total_hunters >= hunter_count)
165                         break;
166                 total_hunters++;
167                 it.survival_status = SURV_STATUS_HUNTER;
168         });
169         SurvivalStatuses_Send();
170
171         FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
172         {
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);
177         });
178 }
179
180 bool Surv_CheckPlayers()
181 {
182         static int prev_missing_players;
183         allowed_to_spawn = true;
184         int playercount = 0;
185         FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
186         {
187                 ++playercount;
188         });
189         if (playercount >= 2)
190         {
191                 if(prev_missing_players > 0)
192                         Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS);
193                 prev_missing_players = -1;
194                 return true;
195         }
196         if(playercount == 0)
197         {
198                 if(prev_missing_players > 0)
199                         Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS);
200                 prev_missing_players = -1;
201                 return false;
202         }
203         // if we get here, only 1 player is missing
204         if(prev_missing_players != 1)
205         {
206                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_PLAYERS, 1);
207                 prev_missing_players = 1;
208         }
209         return false;
210 }
211
212 bool surv_isEliminated(entity e)
213 {
214         if(INGAME_JOINED(e) && (IS_DEAD(e) || e.frags == FRAGS_PLAYER_OUT_OF_GAME))
215                 return true;
216         if(INGAME_JOINING(e))
217                 return true;
218         return false;
219 }
220
221 void surv_Initialize() // run at the start of a match, initiates game mode
222 {
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);
226         });
227
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();
233 }
234
235
236 // ==============
237 // Hook Functions
238 // ==============
239
240 MUTATOR_HOOKFUNCTION(surv, ClientObituary)
241 {
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)
246         {
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);
253         }
254
255         if(frag_attacker.survival_status == SURV_STATUS_HUNTER)
256                 M_ARGV(5, bool) = true; // anonymous attacker
257 }
258
259 MUTATOR_HOOKFUNCTION(surv, PlayerPreThink)
260 {
261         entity player = M_ARGV(0, entity);
262
263         if(IS_PLAYER(player) || INGAME(player))
264         {
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);
271         }
272 }
273
274 MUTATOR_HOOKFUNCTION(surv, PlayerSpawn)
275 {
276         entity player = M_ARGV(0, entity);
277
278         player.survival_status = 0;
279         player.survival_validkills = 0;
280         INGAME_STATUS_SET(player, INGAME_STATUS_JOINED);
281         if (!warmup_stage)
282                 eliminatedPlayers.SendFlags |= 1;
283 }
284
285 MUTATOR_HOOKFUNCTION(surv, ForbidSpawn)
286 {
287         entity player = M_ARGV(0, entity);
288
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))
293                 return true;
294         return false;
295 }
296
297 MUTATOR_HOOKFUNCTION(surv, PutClientInServer)
298 {
299         entity player = M_ARGV(0, entity);
300
301         if (!allowed_to_spawn && IS_PLAYER(player)) // this is true even when player is trying to join
302         {
303                 TRANSMUTE(Observer, player);
304                 if (CS(player).jointime != time && !INGAME(player)) // not when connecting
305                 {
306                         INGAME_STATUS_SET(player, INGAME_STATUS_JOINING);
307                         Send_Notification(NOTIF_ONE_ONLY, player, MSG_INFO, INFO_CA_JOIN_LATE);
308                 }
309         }
310
311         if (!warmup_stage)
312                 eliminatedPlayers.SendFlags |= 1;
313 }
314
315 MUTATOR_HOOKFUNCTION(surv, reset_map_players)
316 {
317         FOREACH_CLIENT(true, {
318                 CS(it).killcount = 0;
319                 it.survival_status = 0;
320                 if (INGAME(it) || IS_BOT_CLIENT(it))
321                 {
322                         TRANSMUTE(Player, it);
323                         INGAME_STATUS_SET(it, INGAME_STATUS_JOINED);
324                         PutClientInServer(it);
325                 }
326         });
327         bot_relinkplayerlist();
328         SurvivalStatuses_Send();
329         return true;
330 }
331
332 MUTATOR_HOOKFUNCTION(surv, reset_map_global)
333 {
334         allowed_to_spawn = true;
335         return true;
336 }
337
338 entity surv_LastPlayerForTeam(entity this)
339 {
340         entity last_pl = NULL;
341         FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
342                 if (!IS_DEAD(it) && this.survival_status == it.survival_status)
343                 {
344                         if (!last_pl)
345                                 last_pl = it;
346                         else
347                                 return NULL;
348                 }
349         });
350         return last_pl;
351 }
352
353 void surv_LastPlayerForTeam_Notify(entity this)
354 {
355         if (!warmup_stage && round_handler_IsActive() && round_handler_IsRoundStarted())
356         {
357                 entity pl = surv_LastPlayerForTeam(this);
358                 if (pl)
359                         Send_Notification(NOTIF_ONE_ONLY, pl, MSG_CENTER, CENTER_ALONE);
360         }
361 }
362
363 MUTATOR_HOOKFUNCTION(surv, PlayerDies)
364 {
365         entity frag_attacker = M_ARGV(1, entity);
366         entity frag_target = M_ARGV(2, entity);
367         float frag_deathtype = M_ARGV(3, float);
368
369         surv_LastPlayerForTeam_Notify(frag_target);
370         if (!allowed_to_spawn)
371         {
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;
375         }
376         frag_target.respawn_flags |= RESPAWN_FORCE;
377         if (!warmup_stage)
378         {
379                 eliminatedPlayers.SendFlags |= 1;
380                 if (IS_BOT_CLIENT(frag_target))
381                         bot_clearqueue(frag_target);
382         }
383
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');
388         return true;
389 }
390
391 MUTATOR_HOOKFUNCTION(surv, ClientDisconnect)
392 {
393         entity player = M_ARGV(0, entity);
394
395         if (IS_PLAYER(player) && !IS_DEAD(player))
396                 surv_LastPlayerForTeam_Notify(player);
397         return true;
398 }
399
400 MUTATOR_HOOKFUNCTION(surv, MakePlayerObserver)
401 {
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);
406
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);
411         if (INGAME(player))
412                 player.frags = FRAGS_PLAYER_OUT_OF_GAME;
413         if (!warmup_stage)
414                 eliminatedPlayers.SendFlags |= 1;
415         if (!INGAME(player))
416         {
417                 player.survival_validkills = 0;
418                 player.survival_status = 0;
419                 return false;  // allow team reset
420         }
421         return true;  // prevent team reset
422 }
423
424 MUTATOR_HOOKFUNCTION(surv, Scores_CountFragsRemaining)
425 {
426         // announce remaining frags?
427         return true;
428 }
429
430 MUTATOR_HOOKFUNCTION(surv, GiveFragsForKill, CBC_ORDER_FIRST)
431 {
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
436         return true;
437 }
438
439 MUTATOR_HOOKFUNCTION(surv, AddPlayerScore)
440 {
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!
444 }
445
446 MUTATOR_HOOKFUNCTION(surv, CalculateRespawnTime)
447 {
448         // no respawn calculations needed, player is forced to spectate anyway
449         return true;
450 }
451
452 MUTATOR_HOOKFUNCTION(surv, Bot_FixCount, CBC_ORDER_EXCLUSIVE)
453 {
454         FOREACH_CLIENT(IS_REAL_CLIENT(it), {
455                 if (IS_PLAYER(it) || INGAME_JOINED(it))
456                         ++M_ARGV(0, int);
457                 ++M_ARGV(1, int);
458         });
459         return true;
460 }
461
462 MUTATOR_HOOKFUNCTION(surv, ClientCommand_Spectate)
463 {
464         entity player = M_ARGV(0, entity);
465
466         if (INGAME(player))
467         {
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;
472         }
473
474         return MUT_SPECCMD_CONTINUE;
475 }
476
477 MUTATOR_HOOKFUNCTION(surv, BotShouldAttack)
478 {
479         entity bot = M_ARGV(0, entity);
480         entity targ = M_ARGV(1, entity);
481
482         if(targ.survival_status == bot.survival_status)
483                 return true;
484 }