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