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