]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/lms/sv_lms.qc
Merge branch 'drjaska/whitelist_weaponarena' into 'master'
[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 int autocvar_g_lms_leader_lives_diff = 2;
16 float autocvar_g_lms_leader_minpercent = 0.5;
17 float autocvar_g_lms_leader_wp_interval = 25;
18 float autocvar_g_lms_leader_wp_interval_jitter = 10;
19 float autocvar_g_lms_leader_wp_time = 5;
20 float autocvar_g_lms_dynamic_respawn_delay = 1;
21 float autocvar_g_lms_dynamic_respawn_delay_base = 2;
22 float autocvar_g_lms_dynamic_respawn_delay_increase = 3;
23 float autocvar_g_lms_dynamic_respawn_delay_max = 20;
24 bool autocvar_g_lms_dynamic_vampire = 1;
25 float autocvar_g_lms_dynamic_vampire_factor_base = 0.1;
26 float autocvar_g_lms_dynamic_vampire_factor_increase = 0.1;
27 float autocvar_g_lms_dynamic_vampire_factor_max = 0.5;
28 int autocvar_g_lms_dynamic_vampire_min_lives_diff = 2;
29
30 .float lms_leader;
31 int lms_leaders;
32 float lms_visible_leaders_time;
33 bool lms_visible_leaders = true; // triggers lms_visible_leaders_time update in the first frame
34 bool lms_visible_leaders_prev;
35 bool autocvar_g_lms_rot;
36
37 // main functions
38 int LMS_NewPlayerLives()
39 {
40         int fl = floor(autocvar_fraglimit);
41         if(fl == 0 || fl > 999)
42                 fl = 999;
43
44         // first player has left the game for dying too much? Nobody else can get in.
45         if(lms_lowest_lives < 1)
46                 return 0;
47
48         if(!autocvar_g_lms_join_anytime)
49                 if(lms_lowest_lives < fl - max(0, floor(autocvar_g_lms_last_join)))
50                         return 0;
51
52         return bound(1, lms_lowest_lives, fl);
53 }
54
55 void ClearWinners();
56
57 // LMS winning condition: game terminates if and only if there's at most one
58 // one player who's living lives. Top two scores being equal cancels the time
59 // limit.
60 int WinningCondition_LMS()
61 {
62         if (warmup_stage || time <= game_starttime)
63                 return WINNING_NO;
64
65         entity first_player = NULL;
66         int totalplayers = 0;
67         int totalplayed = 0;
68         FOREACH_CLIENT(true, {
69                 if (IS_PLAYER(it) && it.frags == FRAGS_PLAYER)
70                 {
71                         if (!totalplayers)
72                                 first_player = it;
73                         ++totalplayers;
74                 }
75                 else if (GameRules_scoring_add(it, LMS_RANK, 0))
76                         ++totalplayed;
77         });
78
79         if (totalplayers)
80         {
81                 if (totalplayers > 1)
82                 {
83                         // two or more active players - continue with the game
84
85                         if (autocvar_g_campaign && campaign_bots_may_start)
86                         {
87                                 FOREACH_CLIENT(IS_REAL_CLIENT(it), {
88                                         float pl_lives = GameRules_scoring_add(it, LMS_LIVES, 0);
89                                         if (!pl_lives)
90                                                 return WINNING_YES; // human player lost, game over
91                                         break;
92                                 });
93                         }
94                 }
95                 else
96                 {
97                         // exactly one player?
98
99                         ClearWinners();
100
101                         if (LMS_NewPlayerLives())
102                         {
103                                 if (totalplayed && game_starttime > 0 && time > game_starttime + autocvar_g_lms_forfeit_min_match_time) // give players time to join
104                                 {
105                                         GameRules_scoring_add(first_player, LMS_RANK, 1);
106                                         first_player.winning = 1;
107                                         return WINNING_YES;
108                                 }
109                                 // game still running (that is, nobody got removed from the game by a frag yet)? then continue
110                                 return WINNING_NO;
111                         }
112                         else
113                         {
114                                 // a winner!
115                                 // and assign them their first place
116                                 GameRules_scoring_add(first_player, LMS_RANK, 1);
117                                 first_player.winning = 1;
118                                 return WINNING_YES;
119                         }
120                 }
121         }
122         else
123         {
124                 // nobody is playing at all...
125                 if (LMS_NewPlayerLives())
126                 {
127                         if (totalplayed && game_starttime > 0 && time > game_starttime + autocvar_g_lms_forfeit_min_match_time) // give players time to join
128                         {
129                                 ClearWinners();
130                                 return WINNING_YES;
131                         }
132                         // wait for players...
133                 }
134                 else
135                 {
136                         // SNAFU (maybe a draw game?)
137                         ClearWinners();
138                         LOG_TRACE("No players, ending game.");
139                         return WINNING_YES;
140                 }
141         }
142
143         // When we get here, we have at least two players who are actually LIVING,
144         // now check if the top two players have equal score.
145         WinningConditionHelper(NULL);
146
147         ClearWinners();
148         if(WinningConditionHelper_winner)
149                 WinningConditionHelper_winner.winning = true;
150         if(WinningConditionHelper_topscore == WinningConditionHelper_secondscore)
151                 return WINNING_NEVER;
152
153         // Top two have different scores? Way to go for our beloved TIMELIMIT!
154         return WINNING_NO;
155 }
156
157 // runs on waypoints which are attached to leaders, updates once per frame
158 bool lms_waypointsprite_visible_for_player(entity this, entity player, entity view)
159 {
160         if(view.lms_leader)
161                 if(IS_SPEC(player))
162                         return false; // we don't want spectators of leaders to see the attached waypoint on the top of their screen
163
164         if (!lms_visible_leaders)
165                 return false;
166
167         return true;
168 }
169
170 int lms_leaders_lives_diff;
171 void lms_UpdateLeaders()
172 {
173         int max_lives = 0;
174         int pl_cnt = 0;
175         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
176                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
177                 if (lives > max_lives)
178                         max_lives = lives;
179                 pl_cnt++;
180         });
181
182         int second_max_lives = 0;
183         int pl_cnt_with_max_lives = 0;
184         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
185                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
186                 if (lives == max_lives)
187                         pl_cnt_with_max_lives++;
188                 else if (lives > second_max_lives)
189                         second_max_lives = lives;
190         });
191
192         lms_leaders_lives_diff = max_lives - second_max_lives;
193
194         int lives_diff = autocvar_g_lms_leader_lives_diff;
195         if (lms_leaders_lives_diff >= lives_diff && pl_cnt_with_max_lives <= pl_cnt * autocvar_g_lms_leader_minpercent)
196                 FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
197                         int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
198                         if (lives == max_lives)
199                         {
200                                 if (!it.lms_leader)
201                                         it.lms_leader = true;
202                         }
203                         else
204                         {
205                                 it.lms_leader = false;
206                         }
207                 });
208         else
209                 FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
210                         if (it.waypointsprite_attachedforcarrier)
211                                 WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
212                         it.lms_leader = false;
213                 });
214 }
215
216 // mutator hooks
217 MUTATOR_HOOKFUNCTION(lms, reset_map_global)
218 {
219         lms_lowest_lives = 999;
220 }
221
222 MUTATOR_HOOKFUNCTION(lms, reset_map_players)
223 {
224         FOREACH_CLIENT(true, {
225                 if (it.frags == FRAGS_PLAYER_OUT_OF_GAME)
226                 {
227                         // players who forfeited (rank >= 256) become spectators
228                         if (it.lms_spectate_warning == 2)
229                                 it.frags = FRAGS_SPECTATOR;
230                         else
231                                 it.frags = FRAGS_PLAYER;
232                 }
233
234                 CS(it).killcount = 0;
235                 INGAME_STATUS_CLEAR(it);
236                 it.lms_spectate_warning = 0;
237                 GameRules_scoring_add(it, LMS_RANK, -GameRules_scoring_add(it, LMS_RANK, 0));
238                 GameRules_scoring_add(it, LMS_LIVES, -GameRules_scoring_add(it, LMS_LIVES, 0));
239
240                 if (it.frags != FRAGS_PLAYER)
241                         continue;
242
243                 TRANSMUTE(Player, it);
244                 PutClientInServer(it);
245                 it.lms_leader = false;
246                 if (it.waypointsprite_attachedforcarrier)
247                         WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
248         });
249 }
250
251 // FIXME add support for sv_ready_restart_after_countdown
252 // that is find a way to respawn/reset players IN GAME without setting lives to 0
253 MUTATOR_HOOKFUNCTION(lms, ReadLevelCvars)
254 {
255         // incompatible
256         sv_ready_restart_after_countdown = 0;
257 }
258
259 // returns true if player is added to the game
260 bool lms_AddPlayer(entity player)
261 {
262         if (!INGAME(player))
263         {
264                 int lives = GameRules_scoring_add(player, LMS_LIVES, LMS_NewPlayerLives());
265                 if(lives <= 0)
266                         return false;
267                 if (time < game_starttime)
268                         INGAME_STATUS_SET(player, INGAME_STATUS_JOINED);
269                 else
270                         INGAME_STATUS_SET(player, INGAME_STATUS_JOINING); // this is just to delay setting health and armor that can't be done here
271         }
272         if (warmup_stage || time <= game_starttime)
273         {
274                 if(player.lms_spectate_warning)
275                 {
276                         player.lms_spectate_warning = 0;
277                         GameRules_scoring_add(player, LMS_RANK, -GameRules_scoring_add(player, LMS_RANK, 0));
278                         int lives = GameRules_scoring_add(player, LMS_LIVES, 0);
279                         if(lives <= 0)
280                                 GameRules_scoring_add(player, LMS_LIVES, LMS_NewPlayerLives());
281                 }
282         }
283         else
284         {
285                 if(GameRules_scoring_add(player, LMS_LIVES, 0) <= 0)
286                 {
287                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_LMS_NOLIVES);
288                         return false;
289                 }
290         }
291         return true;
292 }
293
294 MUTATOR_HOOKFUNCTION(lms, PutClientInServer)
295 {
296         entity player = M_ARGV(0, entity);
297         if (!warmup_stage && (IS_BOT_CLIENT(player) || CS(player).jointime != time))
298         {
299                 if (GameRules_scoring_add(player, LMS_RANK, 0) || !lms_AddPlayer(player))
300                         TRANSMUTE(Observer, player);
301         }
302 }
303
304 MUTATOR_HOOKFUNCTION(lms, PlayerSpawn)
305 {
306         entity player = M_ARGV(0, entity);
307
308         if (warmup_stage || time < game_starttime)
309                 return true;
310
311         if (INGAME_JOINING(player))
312         {
313                 // spawn player with the same amount of health / armor
314                 // as the least healthy player with the least number of lives
315                 int pl_lives = GameRules_scoring_add(player, LMS_LIVES, 0);
316                 float min_health = start_health;
317                 float min_armorvalue = start_armorvalue;
318                 FOREACH_CLIENT(it != player && IS_PLAYER(it) && !IS_DEAD(it) && GameRules_scoring_add(it, LMS_LIVES, 0) == pl_lives, {
319                         if (GetResource(it, RES_HEALTH) < min_health)
320                                 min_health = GetResource(it, RES_HEALTH);
321                         if (GetResource(it, RES_ARMOR) < min_armorvalue)
322                                 min_armorvalue = GetResource(it, RES_ARMOR);
323                 });
324                 if (min_health != start_health)
325                         SetResource(player, RES_HEALTH, max(1, min_health));
326                 if (min_armorvalue != start_armorvalue)
327                         SetResource(player, RES_ARMOR, min_armorvalue);
328                 INGAME_STATUS_SET(player, INGAME_STATUS_JOINED);
329         }
330 }
331
332 MUTATOR_HOOKFUNCTION(lms, ForbidSpawn)
333 {
334         entity player = M_ARGV(0, entity);
335
336         if (warmup_stage || lms_AddPlayer(player))
337                 return false;
338
339         return true;
340 }
341
342 void lms_RemovePlayer(entity player)
343 {
344         if (warmup_stage || time < game_starttime)
345                 return;
346
347         float player_rank = GameRules_scoring_add(player, LMS_RANK, 0);
348         if (!player_rank)
349         {
350                 if (player.lms_spectate_warning < 2)
351                 {
352                         player.frags = FRAGS_PLAYER_OUT_OF_GAME;
353                         int pl_cnt = 0;
354                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, {
355                                 pl_cnt++;
356                         });
357                         GameRules_scoring_add(player, LMS_RANK, pl_cnt + 1);
358                 }
359                 else if (INGAME(player))
360                 {
361                         int min_forfeiter_rank = 665; // different from 666
362                         FOREACH_CLIENT(it != player, {
363                                 // update rank of other players that were eliminated
364                                 if (it.frags == FRAGS_PLAYER_OUT_OF_GAME)
365                                 {
366                                         float it_rank = GameRules_scoring_add(it, LMS_RANK, 0);
367                                         if (it_rank > player_rank && it_rank <= 256)
368                                                 GameRules_scoring_add(it, LMS_RANK, -1);
369                                         if (it_rank > 256 && it_rank <= min_forfeiter_rank)
370                                                 min_forfeiter_rank = it_rank - 1;
371                                 }
372                                 else if (it.frags != FRAGS_SPECTATOR)
373                                 {
374                                         float tl = GameRules_scoring_add(it, LMS_LIVES, 0);
375                                         if(tl < lms_lowest_lives)
376                                                 lms_lowest_lives = tl;
377                                 }
378                         });
379                         GameRules_scoring_add(player, LMS_RANK, min_forfeiter_rank);
380                         if(!warmup_stage)
381                                 GameRules_scoring_add(player, LMS_LIVES, -GameRules_scoring_add(player, LMS_LIVES, 0));
382                         player.frags = FRAGS_PLAYER_OUT_OF_GAME;
383                         TRANSMUTE(Observer, player);
384                 }
385                 if (autocvar_g_lms_leader_lives_diff > 0)
386                         lms_UpdateLeaders();
387         }
388
389         if (CS(player).killcount != FRAGS_SPECTATOR && player.lms_spectate_warning < 3)
390         {
391                 if (GameRules_scoring_add(player, LMS_RANK, 0) > 0 && player.lms_spectate_warning < 2)
392                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_LMS_NOLIVES, player.netname);
393                 else
394                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_LMS_FORFEIT, player.netname);
395         }
396 }
397
398 MUTATOR_HOOKFUNCTION(lms, ClientDisconnect)
399 {
400         entity player = M_ARGV(0, entity);
401
402         // no further message other than the disconnect message
403         player.lms_spectate_warning = 3;
404
405         lms_RemovePlayer(player);
406         INGAME_STATUS_CLEAR(player);
407 }
408
409 MUTATOR_HOOKFUNCTION(lms, MakePlayerObserver)
410 {
411         entity player = M_ARGV(0, entity);
412         bool is_forced = M_ARGV(1, bool);
413
414         if (!IS_PLAYER(player))
415                 return true;
416
417         if (warmup_stage || time <= game_starttime)
418         {
419                 GameRules_scoring_add(player, LMS_LIVES, -GameRules_scoring_add(player, LMS_LIVES, 0));
420                 player.frags = FRAGS_SPECTATOR;
421                 TRANSMUTE(Observer, player);
422                 INGAME_STATUS_CLEAR(player);
423         }
424         else
425         {
426                 if (is_forced)
427                         player.lms_spectate_warning = 2;
428                 if (!GameRules_scoring_add(player, LMS_RANK, 0))
429                         lms_RemovePlayer(player);
430         }
431         return true;  // prevent team reset
432 }
433
434 MUTATOR_HOOKFUNCTION(lms, ClientConnect)
435 {
436         entity player = M_ARGV(0, entity);
437         player.frags = FRAGS_SPECTATOR;
438 }
439
440 MUTATOR_HOOKFUNCTION(lms, PlayerPreThink)
441 {
442         entity player = M_ARGV(0, entity);
443
444         // recycled REDALIVE and BLUEALIVE to avoid adding a dedicated stat
445         STAT(REDALIVE, player) = lms_leaders;
446         STAT(BLUEALIVE, player) = lms_leaders_lives_diff;
447
448         if(player.deadflag == DEAD_DYING)
449                 player.deadflag = DEAD_RESPAWNING;
450 }
451
452 MUTATOR_HOOKFUNCTION(lms, SV_StartFrame)
453 {
454         if (intermission_running)
455                 return;
456
457         lms_leaders = 0;
458         FOREACH_CLIENT(true, {
459                 if (IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME && it.lms_leader)
460                         lms_leaders++;
461         });
462
463         float leader_time = autocvar_g_lms_leader_wp_time;
464         float leader_interval = leader_time + autocvar_g_lms_leader_wp_interval;
465         lms_visible_leaders_prev = lms_visible_leaders;
466         lms_visible_leaders = (lms_leaders && time > lms_visible_leaders_time && time < lms_visible_leaders_time + leader_time);
467         if (!lms_leaders || (lms_visible_leaders_prev && !lms_visible_leaders))
468                 lms_visible_leaders_time = time + leader_interval + random() * autocvar_g_lms_leader_wp_interval_jitter;
469
470         FOREACH_CLIENT(true, {
471                 STAT(OBJECTIVE_STATUS, it) = lms_visible_leaders;
472                 if (IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME)
473                 {
474                         if (it.lms_leader)
475                         {
476                                 if (!it.waypointsprite_attachedforcarrier)
477                                 {
478                                         WaypointSprite_AttachCarrier(WP_LmsLeader, it, RADARICON_FLAGCARRIER);
479                                         it.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = lms_waypointsprite_visible_for_player;
480                                         WaypointSprite_UpdateRule(it.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
481                                         vector pl_color = colormapPaletteColor(it.clientcolors & 0x0F, false);
482                                         WaypointSprite_UpdateTeamRadar(it.waypointsprite_attachedforcarrier, RADARICON_FLAGCARRIER, pl_color);
483                                         WaypointSprite_Ping(it.waypointsprite_attachedforcarrier);
484                                 }
485                                 if (!lms_visible_leaders_prev && lms_visible_leaders && IS_REAL_CLIENT(it))
486                                         Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_LMS_VISIBLE_LEADER);
487                         }
488                         else // if (!it.lms_leader)
489                         {
490                                 if (IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME)
491                                 {
492                                         if (!lms_visible_leaders_prev && lms_visible_leaders && IS_REAL_CLIENT(it))
493                                                 Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_LMS_VISIBLE_OTHER);
494                                 }
495                                 if (it.waypointsprite_attachedforcarrier)
496                                         WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
497                         }
498                 }
499         });
500 }
501
502 MUTATOR_HOOKFUNCTION(lms, PlayerRegen)
503 {
504         if(!autocvar_g_lms_regenerate)
505                 M_ARGV(2, float) = 0;
506         if(!autocvar_g_lms_rot)
507                 M_ARGV(3, float) = 0;
508         return (!autocvar_g_lms_regenerate && !autocvar_g_lms_rot);
509 }
510
511 MUTATOR_HOOKFUNCTION(lms, PlayerPowerups)
512 {
513         entity player = M_ARGV(0, entity);
514         if (player.waypointsprite_attachedforcarrier)
515                 player.effects |= (EF_ADDITIVE | EF_FULLBRIGHT);
516         else
517                 player.effects &= ~(EF_ADDITIVE | EF_FULLBRIGHT);
518 }
519
520 MUTATOR_HOOKFUNCTION(lms, ForbidThrowCurrentWeapon)
521 {
522         // forbode!
523         return true;
524 }
525
526 MUTATOR_HOOKFUNCTION(lms, Damage_Calculate)
527 {
528         if (!autocvar_g_lms_dynamic_vampire)
529                 return;
530
531         entity frag_attacker = M_ARGV(1, entity);
532         entity frag_target = M_ARGV(2, entity);
533         float frag_damage = M_ARGV(4, float);
534
535         if (IS_PLAYER(frag_attacker) && !IS_DEAD(frag_attacker)
536                 && IS_PLAYER(frag_target) && !IS_DEAD(frag_target) && frag_attacker != frag_target)
537         {
538                 float vampire_factor = 0;
539
540                 int frag_attacker_lives = GameRules_scoring_add(frag_attacker, LMS_LIVES, 0);
541                 int frag_target_lives = GameRules_scoring_add(frag_target, LMS_LIVES, 0);
542                 int diff = frag_target_lives - frag_attacker_lives - autocvar_g_lms_dynamic_vampire_min_lives_diff;
543
544                 if (diff >= 0)
545                         vampire_factor = autocvar_g_lms_dynamic_vampire_factor_base + diff * autocvar_g_lms_dynamic_vampire_factor_increase;
546                 if (vampire_factor > 0)
547                 {
548                         vampire_factor = min(vampire_factor, autocvar_g_lms_dynamic_vampire_factor_max);
549                         SetResourceExplicit(frag_attacker, RES_HEALTH,
550                                 min(GetResource(frag_attacker, RES_HEALTH) + frag_damage * vampire_factor, start_health));
551                 }
552         }
553 }
554
555 MUTATOR_HOOKFUNCTION(lms, PlayerDied)
556 {
557         if (!warmup_stage && autocvar_g_lms_leader_lives_diff > 0)
558                 lms_UpdateLeaders();
559 }
560
561 MUTATOR_HOOKFUNCTION(lms, CalculateRespawnTime)
562 {
563         entity player = M_ARGV(0, entity);
564         player.respawn_flags |= RESPAWN_FORCE;
565
566         int pl_lives = GameRules_scoring_add(player, LMS_LIVES, 0);
567         if (pl_lives <= 0)
568         {
569                 player.respawn_flags = RESPAWN_SILENT;
570                 // prevent unwanted sudden rejoin as spectator and movement of spectator camera
571                 player.respawn_time = time + 2;
572                 return true;
573         }
574
575         if (autocvar_g_lms_dynamic_respawn_delay <= 0)
576                 return false;
577
578         int max_lives = 0;
579         int pl_cnt = 0;
580         FOREACH_CLIENT(it != player && IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
581                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
582                 if (lives > max_lives)
583                         max_lives = lives;
584                 pl_cnt++;
585         });
586
587         // min delay with only 2 players
588         if (pl_cnt == 1) // player wasn't counted
589                 max_lives = 0;
590
591         float dlay = autocvar_g_lms_dynamic_respawn_delay_base +
592                 autocvar_g_lms_dynamic_respawn_delay_increase * max(0, max_lives - pl_lives);
593         player.respawn_time = time + min(autocvar_g_lms_dynamic_respawn_delay_max, dlay);
594         return true;
595 }
596
597 MUTATOR_HOOKFUNCTION(lms, GiveFragsForKill)
598 {
599         entity frag_target = M_ARGV(1, entity);
600
601         if (!warmup_stage && time > game_starttime)
602         {
603                 // remove a life
604                 int tl = GameRules_scoring_add(frag_target, LMS_LIVES, -1);
605                 if(tl < lms_lowest_lives)
606                         lms_lowest_lives = tl;
607                 if(tl <= 0)
608                 {
609                         int pl_cnt = 0;
610                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, {
611                                 pl_cnt++;
612                         });
613                         frag_target.frags = FRAGS_PLAYER_OUT_OF_GAME;
614                         GameRules_scoring_add(frag_target, LMS_RANK, pl_cnt);
615                 }
616         }
617         M_ARGV(2, float) = 0; // frag score
618
619         return true;
620 }
621
622 MUTATOR_HOOKFUNCTION(lms, SetStartItems)
623 {
624         start_items &= ~(IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS);
625         if(!cvar("g_use_ammunition"))
626                 start_items |= IT_UNLIMITED_AMMO;
627
628         start_health       = warmup_start_health       = cvar("g_lms_start_health");
629         start_armorvalue   = warmup_start_armorvalue   = cvar("g_lms_start_armor");
630         start_ammo_shells  = warmup_start_ammo_shells  = cvar("g_lms_start_ammo_shells");
631         start_ammo_nails   = warmup_start_ammo_nails   = cvar("g_lms_start_ammo_nails");
632         start_ammo_rockets = warmup_start_ammo_rockets = cvar("g_lms_start_ammo_rockets");
633         start_ammo_cells   = warmup_start_ammo_cells   = cvar("g_lms_start_ammo_cells");
634         start_ammo_plasma  = warmup_start_ammo_plasma  = cvar("g_lms_start_ammo_plasma");
635         start_ammo_fuel    = warmup_start_ammo_fuel    = cvar("g_lms_start_ammo_fuel");
636 }
637
638 MUTATOR_HOOKFUNCTION(lms, ForbidPlayerScore_Clear)
639 {
640         // don't clear player score
641         return true;
642 }
643
644 MUTATOR_HOOKFUNCTION(lms, FilterItemDefinition)
645 {
646         if (autocvar_g_lms_items)
647                 return false;
648
649         entity definition = M_ARGV(0, entity);
650
651         if (autocvar_g_lms_extra_lives && definition == ITEM_ExtraLife)
652         {
653                 return false;
654         }
655         return (autocvar_g_pickup_items <= 0); // only allow items if explicitly enabled
656 }
657
658 void lms_extralife(entity this)
659 {
660         StartItem(this, ITEM_ExtraLife);
661 }
662
663 MUTATOR_HOOKFUNCTION(lms, OnEntityPreSpawn)
664 {
665         if (MUTATOR_RETURNVALUE) return false;
666         if (!autocvar_g_powerups) return false;
667         if (!autocvar_g_lms_extra_lives) return false;
668
669         entity ent = M_ARGV(0, entity);
670
671         // Can't use .itemdef here
672         if (ent.classname != "item_health_mega") return false;
673
674         entity e = spawn();
675         setthink(e, lms_extralife);
676
677         Item_CopyFields(ent, e);
678
679         e.nextthink = time + 0.1;
680
681         return true;
682 }
683
684 MUTATOR_HOOKFUNCTION(lms, ItemTouch)
685 {
686         if(MUTATOR_RETURNVALUE) return false;
687
688         entity item = M_ARGV(0, entity);
689         entity toucher = M_ARGV(1, entity);
690
691         if(item.itemdef == ITEM_ExtraLife)
692         {
693                 Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_EXTRALIVES, autocvar_g_lms_extra_lives);
694                 GameRules_scoring_add(toucher, LMS_LIVES, autocvar_g_lms_extra_lives);
695                 return MUT_ITEMTOUCH_PICKUP;
696         }
697
698         return MUT_ITEMTOUCH_CONTINUE;
699 }
700
701 MUTATOR_HOOKFUNCTION(lms, Bot_FixCount, CBC_ORDER_EXCLUSIVE)
702 {
703         FOREACH_CLIENT(IS_REAL_CLIENT(it), {
704                 if (INGAME(it) && it.lms_spectate_warning < 2)
705                         ++M_ARGV(0, int); // activerealplayers
706                 ++M_ARGV(1, int); // realplayers
707         });
708
709         return true;
710 }
711
712 MUTATOR_HOOKFUNCTION(lms, ClientCommand_Spectate)
713 {
714         entity player = M_ARGV(0, entity);
715
716         if(warmup_stage || time < game_starttime || player.lms_spectate_warning)
717         {
718                 // for the forfeit message...
719                 player.lms_spectate_warning = 2;
720         }
721         else
722         {
723                 if(player.frags != FRAGS_SPECTATOR && player.frags != FRAGS_PLAYER_OUT_OF_GAME)
724                 {
725                         player.lms_spectate_warning = 1;
726                         sprint(player, "^1WARNING:^7 you can't rejoin this match after spectating. Use the same command again to spectate anyway.\n");
727                         Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_LMS_SPECWARN);
728                 }
729                 return MUT_SPECCMD_RETURN;
730         }
731         return MUT_SPECCMD_CONTINUE;
732 }
733
734 MUTATOR_HOOKFUNCTION(lms, CheckRules_World)
735 {
736         M_ARGV(0, float) = WinningCondition_LMS();
737         return true;
738 }
739
740 MUTATOR_HOOKFUNCTION(lms, SetWeaponArena)
741 {
742         if(M_ARGV(0, string) == "0" || M_ARGV(0, string) == "")
743                 M_ARGV(0, string) = autocvar_g_lms_weaponarena;
744 }
745
746 MUTATOR_HOOKFUNCTION(lms, AddPlayerScore)
747 {
748         if(game_stopped)
749         if(M_ARGV(0, entity) == SP_LMS_RANK) // score field
750                 return true; // allow writing to this field in intermission as it is needed for newly joining players
751 }
752
753 void lms_Initialize()
754 {
755         lms_lowest_lives = 999;
756 }