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