]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/lms/sv_lms.qc
LMS: correctly update last forfeiter's health and armor if they forfeited when dead
[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 him his 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                         it.frags = FRAGS_PLAYER;
227
228                 CS(it).killcount = 0;
229                 INGAME_STATUS_CLEAR(it);
230                 it.lms_spectate = false;
231                 GameRules_scoring_add(it, LMS_RANK, -GameRules_scoring_add(it, LMS_RANK, 0));
232                 GameRules_scoring_add(it, LMS_LIVES, -GameRules_scoring_add(it, LMS_LIVES, 0));
233
234                 if (it.frags != FRAGS_PLAYER)
235                         continue;
236
237                 TRANSMUTE(Player, it);
238                 PutClientInServer(it);
239                 it.lms_leader = false;
240                 if (it.waypointsprite_attachedforcarrier)
241                         WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
242         });
243 }
244
245 // FIXME add support for sv_ready_restart_after_countdown
246 // that is find a way to respawn/reset players IN GAME without setting lives to 0
247 MUTATOR_HOOKFUNCTION(lms, ReadLevelCvars)
248 {
249         // incompatible
250         sv_ready_restart_after_countdown = 0;
251 }
252
253 // returns true if player is added to the game
254 bool lms_AddPlayer(entity player)
255 {
256         if (!INGAME(player))
257         {
258                 int lives = GameRules_scoring_add(player, LMS_LIVES, LMS_NewPlayerLives());
259                 if(lives <= 0)
260                         return false;
261                 if (time < game_starttime)
262                         INGAME_STATUS_SET(player, INGAME_STATUS_JOINED);
263                 else
264                         INGAME_STATUS_SET(player, INGAME_STATUS_JOINING); // this is just to delay setting health and armor that can't be done here
265         }
266         if (warmup_stage || time <= game_starttime)
267         {
268                 player.lms_spectate = false;
269                 GameRules_scoring_add(player, LMS_RANK, -GameRules_scoring_add(player, LMS_RANK, 0));
270                 int lives = GameRules_scoring_add(player, LMS_LIVES, 0);
271                 if(lives <= 0)
272                         GameRules_scoring_add(player, LMS_LIVES, LMS_NewPlayerLives());
273         }
274         else
275         {
276                 if(GameRules_scoring_add(player, LMS_LIVES, 0) <= 0)
277                 {
278                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_LMS_NOLIVES);
279                         return false;
280                 }
281         }
282         return true;
283 }
284
285 MUTATOR_HOOKFUNCTION(lms, PutClientInServer)
286 {
287         entity player = M_ARGV(0, entity);
288         if (!warmup_stage && (IS_BOT_CLIENT(player) || CS(player).jointime != time))
289         {
290                 if (GameRules_scoring_add(player, LMS_RANK, 0) || !lms_AddPlayer(player))
291                         TRANSMUTE(Observer, player);
292         }
293 }
294
295 int last_forfeiter_lives;
296 float last_forfeiter_health;
297 float last_forfeiter_armorvalue;
298 MUTATOR_HOOKFUNCTION(lms, PlayerSpawn)
299 {
300         entity player = M_ARGV(0, entity);
301
302         if (warmup_stage || time < game_starttime)
303                 return true;
304
305         if (INGAME_JOINING(player))
306         {
307                 // spawn player with the same amount of health / armor
308                 // as the least healthy player with the least number of lives
309                 int pl_lives = GameRules_scoring_add(player, LMS_LIVES, 0);
310                 float min_health = start_health;
311                 float min_armorvalue = start_armorvalue;
312                 if (last_forfeiter_lives == pl_lives)
313                 {
314                         min_health = last_forfeiter_health;
315                         min_armorvalue = last_forfeiter_armorvalue;
316                 }
317                 FOREACH_CLIENT(it != player && IS_PLAYER(it) && !IS_DEAD(it) && GameRules_scoring_add(it, LMS_LIVES, 0) == pl_lives, {
318                         if (GetResource(it, RES_HEALTH) < min_health)
319                                 min_health = GetResource(it, RES_HEALTH);
320                         if (GetResource(it, RES_ARMOR) < min_armorvalue)
321                                 min_armorvalue = GetResource(it, RES_ARMOR);
322                 });
323                 if (min_health != start_health)
324                         SetResource(player, RES_HEALTH, max(1, min_health));
325                 if (min_armorvalue != start_armorvalue)
326                         SetResource(player, RES_ARMOR, min_armorvalue);
327                 INGAME_STATUS_SET(player, INGAME_STATUS_JOINED);
328         }
329 }
330
331 MUTATOR_HOOKFUNCTION(lms, ForbidSpawn)
332 {
333         entity player = M_ARGV(0, entity);
334
335         if (warmup_stage || lms_AddPlayer(player))
336                 return false;
337
338         return true;
339 }
340
341 void lms_RemovePlayer(entity player)
342 {
343         if (warmup_stage || time < game_starttime)
344                 return;
345
346         float player_rank = GameRules_scoring_add(player, LMS_RANK, 0);
347         if (!player_rank)
348         {
349                 if (!player.lms_spectate)
350                 {
351                         player.frags = FRAGS_PLAYER_OUT_OF_GAME;
352                         int pl_cnt = 0;
353                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, {
354                                 pl_cnt++;
355                         });
356                         GameRules_scoring_add(player, LMS_RANK, pl_cnt + 1);
357                 }
358                 else if (INGAME(player))
359                 {
360                         FOREACH_CLIENT(it != player, {
361                                 // update rank of other players
362                                 if (it.frags == FRAGS_PLAYER_OUT_OF_GAME)
363                                         GameRules_scoring_add(it, LMS_RANK, -1);
364                         });
365                         int rank = GameRules_scoring_add(player, LMS_RANK, 0);
366                         GameRules_scoring_add(player, LMS_RANK, -rank);
367                         if(!warmup_stage)
368                         {
369                                 int pl_lives = GameRules_scoring_add(player, LMS_LIVES, 0);
370                                 float pl_health = IS_DEAD(player) ? start_health : GetResource(player, RES_HEALTH);
371                                 float pl_armor = IS_DEAD(player) ? start_armorvalue : GetResource(player, RES_ARMOR);
372                                 if (!last_forfeiter_lives || pl_lives < last_forfeiter_lives)
373                                 {
374                                         last_forfeiter_lives = pl_lives;
375                                         last_forfeiter_health = pl_health;
376                                         last_forfeiter_armorvalue = pl_armor;
377                                 }
378                                 else if (pl_lives == last_forfeiter_lives)
379                                 {
380                                         // these values actually can belong to a different forfeiter
381                                         last_forfeiter_health = min(last_forfeiter_health, pl_health);
382                                         last_forfeiter_armorvalue = min(last_forfeiter_armorvalue, pl_armor);
383                                 }
384                                 GameRules_scoring_add(player, LMS_LIVES, -pl_lives);
385                         }
386                         player.frags = FRAGS_SPECTATOR;
387                         TRANSMUTE(Observer, player);
388                         INGAME_STATUS_CLEAR(player);
389                         player.lms_spectate = false;
390                         CS(player).killcount = FRAGS_SPECTATOR;
391                 }
392                 if (autocvar_g_lms_leader_lives_diff > 0)
393                         lms_UpdateLeaders();
394         }
395
396         if (CS(player).killcount != FRAGS_SPECTATOR)
397         {
398                 if (GameRules_scoring_add(player, LMS_RANK, 0) > 0)
399                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_LMS_NOLIVES, player.netname);
400         }
401 }
402
403 MUTATOR_HOOKFUNCTION(lms, ClientDisconnect)
404 {
405         entity player = M_ARGV(0, entity);
406
407         player.lms_spectate = true;
408
409         lms_RemovePlayer(player);
410         INGAME_STATUS_CLEAR(player);
411 }
412
413 MUTATOR_HOOKFUNCTION(lms, MakePlayerObserver)
414 {
415         entity player = M_ARGV(0, entity);
416         bool is_forced = M_ARGV(1, bool);
417
418         if (!IS_PLAYER(player))
419                 return true;
420
421         if (warmup_stage || time <= game_starttime)
422         {
423                 GameRules_scoring_add(player, LMS_LIVES, -GameRules_scoring_add(player, LMS_LIVES, 0));
424                 player.frags = FRAGS_SPECTATOR;
425                 TRANSMUTE(Observer, player);
426                 INGAME_STATUS_CLEAR(player);
427         }
428         else
429         {
430                 if (is_forced || player.killindicator_teamchange == -2) // player is forced or wants to spectate
431                         player.lms_spectate = true;
432                 if (!GameRules_scoring_add(player, LMS_RANK, 0))
433                         lms_RemovePlayer(player);
434         }
435         return true;  // prevent team reset
436 }
437
438 MUTATOR_HOOKFUNCTION(lms, ClientConnect)
439 {
440         entity player = M_ARGV(0, entity);
441         player.frags = FRAGS_SPECTATOR;
442 }
443
444 MUTATOR_HOOKFUNCTION(lms, PlayerPreThink)
445 {
446         entity player = M_ARGV(0, entity);
447
448         // recycled REDALIVE and BLUEALIVE to avoid adding a dedicated stat
449         STAT(REDALIVE, player) = lms_leaders;
450         STAT(BLUEALIVE, player) = lms_leaders_lives_diff;
451
452         if(player.deadflag == DEAD_DYING)
453                 player.deadflag = DEAD_RESPAWNING;
454 }
455
456 MUTATOR_HOOKFUNCTION(lms, SV_StartFrame)
457 {
458         if (intermission_running)
459                 return;
460
461         lms_leaders = 0;
462         FOREACH_CLIENT(true, {
463                 if (IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME && it.lms_leader)
464                         lms_leaders++;
465         });
466
467         float leader_time = autocvar_g_lms_leader_wp_time;
468         float leader_interval = leader_time + autocvar_g_lms_leader_wp_interval;
469         lms_visible_leaders_prev = lms_visible_leaders;
470         lms_visible_leaders = (lms_leaders && time > lms_visible_leaders_time && time < lms_visible_leaders_time + leader_time);
471         if (!lms_leaders || (lms_visible_leaders_prev && !lms_visible_leaders))
472                 lms_visible_leaders_time = time + leader_interval + random() * autocvar_g_lms_leader_wp_interval_jitter;
473
474         FOREACH_CLIENT(true, {
475                 STAT(OBJECTIVE_STATUS, it) = lms_visible_leaders;
476                 if (IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME)
477                 {
478                         if (it.lms_leader)
479                         {
480                                 if (!it.waypointsprite_attachedforcarrier)
481                                 {
482                                         WaypointSprite_AttachCarrier(WP_LmsLeader, it, RADARICON_FLAGCARRIER);
483                                         it.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = lms_waypointsprite_visible_for_player;
484                                         WaypointSprite_UpdateRule(it.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
485                                         vector pl_color = colormapPaletteColor(it.clientcolors & 0x0F, false);
486                                         WaypointSprite_UpdateTeamRadar(it.waypointsprite_attachedforcarrier, RADARICON_FLAGCARRIER, pl_color);
487                                         WaypointSprite_Ping(it.waypointsprite_attachedforcarrier);
488                                 }
489                                 if (!lms_visible_leaders_prev && lms_visible_leaders && IS_REAL_CLIENT(it))
490                                         Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_LMS_VISIBLE_LEADER);
491                         }
492                         else // if (!it.lms_leader)
493                         {
494                                 if (IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME)
495                                 {
496                                         if (!lms_visible_leaders_prev && lms_visible_leaders && IS_REAL_CLIENT(it))
497                                                 Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_LMS_VISIBLE_OTHER);
498                                 }
499                                 if (it.waypointsprite_attachedforcarrier)
500                                         WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
501                         }
502                 }
503         });
504 }
505
506 MUTATOR_HOOKFUNCTION(lms, PlayerRegen)
507 {
508         if(!autocvar_g_lms_regenerate)
509                 M_ARGV(2, float) = 0;
510         if(!autocvar_g_lms_rot)
511                 M_ARGV(3, float) = 0;
512         return (!autocvar_g_lms_regenerate && !autocvar_g_lms_rot);
513 }
514
515 MUTATOR_HOOKFUNCTION(lms, PlayerPowerups)
516 {
517         entity player = M_ARGV(0, entity);
518         if (player.waypointsprite_attachedforcarrier)
519                 player.effects |= (EF_ADDITIVE | EF_FULLBRIGHT);
520         else
521                 player.effects &= ~(EF_ADDITIVE | EF_FULLBRIGHT);
522 }
523
524 MUTATOR_HOOKFUNCTION(lms, ForbidThrowCurrentWeapon)
525 {
526         // forbode!
527         return true;
528 }
529
530 MUTATOR_HOOKFUNCTION(lms, Damage_Calculate)
531 {
532         if (!autocvar_g_lms_dynamic_vampire)
533                 return;
534
535         entity frag_attacker = M_ARGV(1, entity);
536         entity frag_target = M_ARGV(2, entity);
537         float frag_damage = M_ARGV(4, float);
538
539         if (IS_PLAYER(frag_attacker) && !IS_DEAD(frag_attacker)
540                 && IS_PLAYER(frag_target) && !IS_DEAD(frag_target) && frag_attacker != frag_target)
541         {
542                 float vampire_factor = 0;
543
544                 int frag_attacker_lives = GameRules_scoring_add(frag_attacker, LMS_LIVES, 0);
545                 int frag_target_lives = GameRules_scoring_add(frag_target, LMS_LIVES, 0);
546                 int diff = frag_target_lives - frag_attacker_lives - autocvar_g_lms_dynamic_vampire_min_lives_diff;
547
548                 if (diff >= 0)
549                         vampire_factor = autocvar_g_lms_dynamic_vampire_factor_base + diff * autocvar_g_lms_dynamic_vampire_factor_increase;
550                 if (vampire_factor > 0)
551                 {
552                         vampire_factor = min(vampire_factor, autocvar_g_lms_dynamic_vampire_factor_max);
553                         SetResourceExplicit(frag_attacker, RES_HEALTH,
554                                 min(GetResource(frag_attacker, RES_HEALTH) + frag_damage * vampire_factor, start_health));
555                 }
556         }
557 }
558
559 MUTATOR_HOOKFUNCTION(lms, PlayerDied)
560 {
561         if (!warmup_stage && autocvar_g_lms_leader_lives_diff > 0)
562                 lms_UpdateLeaders();
563 }
564
565 MUTATOR_HOOKFUNCTION(lms, CalculateRespawnTime)
566 {
567         entity player = M_ARGV(0, entity);
568         player.respawn_flags |= RESPAWN_FORCE;
569
570         int pl_lives = GameRules_scoring_add(player, LMS_LIVES, 0);
571         if (pl_lives <= 0)
572         {
573                 player.respawn_flags = RESPAWN_SILENT;
574                 // prevent unwanted sudden rejoin as spectator and movement of spectator camera
575                 player.respawn_time = time + 2;
576                 return true;
577         }
578
579         if (autocvar_g_lms_dynamic_respawn_delay <= 0)
580                 return false;
581
582         int max_lives = 0;
583         int pl_cnt = 0;
584         FOREACH_CLIENT(it != player && IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
585                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
586                 if (lives > max_lives)
587                         max_lives = lives;
588                 pl_cnt++;
589         });
590
591         // min delay with only 2 players
592         if (pl_cnt == 1) // player wasn't counted
593                 max_lives = 0;
594
595         float dlay = autocvar_g_lms_dynamic_respawn_delay_base +
596                 autocvar_g_lms_dynamic_respawn_delay_increase * max(0, max_lives - pl_lives);
597         player.respawn_time = time + min(autocvar_g_lms_dynamic_respawn_delay_max, dlay);
598         return true;
599 }
600
601 MUTATOR_HOOKFUNCTION(lms, GiveFragsForKill)
602 {
603         entity frag_target = M_ARGV(1, entity);
604
605         if (!warmup_stage && time > game_starttime)
606         {
607                 // remove a life
608                 int tl = GameRules_scoring_add(frag_target, LMS_LIVES, -1);
609                 if(tl < lms_lowest_lives)
610                         lms_lowest_lives = tl;
611                 if(tl <= 0)
612                 {
613                         int pl_cnt = 0;
614                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, {
615                                 pl_cnt++;
616                         });
617                         frag_target.frags = FRAGS_PLAYER_OUT_OF_GAME;
618                         GameRules_scoring_add(frag_target, LMS_RANK, pl_cnt);
619                 }
620         }
621         M_ARGV(2, float) = 0; // frag score
622
623         return true;
624 }
625
626 MUTATOR_HOOKFUNCTION(lms, SetStartItems)
627 {
628         start_items &= ~(IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS);
629         if(!cvar("g_use_ammunition"))
630                 start_items |= IT_UNLIMITED_AMMO;
631
632         start_health       = warmup_start_health       = cvar("g_lms_start_health");
633         start_armorvalue   = warmup_start_armorvalue   = cvar("g_lms_start_armor");
634         start_ammo_shells  = warmup_start_ammo_shells  = cvar("g_lms_start_ammo_shells");
635         start_ammo_nails   = warmup_start_ammo_nails   = cvar("g_lms_start_ammo_nails");
636         start_ammo_rockets = warmup_start_ammo_rockets = cvar("g_lms_start_ammo_rockets");
637         start_ammo_cells   = warmup_start_ammo_cells   = cvar("g_lms_start_ammo_cells");
638         start_ammo_plasma  = warmup_start_ammo_plasma  = cvar("g_lms_start_ammo_plasma");
639         start_ammo_fuel    = warmup_start_ammo_fuel    = cvar("g_lms_start_ammo_fuel");
640 }
641
642 MUTATOR_HOOKFUNCTION(lms, ForbidPlayerScore_Clear)
643 {
644         // don't clear player score
645         return true;
646 }
647
648 MUTATOR_HOOKFUNCTION(lms, FilterItemDefinition)
649 {
650         if (autocvar_g_lms_items)
651                 return false;
652
653         entity definition = M_ARGV(0, entity);
654
655         if (autocvar_g_lms_extra_lives && definition == ITEM_ExtraLife)
656         {
657                 return false;
658         }
659         return (autocvar_g_pickup_items <= 0); // only allow items if explicitly enabled
660 }
661
662 void lms_extralife(entity this)
663 {
664         StartItem(this, ITEM_ExtraLife);
665 }
666
667 MUTATOR_HOOKFUNCTION(lms, OnEntityPreSpawn)
668 {
669         if (MUTATOR_RETURNVALUE) return false;
670         if (!autocvar_g_powerups) return false;
671         if (!autocvar_g_lms_extra_lives) return false;
672
673         entity ent = M_ARGV(0, entity);
674
675         // Can't use .itemdef here
676         if (ent.classname != "item_health_mega") return false;
677
678         entity e = spawn();
679         setthink(e, lms_extralife);
680
681         e.nextthink = time + 0.1;
682         e.spawnflags = ent.spawnflags;
683         e.noalign = ent.noalign;
684         setorigin(e, ent.origin);
685
686         return true;
687 }
688
689 MUTATOR_HOOKFUNCTION(lms, ItemTouch)
690 {
691         if(MUTATOR_RETURNVALUE) return false;
692
693         entity item = M_ARGV(0, entity);
694         entity toucher = M_ARGV(1, entity);
695
696         if(item.itemdef == ITEM_ExtraLife)
697         {
698                 Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_EXTRALIVES, autocvar_g_lms_extra_lives);
699                 GameRules_scoring_add(toucher, LMS_LIVES, autocvar_g_lms_extra_lives);
700                 return MUT_ITEMTOUCH_PICKUP;
701         }
702
703         return MUT_ITEMTOUCH_CONTINUE;
704 }
705
706 MUTATOR_HOOKFUNCTION(lms, Bot_FixCount, CBC_ORDER_EXCLUSIVE)
707 {
708         FOREACH_CLIENT(IS_REAL_CLIENT(it), {
709                 if (INGAME(it))
710                         ++M_ARGV(0, int); // activerealplayers
711                 ++M_ARGV(1, int); // realplayers
712         });
713
714         return true;
715 }
716
717 MUTATOR_HOOKFUNCTION(lms, ClientCommand_Spectate)
718 {
719         entity player = M_ARGV(0, entity);
720         if(player.frags != FRAGS_SPECTATOR && player.frags != FRAGS_PLAYER_OUT_OF_GAME)
721                 return MUT_SPECCMD_CONTINUE;
722         // ranked players (out of game) can no longer become real spectators
723         return MUT_SPECCMD_RETURN;
724 }
725
726 MUTATOR_HOOKFUNCTION(lms, CheckRules_World)
727 {
728         M_ARGV(0, float) = WinningCondition_LMS();
729         return true;
730 }
731
732 MUTATOR_HOOKFUNCTION(lms, SetWeaponArena)
733 {
734         if(M_ARGV(0, string) == "0" || M_ARGV(0, string) == "")
735                 M_ARGV(0, string) = autocvar_g_lms_weaponarena;
736 }
737
738 MUTATOR_HOOKFUNCTION(lms, AddPlayerScore)
739 {
740         if(game_stopped)
741         if(M_ARGV(0, entity) == SP_LMS_RANK) // score field
742                 return true; // allow writing to this field in intermission as it is needed for newly joining players
743 }
744
745 void lms_Initialize()
746 {
747         lms_lowest_lives = 999;
748 }