]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/lms/sv_lms.qc
Reimplement commit a484f02 "LMS: don't show "respawning in x seconds" after losing...
[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, CalculateRespawnTime)
176 {
177         entity player = M_ARGV(0, entity);
178         player.respawn_flags |= RESPAWN_FORCE;
179
180         int pl_lives = GameRules_scoring_add(player, LMS_LIVES, 0);
181         if (pl_lives <= 0)
182         {
183                 player.respawn_flags = RESPAWN_SILENT;
184                 // prevent unwanted sudden rejoin as spectator and movement of spectator camera
185                 player.respawn_time = time + 2;
186                 return true;
187         }
188
189         if (autocvar_g_lms_dynamic_respawn_delay <= 0)
190                 return false;
191
192         int max_lives = 0;
193         int pl_cnt = 0;
194         FOREACH_CLIENT(it != player && IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
195                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
196                 if (lives > max_lives)
197                         max_lives = lives;
198                 pl_cnt++;
199         });
200
201         // min delay with only 2 players
202         if (pl_cnt == 1) // player wasn't counted
203                 max_lives = 0;
204
205         player.respawn_time = time + autocvar_g_lms_dynamic_respawn_delay_base +
206                 autocvar_g_lms_dynamic_respawn_delay_increase * max(0, max_lives - pl_lives);
207         return true;
208 }
209
210 MUTATOR_HOOKFUNCTION(lms, ForbidSpawn)
211 {
212         entity player = M_ARGV(0, entity);
213
214         if(warmup_stage)
215                 return false;
216         if(player.frags == FRAGS_SPECTATOR || GameRules_scoring_add(player, LMS_LIVES, 0) <= 0)
217         {
218                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_LMS_NOLIVES);
219                 return true;
220         }
221         return false;
222 }
223
224 void lms_RemovePlayer(entity player)
225 {
226         static int quitters = 0;
227         float player_rank = GameRules_scoring_add(player, LMS_RANK, 0);
228         if (!player_rank)
229         {
230                 if (player.lms_spectate_warning < 2)
231                 {
232                         player.frags = FRAGS_PLAYER_OUT_OF_GAME;
233                         int pl_cnt = 0;
234                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
235                                 pl_cnt++;
236                         });
237                         GameRules_scoring_add(player, LMS_RANK, pl_cnt + 1);
238                 }
239                 else
240                 {
241                         FOREACH_CLIENT(true, {
242                                 if (it.frags == FRAGS_PLAYER_OUT_OF_GAME)
243                                 {
244                                         float it_rank = GameRules_scoring_add(it, LMS_RANK, 0);
245                                         if (it_rank > player_rank && it_rank <= 256)
246                                                 GameRules_scoring_add(it, LMS_RANK, -1);
247                                 }
248                                 else if (it.frags != FRAGS_SPECTATOR)
249                                 {
250                                         float tl = GameRules_scoring_add(it, LMS_LIVES, 0);
251                                         if(tl < lms_lowest_lives)
252                                                 lms_lowest_lives = tl;
253                                 }
254                         });
255                         GameRules_scoring_add(player, LMS_RANK, 665 - quitters); // different from 666
256                         if(!warmup_stage)
257                         {
258                                 GameRules_scoring_add(player, LMS_LIVES, -GameRules_scoring_add(player, LMS_LIVES, 0));
259                                 ++quitters;
260                         }
261                         player.frags = FRAGS_PLAYER_OUT_OF_GAME;
262                         TRANSMUTE(Observer, player);
263                 }
264         }
265
266         if (CS(player).killcount != FRAGS_SPECTATOR && player.lms_spectate_warning < 3)
267         {
268                 if (GameRules_scoring_add(player, LMS_RANK, 0) > 0 && player.lms_spectate_warning < 2)
269                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_LMS_NOLIVES, player.netname);
270                 else
271                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_LMS_FORFEIT, player.netname);
272         }
273 }
274
275 MUTATOR_HOOKFUNCTION(lms, ClientDisconnect)
276 {
277         entity player = M_ARGV(0, entity);
278
279         // no further message other than the disconnect message
280         player.lms_spectate_warning = 3;
281
282         lms_RemovePlayer(player);
283 }
284
285 MUTATOR_HOOKFUNCTION(lms, MakePlayerObserver)
286 {
287         entity player = M_ARGV(0, entity);
288
289         if (!IS_PLAYER(player))
290                 return true;
291
292         lms_RemovePlayer(player);
293         return true;  // prevent team reset
294 }
295
296 MUTATOR_HOOKFUNCTION(lms, ClientConnect)
297 {
298         entity player = M_ARGV(0, entity);
299
300         if(GameRules_scoring_add(player, LMS_LIVES, LMS_NewPlayerLives()) <= 0)
301         {
302                 GameRules_scoring_add(player, LMS_RANK, 666); // mark as forced spectator for the hud code
303                 player.frags = FRAGS_SPECTATOR;
304         }
305 }
306
307 // FIXME LMS doesn't allow clients to spectate due to its particular implementation
308 MUTATOR_HOOKFUNCTION(lms, AutoJoinOnConnection)
309 {
310         if(autocvar_g_campaign)
311                 return false;
312         return true;
313 }
314
315 MUTATOR_HOOKFUNCTION(lms, PlayerPreThink)
316 {
317         entity player = M_ARGV(0, entity);
318
319         if(player.deadflag == DEAD_DYING)
320                 player.deadflag = DEAD_RESPAWNING;
321 }
322
323 MUTATOR_HOOKFUNCTION(lms, PlayerRegen)
324 {
325         if(autocvar_g_lms_regenerate)
326                 return false;
327         return true;
328 }
329
330 MUTATOR_HOOKFUNCTION(lms, ForbidThrowCurrentWeapon)
331 {
332         // forbode!
333         return true;
334 }
335
336 MUTATOR_HOOKFUNCTION(lms, Damage_Calculate)
337 {
338         if (!autocvar_g_lms_dynamic_vampire)
339                 return;
340
341         entity frag_attacker = M_ARGV(1, entity);
342         entity frag_target = M_ARGV(2, entity);
343         float frag_damage = M_ARGV(4, float);
344
345         if (IS_PLAYER(frag_attacker) && !IS_DEAD(frag_target) && frag_attacker != frag_target)
346         {
347                 float vampire_factor = 0;
348
349                 int frag_attacker_lives = GameRules_scoring_add(frag_attacker, LMS_LIVES, 0);
350                 int frag_target_lives = GameRules_scoring_add(frag_target, LMS_LIVES, 0);
351                 int diff = frag_target_lives - frag_attacker_lives - autocvar_g_lms_dynamic_vampire_min_lives_diff;
352
353                 if (diff >= 0)
354                         vampire_factor = autocvar_g_lms_dynamic_vampire_factor_base + diff * autocvar_g_lms_dynamic_vampire_factor_increase;
355                 if (vampire_factor > 0)
356                 {
357                         vampire_factor = min(vampire_factor, autocvar_g_lms_dynamic_vampire_factor_max);
358                         SetResourceExplicit(frag_attacker, RES_HEALTH,
359                                 min(GetResource(frag_attacker, RES_HEALTH) + frag_damage * vampire_factor, start_health));
360                 }
361         }
362 }
363
364 bool lms_waypointsprite_visible_for_player(entity this, entity player, entity view) // runs on waypoints which are attached to ballcarriers, updates once per frame
365 {
366         if(view.lms_wp_time)
367                 if(IS_SPEC(player))
368                         return false; // we don't want spectators of leaders to see the attached waypoint on the top of their screen
369
370         float leader_time = autocvar_g_lms_leader_wp_time;
371         float leader_repeat_time = leader_time + autocvar_g_lms_leader_wp_time_repeat;
372         float wp_time = this.owner.lms_wp_time;
373         if (wp_time && (time - wp_time) % leader_repeat_time > leader_time)
374                 return false;
375
376         return true;
377 }
378
379 void lms_UpdateWaypoints()
380 {
381         int max_lives = 0;
382         int pl_cnt = 0;
383         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
384                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
385                 if (lives > max_lives)
386                         max_lives = lives;
387                 pl_cnt++;
388         });
389
390         int second_max_lives = 0;
391         int pl_cnt_with_max_lives = 0;
392         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
393                 int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
394                 if (lives == max_lives)
395                         pl_cnt_with_max_lives++;
396                 else if (lives > second_max_lives)
397                         second_max_lives = lives;
398         });
399
400         int lives_diff = autocvar_g_lms_leader_wp_lives;
401         if (max_lives - second_max_lives >= lives_diff && pl_cnt_with_max_lives <= pl_cnt * autocvar_g_lms_leader_wp_max_relative)
402                 FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
403                         int lives = GameRules_scoring_add(it, LMS_LIVES, 0);
404                         if (lives == max_lives)
405                         {
406                                 if (!it.waypointsprite_attachedforcarrier)
407                                 {
408                                         WaypointSprite_AttachCarrier(WP_LmsLeader, it, RADARICON_FLAGCARRIER);
409                                         it.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = lms_waypointsprite_visible_for_player;
410                                         WaypointSprite_UpdateRule(it.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
411                                         vector pl_color = colormapPaletteColor(it.clientcolors & 0x0F, false);
412                                         WaypointSprite_UpdateTeamRadar(it.waypointsprite_attachedforcarrier, RADARICON_FLAGCARRIER, pl_color);
413                                         WaypointSprite_Ping(it.waypointsprite_attachedforcarrier);
414                                 }
415                                 if (!it.lms_wp_time)
416                                         it.lms_wp_time = time;
417                         }
418                         else
419                         {
420                                 if (it.waypointsprite_attachedforcarrier)
421                                         WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
422                                 it.lms_wp_time = 0;
423                         }
424                 });
425         else
426                 FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
427                         if (it.waypointsprite_attachedforcarrier)
428                                 WaypointSprite_Kill(it.waypointsprite_attachedforcarrier);
429                         it.lms_wp_time = 0;
430                 });
431 }
432
433 MUTATOR_HOOKFUNCTION(lms, PlayerDied)
434 {
435         if (!warmup_stage && autocvar_g_lms_leader_wp_lives > 0)
436                 lms_UpdateWaypoints();
437 }
438
439 MUTATOR_HOOKFUNCTION(lms, GiveFragsForKill)
440 {
441         entity frag_target = M_ARGV(1, entity);
442
443         if (!warmup_stage)
444         {
445                 // remove a life
446                 int tl = GameRules_scoring_add(frag_target, LMS_LIVES, -1);
447                 if(tl < lms_lowest_lives)
448                         lms_lowest_lives = tl;
449                 if(tl <= 0)
450                 {
451                         int pl_cnt = 0;
452                         FOREACH_CLIENT(IS_PLAYER(it) && it.frags != FRAGS_PLAYER_OUT_OF_GAME, {
453                                 pl_cnt++;
454                         });
455                         frag_target.frags = FRAGS_PLAYER_OUT_OF_GAME;
456                         GameRules_scoring_add(frag_target, LMS_RANK, pl_cnt);
457                 }
458         }
459         M_ARGV(2, float) = 0; // frag score
460
461         return true;
462 }
463
464 MUTATOR_HOOKFUNCTION(lms, SetStartItems)
465 {
466         start_items &= ~(IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS);
467         start_health       = warmup_start_health       = cvar("g_lms_start_health");
468         start_armorvalue   = warmup_start_armorvalue   = cvar("g_lms_start_armor");
469         start_ammo_shells  = warmup_start_ammo_shells  = cvar("g_lms_start_ammo_shells");
470         start_ammo_nails   = warmup_start_ammo_nails   = cvar("g_lms_start_ammo_nails");
471         start_ammo_rockets = warmup_start_ammo_rockets = cvar("g_lms_start_ammo_rockets");
472         start_ammo_cells   = warmup_start_ammo_cells   = cvar("g_lms_start_ammo_cells");
473         start_ammo_plasma  = warmup_start_ammo_plasma  = cvar("g_lms_start_ammo_plasma");
474         start_ammo_fuel    = warmup_start_ammo_fuel    = cvar("g_lms_start_ammo_fuel");
475 }
476
477 MUTATOR_HOOKFUNCTION(lms, ForbidPlayerScore_Clear)
478 {
479         // don't clear player score
480         return true;
481 }
482
483 MUTATOR_HOOKFUNCTION(lms, FilterItemDefinition)
484 {
485         entity definition = M_ARGV(0, entity);
486
487         if (autocvar_g_lms_extra_lives && definition == ITEM_ExtraLife)
488         {
489                 return false;
490         }
491         return true;
492 }
493
494 void lms_extralife(entity this)
495 {
496         StartItem(this, ITEM_ExtraLife);
497 }
498
499 MUTATOR_HOOKFUNCTION(lms, OnEntityPreSpawn)
500 {
501         if (MUTATOR_RETURNVALUE) return false;
502         if (!autocvar_g_powerups) return false;
503         if (!autocvar_g_lms_extra_lives) return false;
504
505         entity ent = M_ARGV(0, entity);
506
507         // Can't use .itemdef here
508         if (ent.classname != "item_health_mega") return false;
509
510         entity e = spawn();
511         setthink(e, lms_extralife);
512
513         e.nextthink = time + 0.1;
514         e.spawnflags = ent.spawnflags;
515         e.noalign = ent.noalign;
516         setorigin(e, ent.origin);
517
518         return true;
519 }
520
521 MUTATOR_HOOKFUNCTION(lms, ItemTouch)
522 {
523         if(MUTATOR_RETURNVALUE) return false;
524
525         entity item = M_ARGV(0, entity);
526         entity toucher = M_ARGV(1, entity);
527
528         if(item.itemdef == ITEM_ExtraLife)
529         {
530                 Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_EXTRALIVES, autocvar_g_lms_extra_lives);
531                 GameRules_scoring_add(toucher, LMS_LIVES, autocvar_g_lms_extra_lives);
532                 return MUT_ITEMTOUCH_PICKUP;
533         }
534
535         return MUT_ITEMTOUCH_CONTINUE;
536 }
537
538 MUTATOR_HOOKFUNCTION(lms, Bot_FixCount, CBC_ORDER_EXCLUSIVE)
539 {
540         FOREACH_CLIENT(IS_REAL_CLIENT(it), {
541                 ++M_ARGV(0, int); // activerealplayers
542                 ++M_ARGV(1, int); // realplayers
543         });
544
545         return true;
546 }
547
548 MUTATOR_HOOKFUNCTION(lms, ClientCommand_Spectate)
549 {
550         entity player = M_ARGV(0, entity);
551
552         if(warmup_stage || player.lms_spectate_warning)
553         {
554                 // for the forfeit message...
555                 player.lms_spectate_warning = 2;
556         }
557         else
558         {
559                 if(player.frags != FRAGS_SPECTATOR && player.frags != FRAGS_PLAYER_OUT_OF_GAME)
560                 {
561                         player.lms_spectate_warning = 1;
562                         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");
563                 }
564                 return MUT_SPECCMD_RETURN;
565         }
566         return MUT_SPECCMD_CONTINUE;
567 }
568
569 MUTATOR_HOOKFUNCTION(lms, CheckRules_World)
570 {
571         M_ARGV(0, float) = WinningCondition_LMS();
572         return true;
573 }
574
575 MUTATOR_HOOKFUNCTION(lms, SetWeaponArena)
576 {
577         if(M_ARGV(0, string) == "0" || M_ARGV(0, string) == "")
578                 M_ARGV(0, string) = autocvar_g_lms_weaponarena;
579 }
580
581 MUTATOR_HOOKFUNCTION(lms, GetPlayerStatus)
582 {
583         return true;
584 }
585
586 MUTATOR_HOOKFUNCTION(lms, AddPlayerScore)
587 {
588         if(game_stopped)
589         if(M_ARGV(0, entity) == SP_LMS_RANK) // score field
590                 return true; // allow writing to this field in intermission as it is needed for newly joining players
591 }
592
593 void lms_Initialize()
594 {
595         lms_lowest_lives = 999;
596 }