]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/mutator/gamemode_freezetag.qc
Merge branch 'martin-t/warns' into 'master'
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / mutator / gamemode_freezetag.qc
1 #include "gamemode_freezetag.qh"
2
3 float autocvar_g_freezetag_frozen_maxtime;
4 float autocvar_g_freezetag_revive_clearspeed;
5 float autocvar_g_freezetag_round_timelimit;
6 //int autocvar_g_freezetag_teams;
7 int autocvar_g_freezetag_teams_override;
8 float autocvar_g_freezetag_warmup;
9
10 void freezetag_count_alive_players()
11 {
12         total_players = redalive = bluealive = yellowalive = pinkalive = 0;
13         FOREACH_CLIENT(IS_PLAYER(it), {
14                 switch(it.team)
15                 {
16                         case NUM_TEAM_1: ++total_players; if(it.health >= 1 && STAT(FROZEN, it) != 1) ++redalive; break;
17                         case NUM_TEAM_2: ++total_players; if(it.health >= 1 && STAT(FROZEN, it) != 1) ++bluealive; break;
18                         case NUM_TEAM_3: ++total_players; if(it.health >= 1 && STAT(FROZEN, it) != 1) ++yellowalive; break;
19                         case NUM_TEAM_4: ++total_players; if(it.health >= 1 && STAT(FROZEN, it) != 1) ++pinkalive; break;
20                 }
21         });
22         FOREACH_CLIENT(IS_REAL_CLIENT(it), {
23                 it.redalive_stat = redalive;
24                 it.bluealive_stat = bluealive;
25                 it.yellowalive_stat = yellowalive;
26                 it.pinkalive_stat = pinkalive;
27         });
28
29         eliminatedPlayers.SendFlags |= 1;
30 }
31 #define FREEZETAG_ALIVE_TEAMS() ((redalive > 0) + (bluealive > 0) + (yellowalive > 0) + (pinkalive > 0))
32 #define FREEZETAG_ALIVE_TEAMS_OK() (FREEZETAG_ALIVE_TEAMS() == NumTeams(freezetag_teams))
33
34 float freezetag_CheckTeams()
35 {
36         static float prev_missing_teams_mask;
37         if(FREEZETAG_ALIVE_TEAMS_OK())
38         {
39                 if(prev_missing_teams_mask > 0)
40                         Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_TEAMS);
41                 prev_missing_teams_mask = -1;
42                 return 1;
43         }
44         if(total_players == 0)
45         {
46                 if(prev_missing_teams_mask > 0)
47                         Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_TEAMS);
48                 prev_missing_teams_mask = -1;
49                 return 0;
50         }
51         int missing_teams_mask = 0;
52         if(freezetag_teams & BIT(0))
53                 missing_teams_mask += (!redalive) * 1;
54         if(freezetag_teams & BIT(1))
55                 missing_teams_mask += (!bluealive) * 2;
56         if(freezetag_teams & BIT(2))
57                 missing_teams_mask += (!yellowalive) * 4;
58         if(freezetag_teams & BIT(3))
59                 missing_teams_mask += (!pinkalive) * 8;
60         if(prev_missing_teams_mask != missing_teams_mask)
61         {
62                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_TEAMS, missing_teams_mask);
63                 prev_missing_teams_mask = missing_teams_mask;
64         }
65         return 0;
66 }
67
68 float freezetag_getWinnerTeam()
69 {
70         float winner_team = 0;
71         if(redalive >= 1)
72                 winner_team = NUM_TEAM_1;
73         if(bluealive >= 1)
74         {
75                 if(winner_team) return 0;
76                 winner_team = NUM_TEAM_2;
77         }
78         if(yellowalive >= 1)
79         {
80                 if(winner_team) return 0;
81                 winner_team = NUM_TEAM_3;
82         }
83         if(pinkalive >= 1)
84         {
85                 if(winner_team) return 0;
86                 winner_team = NUM_TEAM_4;
87         }
88         if(winner_team)
89                 return winner_team;
90         return -1; // no player left
91 }
92
93 void nades_Clear(entity);
94 void nades_GiveBonus(entity player, float score);
95
96 float freezetag_CheckWinner()
97 {
98         if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
99         {
100                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_OVER);
101                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_OVER);
102                 FOREACH_CLIENT(IS_PLAYER(it), {
103                         it.freezetag_frozen_timeout = 0;
104                         nades_Clear(it);
105                 });
106                 game_stopped = true;
107                 round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit);
108                 return 1;
109         }
110
111         if(FREEZETAG_ALIVE_TEAMS() > 1)
112                 return 0;
113
114         int winner_team = freezetag_getWinnerTeam();
115         if(winner_team > 0)
116         {
117                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, APP_TEAM_NUM(winner_team, CENTER_ROUND_TEAM_WIN));
118                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(winner_team, INFO_ROUND_TEAM_WIN));
119                 TeamScore_AddToTeam(winner_team, ST_SCORE, +1);
120         }
121         else if(winner_team == -1)
122         {
123                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED);
124                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED);
125         }
126
127         FOREACH_CLIENT(IS_PLAYER(it), {
128                 it.freezetag_frozen_timeout = 0;
129                 nades_Clear(it);
130         });
131
132         game_stopped = true;
133         round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit);
134         return 1;
135 }
136
137 entity freezetag_LastPlayerForTeam(entity this)
138 {
139         entity last_pl = NULL;
140         FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
141                 if(it.health >= 1)
142                 if(!STAT(FROZEN, it))
143                 if(SAME_TEAM(it, this))
144                 if(!last_pl)
145                         last_pl = it;
146                 else
147                         return NULL;
148         });
149         return last_pl;
150 }
151
152 void freezetag_LastPlayerForTeam_Notify(entity this)
153 {
154         if(round_handler_IsActive())
155         if(round_handler_IsRoundStarted())
156         {
157                 entity pl = freezetag_LastPlayerForTeam(this);
158                 if(pl)
159                         Send_Notification(NOTIF_ONE, pl, MSG_CENTER, CENTER_ALONE);
160         }
161 }
162
163 void freezetag_Add_Score(entity targ, entity attacker)
164 {
165         if(attacker == targ)
166         {
167                 // you froze your own dumb targ
168                 // counted as "suicide" already
169                 GameRules_scoring_add(targ, SCORE, -1);
170         }
171         else if(IS_PLAYER(attacker))
172         {
173                 // got frozen by an enemy
174                 // counted as "kill" and "death" already
175                 GameRules_scoring_add(targ, SCORE, -1);
176                 GameRules_scoring_add(attacker, SCORE, +1);
177         }
178         // else nothing - got frozen by the game type rules themselves
179 }
180
181 void freezetag_Freeze(entity targ, entity attacker)
182 {
183         if(STAT(FROZEN, targ))
184                 return;
185
186         if(autocvar_g_freezetag_frozen_maxtime > 0)
187                 targ.freezetag_frozen_timeout = time + autocvar_g_freezetag_frozen_maxtime;
188
189         Freeze(targ, 0, 1, true);
190
191         freezetag_count_alive_players();
192
193         freezetag_Add_Score(targ, attacker);
194 }
195
196 void freezetag_Unfreeze(entity this)
197 {
198         this.freezetag_frozen_time = 0;
199         this.freezetag_frozen_timeout = 0;
200
201         Unfreeze(this);
202 }
203
204 float freezetag_isEliminated(entity e)
205 {
206         if(IS_PLAYER(e) && (STAT(FROZEN, e) == 1 || IS_DEAD(e)))
207                 return true;
208         return false;
209 }
210
211
212 // ================
213 // Bot player logic
214 // ================
215
216 void(entity this) havocbot_role_ft_freeing;
217 void(entity this) havocbot_role_ft_offense;
218
219 void havocbot_goalrating_freeplayers(entity this, float ratingscale, vector org, float sradius)
220 {
221         float t;
222         FOREACH_CLIENT(IS_PLAYER(it) && it != this && SAME_TEAM(it, this), {
223                 if (STAT(FROZEN, it) == 1)
224                 {
225                         if(vdist(it.origin - org, >, sradius))
226                                 continue;
227                         navigation_routerating(this, it, ratingscale, 2000);
228                 }
229                 else if(vdist(it.origin - org, >, 400)) // avoid gathering all teammates in one place
230                 {
231                         // If teamate is not frozen still seek them out as fight better
232                         // in a group.
233                         t = 0.2 * 150 / (this.health + this.armorvalue);
234                         navigation_routerating(this, it, t * ratingscale, 2000);
235                 }
236         });
237 }
238
239 void havocbot_role_ft_offense(entity this)
240 {
241         if(IS_DEAD(this))
242                 return;
243
244         if (!this.havocbot_role_timeout)
245                 this.havocbot_role_timeout = time + random() * 10 + 20;
246
247         // Count how many players on team are unfrozen.
248         int unfrozen = 0;
249         FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(it, this) && !(STAT(FROZEN, it) != 1), { unfrozen++; });
250
251         // If only one left on team or if role has timed out then start trying to free players.
252         if (((unfrozen == 0) && (!STAT(FROZEN, this))) || (time > this.havocbot_role_timeout))
253         {
254                 LOG_TRACE("changing role to freeing");
255                 this.havocbot_role = havocbot_role_ft_freeing;
256                 this.havocbot_role_timeout = 0;
257                 return;
258         }
259
260         if (navigation_goalrating_timeout(this))
261         {
262                 navigation_goalrating_start(this);
263                 havocbot_goalrating_items(this, 10000, this.origin, 10000);
264                 havocbot_goalrating_enemyplayers(this, 20000, this.origin, 10000);
265                 havocbot_goalrating_freeplayers(this, 9000, this.origin, 10000);
266                 havocbot_goalrating_waypoints(this, 1, this.origin, 3000);
267                 navigation_goalrating_end(this);
268
269                 navigation_goalrating_timeout_set(this);
270         }
271 }
272
273 void havocbot_role_ft_freeing(entity this)
274 {
275         if(IS_DEAD(this))
276                 return;
277
278         if (!this.havocbot_role_timeout)
279                 this.havocbot_role_timeout = time + random() * 10 + 20;
280
281         if (time > this.havocbot_role_timeout)
282         {
283                 LOG_TRACE("changing role to offense");
284                 this.havocbot_role = havocbot_role_ft_offense;
285                 this.havocbot_role_timeout = 0;
286                 return;
287         }
288
289         if (navigation_goalrating_timeout(this))
290         {
291                 navigation_goalrating_start(this);
292                 havocbot_goalrating_items(this, 8000, this.origin, 10000);
293                 havocbot_goalrating_enemyplayers(this, 10000, this.origin, 10000);
294                 havocbot_goalrating_freeplayers(this, 20000, this.origin, 10000);
295                 havocbot_goalrating_waypoints(this, 1, this.origin, 3000);
296                 navigation_goalrating_end(this);
297
298                 navigation_goalrating_timeout_set(this);
299         }
300 }
301
302
303 // ==============
304 // Hook Functions
305 // ==============
306
307 void ft_RemovePlayer(entity this)
308 {
309         this.health = 0; // neccessary to update correctly alive stats
310         if(!STAT(FROZEN, this))
311                 freezetag_LastPlayerForTeam_Notify(this);
312         freezetag_Unfreeze(this);
313         freezetag_count_alive_players();
314 }
315
316 MUTATOR_HOOKFUNCTION(ft, ClientDisconnect)
317 {
318         entity player = M_ARGV(0, entity);
319
320         ft_RemovePlayer(player);
321         return true;
322 }
323
324 MUTATOR_HOOKFUNCTION(ft, MakePlayerObserver)
325 {
326         entity player = M_ARGV(0, entity);
327
328         ft_RemovePlayer(player);
329 }
330
331 MUTATOR_HOOKFUNCTION(ft, PlayerDies)
332 {
333         entity frag_attacker = M_ARGV(1, entity);
334         entity frag_target = M_ARGV(2, entity);
335         float frag_deathtype = M_ARGV(3, float);
336
337         if(round_handler_IsActive())
338         if(round_handler_CountdownRunning())
339         {
340                 if(STAT(FROZEN, frag_target))
341                         freezetag_Unfreeze(frag_target);
342                 freezetag_count_alive_players();
343                 return true; // let the player die so that he can respawn whenever he wants
344         }
345
346         // Cases DEATH_TEAMCHANGE and DEATH_AUTOTEAMCHANGE are needed to fix a bug whe
347         // you succeed changing team through the menu: you both really die (gibbing) and get frozen
348         if(ITEM_DAMAGE_NEEDKILL(frag_deathtype)
349                 || frag_deathtype == DEATH_TEAMCHANGE.m_id || frag_deathtype == DEATH_AUTOTEAMCHANGE.m_id)
350         {
351                 // let the player die, he will be automatically frozen when he respawns
352                 if(STAT(FROZEN, frag_target) != 1)
353                 {
354                         freezetag_Add_Score(frag_target, frag_attacker);
355                         freezetag_count_alive_players();
356                         freezetag_LastPlayerForTeam_Notify(frag_target);
357                 }
358                 else
359                         freezetag_Unfreeze(frag_target); // remove ice
360                 frag_target.health = 0; // Unfreeze resets health
361                 frag_target.freezetag_frozen_timeout = -2; // freeze on respawn
362                 return true;
363         }
364
365         if(STAT(FROZEN, frag_target))
366                 return true;
367
368         freezetag_Freeze(frag_target, frag_attacker);
369         freezetag_LastPlayerForTeam_Notify(frag_target);
370
371         if(frag_attacker == frag_target || frag_attacker == NULL)
372         {
373                 if(IS_PLAYER(frag_target))
374                         Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_FREEZETAG_SELF);
375                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_SELF, frag_target.netname);
376         }
377         else
378         {
379                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_FREEZE, frag_target.netname, frag_attacker.netname);
380         }
381
382         return true;
383 }
384
385 MUTATOR_HOOKFUNCTION(ft, PlayerSpawn)
386 {
387         entity player = M_ARGV(0, entity);
388
389         if(player.freezetag_frozen_timeout == -1) // if PlayerSpawn is called by reset_map_players
390                 return true; // do nothing, round is starting right now
391
392         if(player.freezetag_frozen_timeout == -2) // player was dead
393         {
394                 freezetag_Freeze(player, NULL);
395                 return true;
396         }
397
398         freezetag_count_alive_players();
399
400         if(round_handler_IsActive())
401         if(round_handler_IsRoundStarted())
402         {
403                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_SPAWN_LATE);
404                 freezetag_Freeze(player, NULL);
405         }
406
407         return true;
408 }
409
410 MUTATOR_HOOKFUNCTION(ft, reset_map_players)
411 {
412         FOREACH_CLIENT(IS_PLAYER(it), {
413                 CS(it).killcount = 0;
414                 it.freezetag_frozen_timeout = -1;
415                 PutClientInServer(it);
416                 it.freezetag_frozen_timeout = 0;
417         });
418         freezetag_count_alive_players();
419         return true;
420 }
421
422 MUTATOR_HOOKFUNCTION(ft, GiveFragsForKill, CBC_ORDER_FIRST)
423 {
424         M_ARGV(2, float) = 0; // no frags counted in Freeze Tag
425         return true;
426 }
427
428 MUTATOR_HOOKFUNCTION(ft, PlayerPreThink, CBC_ORDER_FIRST)
429 {
430         if(game_stopped)
431                 return true;
432
433         if(round_handler_IsActive())
434         if(!round_handler_IsRoundStarted())
435                 return true;
436
437         int n;
438         entity o = NULL;
439         entity player = M_ARGV(0, entity);
440         //if(STAT(FROZEN, player))
441         //if(player.freezetag_frozen_timeout > 0 && time < player.freezetag_frozen_timeout)
442                 //player.iceblock.alpha = ICE_MIN_ALPHA + (ICE_MAX_ALPHA - ICE_MIN_ALPHA) * (player.freezetag_frozen_timeout - time) / (player.freezetag_frozen_timeout - player.freezetag_frozen_time);
443
444         if(player.freezetag_frozen_timeout > 0 && time >= player.freezetag_frozen_timeout)
445                 n = -1;
446         else
447         {
448                 vector revive_extra_size = '1 1 1' * autocvar_g_freezetag_revive_extra_size;
449                 n = 0;
450                 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
451                         if(STAT(FROZEN, it) == 0)
452                         if(!IS_DEAD(it))
453                         if(SAME_TEAM(it, player))
454                         if(boxesoverlap(player.absmin - revive_extra_size, player.absmax + revive_extra_size, it.absmin, it.absmax))
455                         {
456                                 if(!o)
457                                         o = it;
458                                 if(STAT(FROZEN, player) == 1)
459                                         it.reviving = true;
460                                 ++n;
461                         }
462                 });
463
464         }
465
466         if(n && STAT(FROZEN, player) == 1) // OK, there is at least one teammate reviving us
467         {
468                 player.revive_progress = bound(0, player.revive_progress + frametime * max(1/60, autocvar_g_freezetag_revive_speed), 1);
469                 player.health = max(1, player.revive_progress * ((warmup_stage) ? warmup_start_health : start_health));
470
471                 if(player.revive_progress >= 1)
472                 {
473                         freezetag_Unfreeze(player);
474                         freezetag_count_alive_players();
475
476                         if(n == -1)
477                         {
478                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_AUTO_REVIVED, autocvar_g_freezetag_frozen_maxtime);
479                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_AUTO_REVIVED, player.netname, autocvar_g_freezetag_frozen_maxtime);
480                                 return true;
481                         }
482
483                         // EVERY team mate nearby gets a point (even if multiple!)
484                         FOREACH_CLIENT(IS_PLAYER(it) && it.reviving, {
485                                 GameRules_scoring_add(it, FREEZETAG_REVIVALS, +1);
486                                 GameRules_scoring_add(it, SCORE, +1);
487                                 nades_GiveBonus(it,autocvar_g_nades_bonus_score_low);
488                         });
489
490                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_REVIVED, o.netname);
491                         Send_Notification(NOTIF_ONE, o, MSG_CENTER, CENTER_FREEZETAG_REVIVE, player.netname);
492                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED, player.netname, o.netname);
493                 }
494
495                 FOREACH_CLIENT(IS_PLAYER(it) && it.reviving, {
496                         it.revive_progress = player.revive_progress;
497                         it.reviving = false;
498                 });
499         }
500         else if(!n && STAT(FROZEN, player) == 1) // only if no teammate is nearby will we reset
501         {
502                 player.revive_progress = bound(0, player.revive_progress - frametime * autocvar_g_freezetag_revive_clearspeed, 1);
503                 player.health = max(1, player.revive_progress * ((warmup_stage) ? warmup_start_health : start_health));
504         }
505         else if(!n && !STAT(FROZEN, player))
506         {
507                 player.revive_progress = 0; // thawing nobody
508         }
509
510         return true;
511 }
512
513 MUTATOR_HOOKFUNCTION(ft, SetStartItems)
514 {
515         start_items &= ~IT_UNLIMITED_AMMO;
516         //start_health       = warmup_start_health       = cvar("g_lms_start_health");
517         //start_armorvalue   = warmup_start_armorvalue   = cvar("g_lms_start_armor");
518         start_ammo_shells  = warmup_start_ammo_shells  = cvar("g_lms_start_ammo_shells");
519         start_ammo_nails   = warmup_start_ammo_nails   = cvar("g_lms_start_ammo_nails");
520         start_ammo_rockets = warmup_start_ammo_rockets = cvar("g_lms_start_ammo_rockets");
521         start_ammo_cells   = warmup_start_ammo_cells   = cvar("g_lms_start_ammo_cells");
522         start_ammo_plasma  = warmup_start_ammo_plasma  = cvar("g_lms_start_ammo_plasma");
523         start_ammo_fuel    = warmup_start_ammo_fuel    = cvar("g_lms_start_ammo_fuel");
524 }
525
526 MUTATOR_HOOKFUNCTION(ft, HavocBot_ChooseRole)
527 {
528         entity bot = M_ARGV(0, entity);
529
530         if (!IS_DEAD(bot))
531         {
532                 if (random() < 0.5)
533                         bot.havocbot_role = havocbot_role_ft_freeing;
534                 else
535                         bot.havocbot_role = havocbot_role_ft_offense;
536         }
537
538         return true;
539 }
540
541 MUTATOR_HOOKFUNCTION(ft, CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
542 {
543         M_ARGV(0, float) = freezetag_teams;
544 }
545
546 MUTATOR_HOOKFUNCTION(ft, SetWeaponArena)
547 {
548         // most weapons arena
549         if(M_ARGV(0, string) == "0" || M_ARGV(0, string) == "")
550                 M_ARGV(0, string) = "most";
551 }
552
553 MUTATOR_HOOKFUNCTION(ft, FragCenterMessage)
554 {
555         entity frag_attacker = M_ARGV(0, entity);
556         entity frag_target = M_ARGV(1, entity);
557         //float frag_deathtype = M_ARGV(2, float);
558         int kill_count_to_attacker = M_ARGV(3, int);
559         int kill_count_to_target = M_ARGV(4, int);
560
561         if(STAT(FROZEN, frag_target))
562                 return; // target was already frozen, so this is just pushing them off the cliff
563
564         Send_Notification(NOTIF_ONE, frag_attacker, MSG_CHOICE, CHOICE_FRAG_FREEZE, frag_target.netname, kill_count_to_attacker, (IS_BOT_CLIENT(frag_target) ? -1 : CS(frag_target).ping));
565         Send_Notification(NOTIF_ONE, frag_target, MSG_CHOICE, CHOICE_FRAGGED_FREEZE, frag_attacker.netname, kill_count_to_target, frag_attacker.health, frag_attacker.armorvalue, (IS_BOT_CLIENT(frag_attacker) ? -1 : CS(frag_attacker).ping));
566
567         return true;
568 }
569
570 void freezetag_Initialize()
571 {
572         freezetag_teams = autocvar_g_freezetag_teams_override;
573         if(freezetag_teams < 2)
574                 freezetag_teams = cvar("g_freezetag_teams"); // read the cvar directly as it gets written earlier in the same frame
575
576         freezetag_teams = BITS(bound(2, freezetag_teams, 4));
577         GameRules_scoring(freezetag_teams, SFL_SORT_PRIO_PRIMARY, SFL_SORT_PRIO_PRIMARY, {
578             field(SP_FREEZETAG_REVIVALS, "revivals", 0);
579         });
580
581         round_handler_Spawn(freezetag_CheckTeams, freezetag_CheckWinner, func_null);
582         round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit);
583
584         EliminatedPlayers_Init(freezetag_isEliminated);
585 }