LMS: fix all players declared winners in console
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / gamemodes / gamemode / lms / sv_lms.qc
1 #include "sv_lms.qh"
2
3 #include <common/mutators/mutator/instagib/items.qh>
4 #include <server/campaign.qh>
5 #include <server/command/_mod.qh>
6 #include <server/world.qh>
7 #include <server/items/items.qh>
8
9 int autocvar_g_lms_extra_lives;
10 float autocvar_g_lms_forfeit_min_match_time;
11 bool autocvar_g_lms_join_anytime;
12 int autocvar_g_lms_last_join;
13 bool autocvar_g_lms_items;
14 bool autocvar_g_lms_regenerate;
15 bool autocvar_g_lms_rot;
16
17 // main functions
18 int LMS_NewPlayerLives()
19 {
20         int fl = floor(autocvar_fraglimit);
21         if(fl == 0 || fl > 999)
22                 fl = 999;
23
24         // first player has left the game for dying too much? Nobody else can get in.
25         if(lms_lowest_lives < 1)
26                 return 0;
27
28         if(!autocvar_g_lms_join_anytime)
29                 if(lms_lowest_lives < fl - max(0, floor(autocvar_g_lms_last_join)))
30                         return 0;
31
32         return bound(1, lms_lowest_lives, fl);
33 }
34
35 void ClearWinners();
36
37 // LMS winning condition: game terminates if and only if there's at most one
38 // one player who's living lives. Top two scores being equal cancels the time
39 // limit.
40 int WinningCondition_LMS()
41 {
42         if (warmup_stage || time <= game_starttime)
43                 return WINNING_NO;
44
45         entity first_player = NULL;
46         int totalplayers = 0;
47         int totalplayed = 0;
48         FOREACH_CLIENT(true, {
49                 if (IS_PLAYER(it) && it.frags == FRAGS_PLAYER)
50                 {
51                         if (!totalplayers)
52                                 first_player = it;
53                         ++totalplayers;
54                 }
55                 else if (GameRules_scoring_add(it, LMS_RANK, 0))
56                         ++totalplayed;
57         });
58
59         if (totalplayers)
60         {
61                 if (totalplayers > 1)
62                 {
63                         // two or more active players - continue with the game
64
65                         if (autocvar_g_campaign && campaign_bots_may_start)
66                         {
67                                 FOREACH_CLIENT(IS_REAL_CLIENT(it), {
68                                         float pl_lives = GameRules_scoring_add(it, LMS_LIVES, 0);
69                                         if (!pl_lives)
70                                                 return WINNING_YES; // human player lost, game over
71                                         break;
72                                 });
73                         }
74                 }
75                 else
76                 {
77                         // exactly one player?
78
79                         ClearWinners();
80
81                         if (LMS_NewPlayerLives())
82                         {
83                                 if (totalplayed && game_starttime > 0 && time > game_starttime + autocvar_g_lms_forfeit_min_match_time) // give players time to join
84                                 {
85                                         GameRules_scoring_add(first_player, LMS_RANK, 1);
86                                         first_player.winning = 1;
87                                         return WINNING_YES;
88                                 }
89                                 // game still running (that is, nobody got removed from the game by a frag yet)? then continue
90                                 return WINNING_NO;
91                         }
92                         else
93                         {
94                                 // a winner!
95                                 // and assign him his first place
96                                 GameRules_scoring_add(first_player, LMS_RANK, 1);
97                                 first_player.winning = 1;
98                                 return WINNING_YES;
99                         }
100                 }
101         }
102         else
103         {
104                 // nobody is playing at all...
105                 if (LMS_NewPlayerLives())
106                 {
107                         if (totalplayed && game_starttime > 0 && time > game_starttime + autocvar_g_lms_forfeit_min_match_time) // give players time to join
108                         {
109                                 ClearWinners();
110                                 return WINNING_YES;
111                         }
112                         // wait for players...
113                 }
114                 else
115                 {
116                         // SNAFU (maybe a draw game?)
117                         ClearWinners();
118                         LOG_TRACE("No players, ending game.");
119                         return WINNING_YES;
120                 }
121         }
122
123         // When we get here, we have at least two players who are actually LIVING,
124         // now check if the top two players have equal score.
125         WinningConditionHelper(NULL);
126
127         ClearWinners();
128         if(WinningConditionHelper_winner)
129                 WinningConditionHelper_winner.winning = true;
130         if(WinningConditionHelper_topscore == WinningConditionHelper_secondscore)
131                 return WINNING_NEVER;
132
133         // Top two have different scores? Way to go for our beloved TIMELIMIT!
134         return WINNING_NO;
135 }
136
137 // mutator hooks
138 MUTATOR_HOOKFUNCTION(lms, reset_map_global)
139 {
140         lms_lowest_lives = 999;
141 }
142
143 MUTATOR_HOOKFUNCTION(lms, reset_map_players)
144 {
145         FOREACH_CLIENT(true, {
146                 if (it.frags == FRAGS_PLAYER_OUT_OF_GAME)
147                 {
148                         // players who forfeited (rank >= 256) become spectators
149                         if (it.lms_spectate_warning == 2)
150                                 it.frags = FRAGS_SPECTATOR;
151                         else
152                                 it.frags = FRAGS_PLAYER;
153                 }
154
155                 CS(it).killcount = 0;
156                 INGAME_STATUS_CLEAR(it);
157                 it.lms_spectate_warning = 0;
158                 GameRules_scoring_add(it, LMS_RANK, -GameRules_scoring_add(it, LMS_RANK, 0));
159                 GameRules_scoring_add(it, LMS_LIVES, -GameRules_scoring_add(it, LMS_LIVES, 0));
160
161                 if (it.frags != FRAGS_PLAYER)
162                         continue;
163
164                 TRANSMUTE(Player, it);
165                 PutClientInServer(it);
166         });
167 }
168
169 // FIXME add support for sv_ready_restart_after_countdown
170 // that is find a way to respawn/reset players IN GAME without setting lives to 0
171 MUTATOR_HOOKFUNCTION(lms, ReadLevelCvars)
172 {
173         // incompatible
174         sv_ready_restart_after_countdown = 0;
175 }
176
177 // returns true if player is added to the game
178 bool lms_AddPlayer(entity player)
179 {
180         if (!INGAME(player))
181         {
182                 int lives = GameRules_scoring_add(player, LMS_LIVES, LMS_NewPlayerLives());
183                 if(lives <= 0)
184                         return false;
185                 if (time < game_starttime)
186                         INGAME_STATUS_SET(player, INGAME_STATUS_JOINED);
187                 else
188                         INGAME_STATUS_SET(player, INGAME_STATUS_JOINING); // this is just to delay setting health and armor that can't be done here
189         }
190         if (warmup_stage || time <= game_starttime)
191         {
192                 if(player.lms_spectate_warning)
193                 {
194                         player.lms_spectate_warning = 0;
195                         GameRules_scoring_add(player, LMS_RANK, -GameRules_scoring_add(player, LMS_RANK, 0));
196                         int lives = GameRules_scoring_add(player, LMS_LIVES, 0);
197                         if(lives <= 0)
198                                 GameRules_scoring_add(player, LMS_LIVES, LMS_NewPlayerLives());
199                 }
200         }
201         else
202         {
203                 if(GameRules_scoring_add(player, LMS_LIVES, 0) <= 0)
204                 {
205                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_LMS_NOLIVES);
206                         return false;
207                 }
208         }
209         return true;
210 }
211
212 MUTATOR_HOOKFUNCTION(lms, PutClientInServer)
213 {
214         entity player = M_ARGV(0, entity);
215         if (!warmup_stage && (IS_BOT_CLIENT(player) || CS(player).jointime != time))
216         {
217                 if (GameRules_scoring_add(player, LMS_RANK, 0) || !lms_AddPlayer(player))
218                         TRANSMUTE(Observer, player);
219         }
220 }
221
222 MUTATOR_HOOKFUNCTION(lms, PlayerSpawn)
223 {
224         entity player = M_ARGV(0, entity);
225
226         if (warmup_stage || time < game_starttime)
227                 return true;
228
229         if (INGAME_JOINING(player))
230         {
231                 // spawn player with the same amount of health / armor
232                 // as the least healthy player with the least number of lives
233                 int pl_lives = GameRules_scoring_add(player, LMS_LIVES, 0);
234                 float min_health = start_health;
235                 float min_armorvalue = start_armorvalue;
236                 FOREACH_CLIENT(it != player && IS_PLAYER(it) && !IS_DEAD(it) && GameRules_scoring_add(it, LMS_LIVES, 0) == pl_lives, {
237                         if (GetResource(it, RES_HEALTH) < min_health)
238                                 min_health = GetResource(it, RES_HEALTH);
239                         if (GetResource(it, RES_ARMOR) < min_armorvalue)
240                                 min_armorvalue = GetResource(it, RES_ARMOR);
241                 });
242                 if (min_health != start_health)
243                         SetResource(player, RES_HEALTH, max(1, min_health));
244                 if (min_armorvalue != start_armorvalue)
245                         SetResource(player, RES_ARMOR, min_armorvalue);
246                 INGAME_STATUS_SET(player, INGAME_STATUS_JOINED);
247         }
248 }
249
250 MUTATOR_HOOKFUNCTION(lms, ForbidSpawn)
251 {
252         entity player = M_ARGV(0, entity);
253
254         if (warmup_stage || lms_AddPlayer(player))
255                 return false;
256
257         return true;
258 }
259
260 MUTATOR_HOOKFUNCTION(lms, PlayerDies)
261 {
262         entity frag_target = M_ARGV(2, entity);
263
264         float tl = GameRules_scoring_add(frag_target, LMS_LIVES, 0);
265         if (tl <= 0)
266         {
267                 frag_target.respawn_flags = RESPAWN_SILENT;
268                 // prevent unwanted sudden rejoin as spectator and movement of spectator camera
269                 frag_target.respawn_time = time + 2;
270         }
271         frag_target.respawn_flags |= RESPAWN_FORCE;
272 }
273
274 void lms_RemovePlayer(entity player)
275 {
276         if (warmup_stage || time < game_starttime)
277                 return;
278
279         float player_rank = GameRules_scoring_add(player, LMS_RANK, 0);
280         if (!player_rank)
281         {
282                 if (player.lms_spectate_warning < 2)
283                 {
284                         player.frags = FRAGS_PLAYER_OUT_OF_GAME;
285                         int pl_cnt = 0;
286                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, {
287                                 pl_cnt++;
288                         });
289                         GameRules_scoring_add(player, LMS_RANK, pl_cnt + 1);
290                 }
291                 else if (INGAME(player))
292                 {
293                         int min_forfeiter_rank = 665; // different from 666
294                         FOREACH_CLIENT(it != player, {
295                                 // update rank of other players that were eliminated
296                                 if (it.frags == FRAGS_PLAYER_OUT_OF_GAME)
297                                 {
298                                         float it_rank = GameRules_scoring_add(it, LMS_RANK, 0);
299                                         if (it_rank > player_rank && it_rank <= 256)
300                                                 GameRules_scoring_add(it, LMS_RANK, -1);
301                                         if (it_rank > 256 && it_rank <= min_forfeiter_rank)
302                                                 min_forfeiter_rank = it_rank - 1;
303                                 }
304                                 else if (it.frags != FRAGS_SPECTATOR)
305                                 {
306                                         float tl = GameRules_scoring_add(it, LMS_LIVES, 0);
307                                         if(tl < lms_lowest_lives)
308                                                 lms_lowest_lives = tl;
309                                 }
310                         });
311                         GameRules_scoring_add(player, LMS_RANK, min_forfeiter_rank);
312                         if(!warmup_stage)
313                                 GameRules_scoring_add(player, LMS_LIVES, -GameRules_scoring_add(player, LMS_LIVES, 0));
314                         player.frags = FRAGS_PLAYER_OUT_OF_GAME;
315                         TRANSMUTE(Observer, player);
316                 }
317         }
318
319         if (CS(player).killcount != FRAGS_SPECTATOR && player.lms_spectate_warning < 3)
320         {
321                 if (GameRules_scoring_add(player, LMS_RANK, 0) > 0 && player.lms_spectate_warning < 2)
322                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_LMS_NOLIVES, player.netname);
323                 else
324                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_LMS_FORFEIT, player.netname);
325         }
326 }
327
328 MUTATOR_HOOKFUNCTION(lms, ClientDisconnect)
329 {
330         entity player = M_ARGV(0, entity);
331
332         // no further message other than the disconnect message
333         player.lms_spectate_warning = 3;
334
335         lms_RemovePlayer(player);
336         INGAME_STATUS_CLEAR(player);
337 }
338
339 MUTATOR_HOOKFUNCTION(lms, MakePlayerObserver)
340 {
341         entity player = M_ARGV(0, entity);
342         bool is_forced = M_ARGV(1, bool);
343
344         if (!IS_PLAYER(player))
345                 return true;
346
347         if (warmup_stage || time <= game_starttime)
348         {
349                 GameRules_scoring_add(player, LMS_LIVES, -GameRules_scoring_add(player, LMS_LIVES, 0));
350                 player.frags = FRAGS_SPECTATOR;
351                 TRANSMUTE(Observer, player);
352                 INGAME_STATUS_CLEAR(player);
353         }
354         else
355         {
356                 if (is_forced)
357                         player.lms_spectate_warning = 2;
358                 if (!GameRules_scoring_add(player, LMS_RANK, 0))
359                         lms_RemovePlayer(player);
360         }
361         return true;  // prevent team reset
362 }
363
364 MUTATOR_HOOKFUNCTION(lms, ClientConnect)
365 {
366         entity player = M_ARGV(0, entity);
367         player.frags = FRAGS_SPECTATOR;
368 }
369
370 MUTATOR_HOOKFUNCTION(lms, PlayerPreThink)
371 {
372         entity player = M_ARGV(0, entity);
373
374         if(player.deadflag == DEAD_DYING)
375                 player.deadflag = DEAD_RESPAWNING;
376 }
377
378 MUTATOR_HOOKFUNCTION(lms, PlayerRegen)
379 {
380         if(!autocvar_g_lms_regenerate)
381                 M_ARGV(2, float) = 0;
382         if(!autocvar_g_lms_rot)
383                 M_ARGV(3, float) = 0;
384         return (!autocvar_g_lms_regenerate && !autocvar_g_lms_rot);
385 }
386
387 MUTATOR_HOOKFUNCTION(lms, ForbidThrowCurrentWeapon)
388 {
389         // forbode!
390         return true;
391 }
392
393 MUTATOR_HOOKFUNCTION(lms, GiveFragsForKill)
394 {
395         entity frag_target = M_ARGV(1, entity);
396
397         if (!warmup_stage && time > game_starttime)
398         {
399                 // remove a life
400                 int tl = GameRules_scoring_add(frag_target, LMS_LIVES, -1);
401                 if(tl < lms_lowest_lives)
402                         lms_lowest_lives = tl;
403                 if(tl <= 0)
404                 {
405                         int pl_cnt = 0;
406                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, {
407                                 pl_cnt++;
408                         });
409                         frag_target.frags = FRAGS_PLAYER_OUT_OF_GAME;
410                         GameRules_scoring_add(frag_target, LMS_RANK, pl_cnt);
411                 }
412         }
413         M_ARGV(2, float) = 0; // frag score
414
415         return true;
416 }
417
418 MUTATOR_HOOKFUNCTION(lms, SetStartItems)
419 {
420         start_items &= ~(IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS);
421         if(!cvar("g_use_ammunition"))
422                 start_items |= IT_UNLIMITED_AMMO;
423
424         start_health       = warmup_start_health       = cvar("g_lms_start_health");
425         start_armorvalue   = warmup_start_armorvalue   = cvar("g_lms_start_armor");
426         start_ammo_shells  = warmup_start_ammo_shells  = cvar("g_lms_start_ammo_shells");
427         start_ammo_nails   = warmup_start_ammo_nails   = cvar("g_lms_start_ammo_nails");
428         start_ammo_rockets = warmup_start_ammo_rockets = cvar("g_lms_start_ammo_rockets");
429         start_ammo_cells   = warmup_start_ammo_cells   = cvar("g_lms_start_ammo_cells");
430         start_ammo_plasma  = warmup_start_ammo_plasma  = cvar("g_lms_start_ammo_plasma");
431         start_ammo_fuel    = warmup_start_ammo_fuel    = cvar("g_lms_start_ammo_fuel");
432 }
433
434 MUTATOR_HOOKFUNCTION(lms, ForbidPlayerScore_Clear)
435 {
436         // don't clear player score
437         return true;
438 }
439
440 MUTATOR_HOOKFUNCTION(lms, FilterItemDefinition)
441 {
442         if (autocvar_g_lms_items)
443                 return false;
444
445         entity definition = M_ARGV(0, entity);
446
447         if (autocvar_g_lms_extra_lives && definition == ITEM_ExtraLife)
448         {
449                 return false;
450         }
451         return (autocvar_g_pickup_items <= 0); // only allow items if explicitly enabled
452 }
453
454 void lms_extralife(entity this)
455 {
456         StartItem(this, ITEM_ExtraLife);
457 }
458
459 MUTATOR_HOOKFUNCTION(lms, OnEntityPreSpawn)
460 {
461         if (MUTATOR_RETURNVALUE) return false;
462         if (!autocvar_g_powerups) return false;
463         if (!autocvar_g_lms_extra_lives) return false;
464
465         entity ent = M_ARGV(0, entity);
466
467         // Can't use .itemdef here
468         if (ent.classname != "item_health_mega") return false;
469
470         entity e = spawn();
471         setthink(e, lms_extralife);
472
473         e.nextthink = time + 0.1;
474         e.spawnflags = ent.spawnflags;
475         e.noalign = ent.noalign;
476         setorigin(e, ent.origin);
477
478         return true;
479 }
480
481 MUTATOR_HOOKFUNCTION(lms, ItemTouch)
482 {
483         if(MUTATOR_RETURNVALUE) return false;
484
485         entity item = M_ARGV(0, entity);
486         entity toucher = M_ARGV(1, entity);
487
488         if(item.itemdef == ITEM_ExtraLife)
489         {
490                 Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_EXTRALIVES, autocvar_g_lms_extra_lives);
491                 GameRules_scoring_add(toucher, LMS_LIVES, autocvar_g_lms_extra_lives);
492                 return MUT_ITEMTOUCH_PICKUP;
493         }
494
495         return MUT_ITEMTOUCH_CONTINUE;
496 }
497
498 MUTATOR_HOOKFUNCTION(lms, Bot_FixCount, CBC_ORDER_EXCLUSIVE)
499 {
500         FOREACH_CLIENT(IS_REAL_CLIENT(it), {
501                 if (INGAME(it) && it.lms_spectate_warning < 2)
502                         ++M_ARGV(0, int); // activerealplayers
503                 ++M_ARGV(1, int); // realplayers
504         });
505
506         return true;
507 }
508
509 MUTATOR_HOOKFUNCTION(lms, ClientCommand_Spectate)
510 {
511         entity player = M_ARGV(0, entity);
512
513         if(warmup_stage || time < game_starttime || player.lms_spectate_warning)
514         {
515                 // for the forfeit message...
516                 player.lms_spectate_warning = 2;
517         }
518         else
519         {
520                 if(player.frags != FRAGS_SPECTATOR && player.frags != FRAGS_PLAYER_OUT_OF_GAME)
521                 {
522                         player.lms_spectate_warning = 1;
523                         sprint(player, "^1WARNING:^7 you can't rejoin this match after spectating. Use the same command again to spectate anyway.\n");
524                         Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_LMS_SPECWARN);
525                 }
526                 return MUT_SPECCMD_RETURN;
527         }
528         return MUT_SPECCMD_CONTINUE;
529 }
530
531 MUTATOR_HOOKFUNCTION(lms, CheckRules_World)
532 {
533         M_ARGV(0, float) = WinningCondition_LMS();
534         return true;
535 }
536
537 MUTATOR_HOOKFUNCTION(lms, SetWeaponArena)
538 {
539         if(M_ARGV(0, string) == "0" || M_ARGV(0, string) == "")
540                 M_ARGV(0, string) = autocvar_g_lms_weaponarena;
541 }
542
543 MUTATOR_HOOKFUNCTION(lms, AddPlayerScore)
544 {
545         if(game_stopped)
546         if(M_ARGV(0, entity) == SP_LMS_RANK) // score field
547                 return true; // allow writing to this field in intermission as it is needed for newly joining players
548 }
549
550 void lms_Initialize()
551 {
552         lms_lowest_lives = 999;
553 }