]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/lms/sv_lms.qc
Prepare LMS code for an cleaner merge (no actual change)
[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;
14 float autocvar_g_lms_leader_wp_max_relative;
15 float autocvar_g_lms_leader_wp_time;
16 float autocvar_g_lms_leader_wp_time_repeat;
17 float autocvar_g_lms_dynamic_respawn_delay;
18 float autocvar_g_lms_dynamic_respawn_delay_base;
19 float autocvar_g_lms_dynamic_respawn_delay_increase;
20 bool autocvar_g_lms_dynamic_vampire;
21 float autocvar_g_lms_dynamic_vampire_factor_base;
22 float autocvar_g_lms_dynamic_vampire_factor_increase;
23 float autocvar_g_lms_dynamic_vampire_factor_max;
24 int autocvar_g_lms_dynamic_vampire_min_lives_diff;
25
26 .float lms_wp_time;
27
28 // main functions
29 int LMS_NewPlayerLives()
30 {
31         int fl = floor(autocvar_fraglimit);
32         if(fl == 0 || fl > 999)
33                 fl = 999;
34
35         // first player has left the game for dying too much? Nobody else can get in.
36         if(lms_lowest_lives < 1)
37                 return 0;
38
39         if(!autocvar_g_lms_join_anytime)
40                 if(lms_lowest_lives < fl - max(0, floor(autocvar_g_lms_last_join)))
41                         return 0;
42
43         return bound(1, lms_lowest_lives, fl);
44 }
45
46 void ClearWinners();
47
48 // LMS winning condition: game terminates if and only if there's at most one
49 // one player who's living lives. Top two scores being equal cancels the time
50 // limit.
51 int WinningCondition_LMS()
52 {
53         entity first_player = NULL;
54         int totalplayers = 0;
55         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
56                 if (!totalplayers)
57                         first_player = it;
58                 ++totalplayers;
59         });
60
61         if (totalplayers)
62         {
63                 if (totalplayers > 1)
64                 {
65                         // two or more active players - continue with the game
66
67                         if (autocvar_g_campaign)
68                         {
69                                 FOREACH_CLIENT(IS_REAL_CLIENT(it), {
70                                         float pl_lives = GameRules_scoring_add(it, LMS_LIVES, 0);
71                                         if (!pl_lives)
72                                                 return WINNING_YES; // human player lost, game over
73                                         break;
74                                 });
75                         }
76                 }
77                 else
78                 {
79                         // exactly one player?
80
81                         ClearWinners();
82                         SetWinners(winning, 0); // NOTE: exactly one player is still "player", so this works out
83
84                         if (LMS_NewPlayerLives())
85                         {
86                                 // game still running (that is, nobody got removed from the game by a frag yet)? then continue
87                                 return WINNING_NO;
88                         }
89                         else
90                         {
91                                 // a winner!
92                                 // and assign him his first place
93                                 GameRules_scoring_add(first_player, LMS_RANK, 1);
94                                 if(warmup_stage)
95                                         return WINNING_NO;
96                                 else
97                                         return WINNING_YES;
98                         }
99                 }
100         }
101         else
102         {
103                 // nobody is playing at all...
104                 if (LMS_NewPlayerLives())
105                 {
106                         // wait for players...
107                 }
108                 else
109                 {
110                         // SNAFU (maybe a draw game?)
111                         ClearWinners();
112                         LOG_TRACE("No players, ending game.");
113                         return WINNING_YES;
114                 }
115         }
116
117         // When we get here, we have at least two players who are actually LIVING,
118         // now check if the top two players have equal score.
119         WinningConditionHelper(NULL);
120
121         ClearWinners();
122         if(WinningConditionHelper_winner)
123                 WinningConditionHelper_winner.winning = true;
124         if(WinningConditionHelper_topscore == WinningConditionHelper_secondscore)
125                 return WINNING_NEVER;
126
127         // Top two have different scores? Way to go for our beloved TIMELIMIT!
128         return WINNING_NO;
129 }
130
131 // mutator hooks
132 MUTATOR_HOOKFUNCTION(lms, reset_map_global)
133 {
134         lms_lowest_lives = 999;
135 }
136
137 MUTATOR_HOOKFUNCTION(lms, reset_map_players)
138 {
139         FOREACH_CLIENT(true, {
140                 TRANSMUTE(Player, it);
141                 it.frags = FRAGS_PLAYER;
142                 GameRules_scoring_add(it, LMS_LIVES, LMS_NewPlayerLives());
143                 PutClientInServer(it);
144                 if (it.waypointsprite_attachedforcarrier)
145                         WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
146         });
147 }
148
149 // FIXME add support for sv_ready_restart_after_countdown
150 // that is find a way to respawn/reset players IN GAME without setting lives to 0
151 MUTATOR_HOOKFUNCTION(lms, ReadLevelCvars)
152 {
153         // incompatible
154         sv_ready_restart_after_countdown = 0;
155 }
156
157 MUTATOR_HOOKFUNCTION(lms, PutClientInServer)
158 {
159         entity player = M_ARGV(0, entity);
160
161         if(player.frags == FRAGS_SPECTATOR)
162                 TRANSMUTE(Observer, player);
163         else
164         {
165                 float tl = GameRules_scoring_add(player, LMS_LIVES, 0);
166                 if(tl < lms_lowest_lives)
167                         lms_lowest_lives = tl;
168                 if(tl <= 0)
169                         TRANSMUTE(Observer, player);
170                 if(warmup_stage)
171                         GameRules_scoring_add(player, LMS_RANK, -GameRules_scoring_add(player, LMS_RANK, 0));
172         }
173 }
174
175 MUTATOR_HOOKFUNCTION(lms, PlayerSpawn)
176 {
177 }
178
179 MUTATOR_HOOKFUNCTION(lms, ForbidSpawn)
180 {
181         entity player = M_ARGV(0, entity);
182
183         if(warmup_stage)
184                 return false;
185         if(player.frags == FRAGS_SPECTATOR || GameRules_scoring_add(player, LMS_LIVES, 0) <= 0)
186         {
187                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_LMS_NOLIVES);
188                 return true;
189         }
190         return false;
191 }
192
193 MUTATOR_HOOKFUNCTION(lms, PlayerDies)
194 {
195 }
196
197 void lms_RemovePlayer(entity player)
198 {
199         static int quitters = 0;
200         float player_rank = GameRules_scoring_add(player, LMS_RANK, 0);
201         if (!player_rank)
202         {
203                 if (player.lms_spectate_warning < 2)
204                 {
205                         player.frags = FRAGS_PLAYER_OUT_OF_GAME;
206                         int pl_cnt = 0;
207                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
208                                 pl_cnt++;
209                         });
210                         GameRules_scoring_add(player, LMS_RANK, pl_cnt + 1);
211                 }
212                 else
213                 {
214                         FOREACH_CLIENT(true, {
215                                 if (it.frags == FRAGS_PLAYER_OUT_OF_GAME)
216                                 {
217                                         float it_rank = GameRules_scoring_add(it, LMS_RANK, 0);
218                                         if (it_rank > player_rank && it_rank <= 256)
219                                                 GameRules_scoring_add(it, LMS_RANK, -1);
220                                 }
221                                 else if (it.frags != FRAGS_SPECTATOR)
222                                 {
223                                         float tl = GameRules_scoring_add(it, LMS_LIVES, 0);
224                                         if(tl < lms_lowest_lives)
225                                                 lms_lowest_lives = tl;
226                                 }
227                         });
228                         GameRules_scoring_add(player, LMS_RANK, 665 - quitters); // different from 666
229                         if(!warmup_stage)
230                         {
231                                 GameRules_scoring_add(player, LMS_LIVES, -GameRules_scoring_add(player, LMS_LIVES, 0));
232                                 ++quitters;
233                         }
234                         player.frags = FRAGS_PLAYER_OUT_OF_GAME;
235                         TRANSMUTE(Observer, player);
236                 }
237         }
238
239         if (CS(player).killcount != FRAGS_SPECTATOR && player.lms_spectate_warning < 3)
240         {
241                 if (GameRules_scoring_add(player, LMS_RANK, 0) > 0 && player.lms_spectate_warning < 2)
242                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_LMS_NOLIVES, player.netname);
243                 else
244                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_LMS_FORFEIT, player.netname);
245         }
246 }
247
248 MUTATOR_HOOKFUNCTION(lms, ClientDisconnect)
249 {
250         entity player = M_ARGV(0, entity);
251
252         // no further message other than the disconnect message
253         player.lms_spectate_warning = 3;
254
255         lms_RemovePlayer(player);
256 }
257
258 MUTATOR_HOOKFUNCTION(lms, MakePlayerObserver)
259 {
260         entity player = M_ARGV(0, entity);
261
262         if (!IS_PLAYER(player))
263                 return true;
264
265         lms_RemovePlayer(player);
266         return true;  // prevent team reset
267 }
268
269 MUTATOR_HOOKFUNCTION(lms, ClientConnect)
270 {
271         entity player = M_ARGV(0, entity);
272
273         if(GameRules_scoring_add(player, LMS_LIVES, LMS_NewPlayerLives()) <= 0)
274         {
275                 GameRules_scoring_add(player, LMS_RANK, 666); // mark as forced spectator for the hud code
276                 player.frags = FRAGS_SPECTATOR;
277         }
278 }
279
280 // FIXME LMS doesn't allow clients to spectate due to its particular implementation
281 MUTATOR_HOOKFUNCTION(lms, AutoJoinOnConnection)
282 {
283         if(autocvar_g_campaign)
284                 return false;
285         return true;
286 }
287
288 MUTATOR_HOOKFUNCTION(lms, PlayerPreThink)
289 {
290         entity player = M_ARGV(0, entity);
291
292         if(player.deadflag == DEAD_DYING)
293                 player.deadflag = DEAD_RESPAWNING;
294 }
295
296 MUTATOR_HOOKFUNCTION(lms, PlayerRegen)
297 {
298         if(autocvar_g_lms_regenerate)
299                 return false;
300         return true;
301 }
302
303 MUTATOR_HOOKFUNCTION(lms, ForbidThrowCurrentWeapon)
304 {
305         // forbode!
306         return true;
307 }
308
309 MUTATOR_HOOKFUNCTION(lms, Damage_Calculate)
310 {
311         if (!autocvar_g_lms_dynamic_vampire)
312                 return;
313
314         entity frag_attacker = M_ARGV(1, entity);
315         entity frag_target = M_ARGV(2, entity);
316         float frag_damage = M_ARGV(4, float);
317
318         if (IS_PLAYER(frag_attacker) && !IS_DEAD(frag_attacker)
319                 && IS_PLAYER(frag_target) && !IS_DEAD(frag_target) && frag_attacker != frag_target)
320         {
321                 float vampire_factor = 0;
322
323                 int frag_attacker_lives = GameRules_scoring_add(frag_attacker, LMS_LIVES, 0);
324                 int frag_target_lives = GameRules_scoring_add(frag_target, LMS_LIVES, 0);
325                 int diff = frag_target_lives - frag_attacker_lives - autocvar_g_lms_dynamic_vampire_min_lives_diff;
326
327                 if (diff >= 0)
328                         vampire_factor = autocvar_g_lms_dynamic_vampire_factor_base + diff * autocvar_g_lms_dynamic_vampire_factor_increase;
329                 if (vampire_factor > 0)
330                 {
331                         vampire_factor = min(vampire_factor, autocvar_g_lms_dynamic_vampire_factor_max);
332                         SetResourceExplicit(frag_attacker, RES_HEALTH,
333                                 min(GetResource(frag_attacker, RES_HEALTH) + frag_damage * vampire_factor, start_health));
334                 }
335         }
336 }
337
338 bool lms_waypointsprite_visible_for_player(entity this, entity player, entity view) // runs on waypoints which are attached to ballcarriers, updates once per frame
339 {
340         if(view.lms_wp_time)
341                 if(IS_SPEC(player))
342                         return false; // we don't want spectators of leaders to see the attached waypoint on the top of their screen
343
344         float leader_time = autocvar_g_lms_leader_wp_time;
345         float leader_repeat_time = leader_time + autocvar_g_lms_leader_wp_time_repeat;
346         float wp_time = this.owner.lms_wp_time;
347         if (wp_time && (time - wp_time) % leader_repeat_time > leader_time)
348                 return false;
349
350         return true;
351 }
352
353 void lms_UpdateWaypoints()
354 {
355         int max_lives = 0;
356         int pl_cnt = 0;
357         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
358                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
359                 if (lives > max_lives)
360                         max_lives = lives;
361                 pl_cnt++;
362         });
363
364         int second_max_lives = 0;
365         int pl_cnt_with_max_lives = 0;
366         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
367                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
368                 if (lives == max_lives)
369                         pl_cnt_with_max_lives++;
370                 else if (lives > second_max_lives)
371                         second_max_lives = lives;
372         });
373
374         int lives_diff = autocvar_g_lms_leader_wp_lives;
375         if (max_lives - second_max_lives >= lives_diff && pl_cnt_with_max_lives <= pl_cnt * autocvar_g_lms_leader_wp_max_relative)
376                 FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
377                         int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
378                         if (lives == max_lives)
379                         {
380                                 if (!it.waypointsprite_attachedforcarrier)
381                                 {
382                                         WaypointSprite_AttachCarrier(WP_LmsLeader, it, RADARICON_FLAGCARRIER);
383                                         it.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = lms_waypointsprite_visible_for_player;
384                                         WaypointSprite_UpdateRule(it.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
385                                         vector pl_color = colormapPaletteColor(it.clientcolors & 0x0F, false);
386                                         WaypointSprite_UpdateTeamRadar(it.waypointsprite_attachedforcarrier, RADARICON_FLAGCARRIER, pl_color);
387                                         WaypointSprite_Ping(it.waypointsprite_attachedforcarrier);
388                                 }
389                                 if (!it.lms_wp_time)
390                                         it.lms_wp_time = time;
391                         }
392                         else
393                         {
394                                 if (it.waypointsprite_attachedforcarrier)
395                                         WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
396                                 it.lms_wp_time = 0;
397                         }
398                 });
399         else
400                 FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
401                         if (it.waypointsprite_attachedforcarrier)
402                                 WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
403                         it.lms_wp_time = 0;
404                 });
405 }
406
407 MUTATOR_HOOKFUNCTION(lms, PlayerDied)
408 {
409         if (!warmup_stage && autocvar_g_lms_leader_wp_lives > 0)
410                 lms_UpdateWaypoints();
411 }
412
413 MUTATOR_HOOKFUNCTION(lms, CalculateRespawnTime)
414 {
415         entity player = M_ARGV(0, entity);
416         player.respawn_flags |= RESPAWN_FORCE;
417
418         int pl_lives = GameRules_scoring_add(player, LMS_LIVES, 0);
419         if (pl_lives <= 0)
420         {
421                 player.respawn_flags = RESPAWN_SILENT;
422                 // prevent unwanted sudden rejoin as spectator and movement of spectator camera
423                 player.respawn_time = time + 2;
424                 return true;
425         }
426
427         if (autocvar_g_lms_dynamic_respawn_delay <= 0)
428                 return false;
429
430         int max_lives = 0;
431         int pl_cnt = 0;
432         FOREACH_CLIENT(it != player && IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
433                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
434                 if (lives > max_lives)
435                         max_lives = lives;
436                 pl_cnt++;
437         });
438
439         // min delay with only 2 players
440         if (pl_cnt == 1) // player wasn't counted
441                 max_lives = 0;
442
443         player.respawn_time = time + autocvar_g_lms_dynamic_respawn_delay_base +
444                 autocvar_g_lms_dynamic_respawn_delay_increase * max(0, max_lives - pl_lives);
445         return true;
446 }
447
448 MUTATOR_HOOKFUNCTION(lms, GiveFragsForKill)
449 {
450         entity frag_target = M_ARGV(1, entity);
451
452         if (!warmup_stage)
453         {
454                 // remove a life
455                 int tl = GameRules_scoring_add(frag_target, LMS_LIVES, -1);
456                 if(tl < lms_lowest_lives)
457                         lms_lowest_lives = tl;
458                 if(tl <= 0)
459                 {
460                         int pl_cnt = 0;
461                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
462                                 pl_cnt++;
463                         });
464                         frag_target.frags = FRAGS_PLAYER_OUT_OF_GAME;
465                         GameRules_scoring_add(frag_target, LMS_RANK, pl_cnt);
466                 }
467         }
468         M_ARGV(2, float) = 0; // frag score
469
470         return true;
471 }
472
473 MUTATOR_HOOKFUNCTION(lms, SetStartItems)
474 {
475         start_items &= ~(IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS);
476         start_health       = warmup_start_health       = cvar("g_lms_start_health");
477         start_armorvalue   = warmup_start_armorvalue   = cvar("g_lms_start_armor");
478         start_ammo_shells  = warmup_start_ammo_shells  = cvar("g_lms_start_ammo_shells");
479         start_ammo_nails   = warmup_start_ammo_nails   = cvar("g_lms_start_ammo_nails");
480         start_ammo_rockets = warmup_start_ammo_rockets = cvar("g_lms_start_ammo_rockets");
481         start_ammo_cells   = warmup_start_ammo_cells   = cvar("g_lms_start_ammo_cells");
482         start_ammo_plasma  = warmup_start_ammo_plasma  = cvar("g_lms_start_ammo_plasma");
483         start_ammo_fuel    = warmup_start_ammo_fuel    = cvar("g_lms_start_ammo_fuel");
484 }
485
486 MUTATOR_HOOKFUNCTION(lms, ForbidPlayerScore_Clear)
487 {
488         // don't clear player score
489         return true;
490 }
491
492 MUTATOR_HOOKFUNCTION(lms, FilterItemDefinition)
493 {
494         entity definition = M_ARGV(0, entity);
495
496         if (autocvar_g_lms_extra_lives && definition == ITEM_ExtraLife)
497         {
498                 return false;
499         }
500         return true;
501 }
502
503 void lms_extralife(entity this)
504 {
505         StartItem(this, ITEM_ExtraLife);
506 }
507
508 MUTATOR_HOOKFUNCTION(lms, OnEntityPreSpawn)
509 {
510         if (MUTATOR_RETURNVALUE) return false;
511         if (!autocvar_g_powerups) return false;
512         if (!autocvar_g_lms_extra_lives) return false;
513
514         entity ent = M_ARGV(0, entity);
515
516         // Can't use .itemdef here
517         if (ent.classname != "item_health_mega") return false;
518
519         entity e = spawn();
520         setthink(e, lms_extralife);
521
522         e.nextthink = time + 0.1;
523         e.spawnflags = ent.spawnflags;
524         e.noalign = ent.noalign;
525         setorigin(e, ent.origin);
526
527         return true;
528 }
529
530 MUTATOR_HOOKFUNCTION(lms, ItemTouch)
531 {
532         if(MUTATOR_RETURNVALUE) return false;
533
534         entity item = M_ARGV(0, entity);
535         entity toucher = M_ARGV(1, entity);
536
537         if(item.itemdef == ITEM_ExtraLife)
538         {
539                 Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_EXTRALIVES, autocvar_g_lms_extra_lives);
540                 GameRules_scoring_add(toucher, LMS_LIVES, autocvar_g_lms_extra_lives);
541                 return MUT_ITEMTOUCH_PICKUP;
542         }
543
544         return MUT_ITEMTOUCH_CONTINUE;
545 }
546
547 MUTATOR_HOOKFUNCTION(lms, Bot_FixCount, CBC_ORDER_EXCLUSIVE)
548 {
549         FOREACH_CLIENT(IS_REAL_CLIENT(it), {
550                 ++M_ARGV(0, int); // activerealplayers
551                 ++M_ARGV(1, int); // realplayers
552         });
553
554         return true;
555 }
556
557 MUTATOR_HOOKFUNCTION(lms, ClientCommand_Spectate)
558 {
559         entity player = M_ARGV(0, entity);
560
561         if(warmup_stage || player.lms_spectate_warning)
562         {
563                 // for the forfeit message...
564                 player.lms_spectate_warning = 2;
565         }
566         else
567         {
568                 if(player.frags != FRAGS_SPECTATOR && player.frags != FRAGS_PLAYER_OUT_OF_GAME)
569                 {
570                         player.lms_spectate_warning = 1;
571                         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");
572                 }
573                 return MUT_SPECCMD_RETURN;
574         }
575         return MUT_SPECCMD_CONTINUE;
576 }
577
578 MUTATOR_HOOKFUNCTION(lms, CheckRules_World)
579 {
580         M_ARGV(0, float) = WinningCondition_LMS();
581         return true;
582 }
583
584 MUTATOR_HOOKFUNCTION(lms, SetWeaponArena)
585 {
586         if(M_ARGV(0, string) == "0" || M_ARGV(0, string) == "")
587                 M_ARGV(0, string) = autocvar_g_lms_weaponarena;
588 }
589
590 MUTATOR_HOOKFUNCTION(lms, GetPlayerStatus)
591 {
592         return true;
593 }
594
595 MUTATOR_HOOKFUNCTION(lms, AddPlayerScore)
596 {
597         if(game_stopped)
598         if(M_ARGV(0, entity) == SP_LMS_RANK) // score field
599                 return true; // allow writing to this field in intermission as it is needed for newly joining players
600 }
601
602 void lms_Initialize()
603 {
604         lms_lowest_lives = 999;
605 }