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