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