]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/lms/sv_lms.qc
LMS: jitter waypoint repeat time to make it less predictable
[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_wp_lives = 2;
14 float autocvar_g_lms_leader_wp_max_relative = 0.5;
15 float autocvar_g_lms_leader_wp_time = 5;
16 float autocvar_g_lms_leader_wp_time_jitter = 10;
17 float autocvar_g_lms_leader_wp_time_repeat = 25;
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 bool lms_visible_leaders;
31 bool lms_visible_leaders_prev;
32 float lms_leader_time_jitter;
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_wp_lives;
172         if (max_lives - second_max_lives >= lives_diff && pl_cnt_with_max_lives <= pl_cnt * autocvar_g_lms_leader_wp_max_relative)
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_wp_lives > 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_repeat_time = leader_time + autocvar_g_lms_leader_wp_time_repeat;
429         lms_visible_leaders_prev = lms_visible_leaders;
430         lms_visible_leaders = (time % leader_repeat_time < leader_time);
431         if (lms_visible_leaders_prev && !lms_visible_leaders)
432                 lms_leader_time_jitter = random() * autocvar_g_lms_leader_wp_time_jitter;
433         leader_repeat_time += lms_leader_time_jitter;
434
435         lms_leaders = 0;
436         FOREACH_CLIENT(true, {
437                 STAT(OBJECTIVE_STATUS, it) = lms_visible_leaders;
438                 if (IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME)
439                 {
440                         if (it.lms_leader)
441                         {
442                                 if (!it.waypointsprite_attachedforcarrier)
443                                 {
444                                         WaypointSprite_AttachCarrier(WP_LmsLeader, it, RADARICON_FLAGCARRIER);
445                                         it.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = lms_waypointsprite_visible_for_player;
446                                         WaypointSprite_UpdateRule(it.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
447                                         vector pl_color = colormapPaletteColor(it.clientcolors & 0x0F, false);
448                                         WaypointSprite_UpdateTeamRadar(it.waypointsprite_attachedforcarrier, RADARICON_FLAGCARRIER, pl_color);
449                                         WaypointSprite_Ping(it.waypointsprite_attachedforcarrier);
450                                 }
451                                 if (!lms_visible_leaders_prev && lms_visible_leaders && IS_REAL_CLIENT(it))
452                                         Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_LMS_VISIBLE_LEADER);
453                                 lms_leaders++;
454                         }
455                 }
456                 else
457                 {
458                         if (IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME)
459                         {
460                                 if (!lms_visible_leaders_prev && lms_visible_leaders && IS_REAL_CLIENT(it))
461                                         Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_LMS_VISIBLE_OTHER);
462                         }
463                         if (it.waypointsprite_attachedforcarrier)
464                                 WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
465                 }
466         });
467 }
468
469 MUTATOR_HOOKFUNCTION(lms, PlayerRegen)
470 {
471         if(autocvar_g_lms_regenerate)
472                 return false;
473         return true;
474 }
475
476 MUTATOR_HOOKFUNCTION(lms, PlayerPowerups)
477 {
478         entity player = M_ARGV(0, entity);
479         if (player.waypointsprite_attachedforcarrier)
480                 player.effects |= (EF_ADDITIVE | EF_FULLBRIGHT);
481         else
482                 player.effects &= ~(EF_ADDITIVE | EF_FULLBRIGHT);
483 }
484
485 MUTATOR_HOOKFUNCTION(lms, ForbidThrowCurrentWeapon)
486 {
487         // forbode!
488         return true;
489 }
490
491 MUTATOR_HOOKFUNCTION(lms, Damage_Calculate)
492 {
493         if (!autocvar_g_lms_dynamic_vampire)
494                 return;
495
496         entity frag_attacker = M_ARGV(1, entity);
497         entity frag_target = M_ARGV(2, entity);
498         float frag_damage = M_ARGV(4, float);
499
500         if (IS_PLAYER(frag_attacker) && !IS_DEAD(frag_attacker)
501                 && IS_PLAYER(frag_target) && !IS_DEAD(frag_target) && frag_attacker != frag_target)
502         {
503                 float vampire_factor = 0;
504
505                 int frag_attacker_lives = GameRules_scoring_add(frag_attacker, LMS_LIVES, 0);
506                 int frag_target_lives = GameRules_scoring_add(frag_target, LMS_LIVES, 0);
507                 int diff = frag_target_lives - frag_attacker_lives - autocvar_g_lms_dynamic_vampire_min_lives_diff;
508
509                 if (diff >= 0)
510                         vampire_factor = autocvar_g_lms_dynamic_vampire_factor_base + diff * autocvar_g_lms_dynamic_vampire_factor_increase;
511                 if (vampire_factor > 0)
512                 {
513                         vampire_factor = min(vampire_factor, autocvar_g_lms_dynamic_vampire_factor_max);
514                         SetResourceExplicit(frag_attacker, RES_HEALTH,
515                                 min(GetResource(frag_attacker, RES_HEALTH) + frag_damage * vampire_factor, start_health));
516                 }
517         }
518 }
519
520 MUTATOR_HOOKFUNCTION(lms, PlayerDied)
521 {
522         if (!warmup_stage && autocvar_g_lms_leader_wp_lives > 0)
523                 lms_UpdateLeaders();
524 }
525
526 MUTATOR_HOOKFUNCTION(lms, CalculateRespawnTime)
527 {
528         entity player = M_ARGV(0, entity);
529         player.respawn_flags |= RESPAWN_FORCE;
530
531         int pl_lives = GameRules_scoring_add(player, LMS_LIVES, 0);
532         if (pl_lives <= 0)
533         {
534                 player.respawn_flags = RESPAWN_SILENT;
535                 // prevent unwanted sudden rejoin as spectator and movement of spectator camera
536                 player.respawn_time = time + 2;
537                 return true;
538         }
539
540         if (autocvar_g_lms_dynamic_respawn_delay <= 0)
541                 return false;
542
543         int max_lives = 0;
544         int pl_cnt = 0;
545         FOREACH_CLIENT(it != player && IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
546                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
547                 if (lives > max_lives)
548                         max_lives = lives;
549                 pl_cnt++;
550         });
551
552         // min delay with only 2 players
553         if (pl_cnt == 1) // player wasn't counted
554                 max_lives = 0;
555
556         float dlay = autocvar_g_lms_dynamic_respawn_delay_base +
557                 autocvar_g_lms_dynamic_respawn_delay_increase * max(0, max_lives - pl_lives);
558         player.respawn_time = time + min(autocvar_g_lms_dynamic_respawn_delay_max, dlay);
559         return true;
560 }
561
562 MUTATOR_HOOKFUNCTION(lms, GiveFragsForKill)
563 {
564         entity frag_target = M_ARGV(1, entity);
565
566         if (!warmup_stage && time > game_starttime)
567         {
568                 // remove a life
569                 int tl = GameRules_scoring_add(frag_target, LMS_LIVES, -1);
570                 if(tl < lms_lowest_lives)
571                         lms_lowest_lives = tl;
572                 if(tl <= 0)
573                 {
574                         int pl_cnt = 0;
575                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags == FRAGS_PLAYER, {
576                                 pl_cnt++;
577                         });
578                         frag_target.frags = FRAGS_PLAYER_OUT_OF_GAME;
579                         GameRules_scoring_add(frag_target, LMS_RANK, pl_cnt);
580                 }
581         }
582         M_ARGV(2, float) = 0; // frag score
583
584         return true;
585 }
586
587 MUTATOR_HOOKFUNCTION(lms, SetStartItems)
588 {
589         start_items &= ~(IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS);
590         start_health       = warmup_start_health       = cvar("g_lms_start_health");
591         start_armorvalue   = warmup_start_armorvalue   = cvar("g_lms_start_armor");
592         start_ammo_shells  = warmup_start_ammo_shells  = cvar("g_lms_start_ammo_shells");
593         start_ammo_nails   = warmup_start_ammo_nails   = cvar("g_lms_start_ammo_nails");
594         start_ammo_rockets = warmup_start_ammo_rockets = cvar("g_lms_start_ammo_rockets");
595         start_ammo_cells   = warmup_start_ammo_cells   = cvar("g_lms_start_ammo_cells");
596         start_ammo_plasma  = warmup_start_ammo_plasma  = cvar("g_lms_start_ammo_plasma");
597         start_ammo_fuel    = warmup_start_ammo_fuel    = cvar("g_lms_start_ammo_fuel");
598 }
599
600 MUTATOR_HOOKFUNCTION(lms, ForbidPlayerScore_Clear)
601 {
602         // don't clear player score
603         return true;
604 }
605
606 MUTATOR_HOOKFUNCTION(lms, FilterItemDefinition)
607 {
608         entity definition = M_ARGV(0, entity);
609
610         if (autocvar_g_lms_extra_lives && definition == ITEM_ExtraLife)
611         {
612                 return false;
613         }
614         return true;
615 }
616
617 void lms_extralife(entity this)
618 {
619         StartItem(this, ITEM_ExtraLife);
620 }
621
622 MUTATOR_HOOKFUNCTION(lms, OnEntityPreSpawn)
623 {
624         if (MUTATOR_RETURNVALUE) return false;
625         if (!autocvar_g_powerups) return false;
626         if (!autocvar_g_lms_extra_lives) return false;
627
628         entity ent = M_ARGV(0, entity);
629
630         // Can't use .itemdef here
631         if (ent.classname != "item_health_mega") return false;
632
633         entity e = spawn();
634         setthink(e, lms_extralife);
635
636         e.nextthink = time + 0.1;
637         e.spawnflags = ent.spawnflags;
638         e.noalign = ent.noalign;
639         setorigin(e, ent.origin);
640
641         return true;
642 }
643
644 MUTATOR_HOOKFUNCTION(lms, ItemTouch)
645 {
646         if(MUTATOR_RETURNVALUE) return false;
647
648         entity item = M_ARGV(0, entity);
649         entity toucher = M_ARGV(1, entity);
650
651         if(item.itemdef == ITEM_ExtraLife)
652         {
653                 Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_EXTRALIVES, autocvar_g_lms_extra_lives);
654                 GameRules_scoring_add(toucher, LMS_LIVES, autocvar_g_lms_extra_lives);
655                 return MUT_ITEMTOUCH_PICKUP;
656         }
657
658         return MUT_ITEMTOUCH_CONTINUE;
659 }
660
661 MUTATOR_HOOKFUNCTION(lms, Bot_FixCount, CBC_ORDER_EXCLUSIVE)
662 {
663         FOREACH_CLIENT(IS_REAL_CLIENT(it), {
664                 if (it.lmsplayer && it.lms_spectate_warning < 2)
665                         ++M_ARGV(0, int); // activerealplayers
666                 ++M_ARGV(1, int); // realplayers
667         });
668
669         return true;
670 }
671
672 MUTATOR_HOOKFUNCTION(lms, ClientCommand_Spectate)
673 {
674         entity player = M_ARGV(0, entity);
675
676         if(warmup_stage || time < game_starttime || player.lms_spectate_warning)
677         {
678                 // for the forfeit message...
679                 player.lms_spectate_warning = 2;
680         }
681         else
682         {
683                 if(player.frags != FRAGS_SPECTATOR && player.frags != FRAGS_PLAYER_OUT_OF_GAME)
684                 {
685                         player.lms_spectate_warning = 1;
686                         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");
687                 }
688                 return MUT_SPECCMD_RETURN;
689         }
690         return MUT_SPECCMD_CONTINUE;
691 }
692
693 MUTATOR_HOOKFUNCTION(lms, CheckRules_World)
694 {
695         M_ARGV(0, float) = WinningCondition_LMS();
696         return true;
697 }
698
699 MUTATOR_HOOKFUNCTION(lms, SetWeaponArena)
700 {
701         if(M_ARGV(0, string) == "0" || M_ARGV(0, string) == "")
702                 M_ARGV(0, string) = autocvar_g_lms_weaponarena;
703 }
704
705 MUTATOR_HOOKFUNCTION(lms, GetPlayerStatus)
706 {
707         entity player = M_ARGV(0, entity);
708
709         return boolean(player.lmsplayer);
710 }
711
712 MUTATOR_HOOKFUNCTION(lms, AddPlayerScore)
713 {
714         if(game_stopped)
715         if(M_ARGV(0, entity) == SP_LMS_RANK) // score field
716                 return true; // allow writing to this field in intermission as it is needed for newly joining players
717 }
718
719 void lms_Initialize()
720 {
721         lms_lowest_lives = 999;
722 }