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