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