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