]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/freezetag/sv_freezetag.qc
Merge branch 'master' into LegendaryGuard/cyber
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / gamemodes / gamemode / freezetag / sv_freezetag.qc
1 #include "sv_freezetag.qh"
2
3 #include <server/elimination.qh>
4 #include <server/resources.qh>
5
6 float autocvar_g_freezetag_frozen_maxtime;
7 float autocvar_g_freezetag_revive_clearspeed;
8 float autocvar_g_freezetag_round_timelimit;
9 //int autocvar_g_freezetag_teams;
10 int autocvar_g_freezetag_teams_override;
11 float autocvar_g_freezetag_warmup;
12
13 float autocvar_g_ft_start_health = 100;
14 float autocvar_g_ft_start_armor = 100;
15 float autocvar_g_ft_start_ammo_shells = 60;
16 float autocvar_g_ft_start_ammo_nails = 320;
17 float autocvar_g_ft_start_ammo_rockets = 160;
18 float autocvar_g_ft_start_ammo_cells = 180;
19 float autocvar_g_ft_start_ammo_plasma = 180;
20 float autocvar_g_ft_start_ammo_fuel = 0;
21
22 void freezetag_count_alive_players()
23 {
24         total_players = 0;
25         for (int i = 1; i <= NUM_TEAMS; ++i)
26         {
27                 Team_SetNumberOfAlivePlayers(Team_GetTeamFromIndex(i), 0);
28         }
29         FOREACH_CLIENT(IS_PLAYER(it) && Entity_HasValidTeam(it),
30         {
31                 ++total_players;
32                 if (GetResource(it, RES_HEALTH) < 1 || STAT(FROZEN, it) == FROZEN_NORMAL)
33                 {
34                         continue;
35                 }
36                 entity team_ = Entity_GetTeam(it);
37                 int num_alive = Team_GetNumberOfAlivePlayers(team_);
38                 ++num_alive;
39                 Team_SetNumberOfAlivePlayers(team_, num_alive);
40         });
41         FOREACH_CLIENT(IS_REAL_CLIENT(it),
42         {
43                 STAT(REDALIVE, it) = Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(1));
44                 STAT(BLUEALIVE, it) = Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(2));
45                 STAT(YELLOWALIVE, it) = Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(3));
46                 STAT(PINKALIVE, it) = Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(4));
47         });
48
49         eliminatedPlayers.SendFlags |= 1;
50 }
51
52 bool freezetag_CheckTeams()
53 {
54         static float prev_missing_teams_mask;
55         if (Team_GetNumberOfAliveTeams() == NumTeams(freezetag_teams))
56         {
57                 if(prev_missing_teams_mask > 0)
58                         Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_TEAMS);
59                 prev_missing_teams_mask = -1;
60                 return true;
61         }
62         if(total_players == 0)
63         {
64                 if(prev_missing_teams_mask > 0)
65                         Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_TEAMS);
66                 prev_missing_teams_mask = -1;
67                 return false;
68         }
69         int missing_teams_mask = 0;
70         for (int i = 1; i <= NUM_TEAMS; ++i)
71         {
72                 if ((freezetag_teams & Team_IndexToBit(i)) &&
73                         (Team_GetNumberOfAlivePlayers(Team_GetTeamFromIndex(i)) == 0))
74                 {
75                         missing_teams_mask |= Team_IndexToBit(i);
76                 }
77         }
78         if(prev_missing_teams_mask != missing_teams_mask)
79         {
80                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_TEAMS, missing_teams_mask);
81                 prev_missing_teams_mask = missing_teams_mask;
82         }
83         return false;
84 }
85
86 void nades_Clear(entity);
87 void nades_GiveBonus(entity player, float score);
88
89 entity freezetag_LastPlayer(float tm)
90 {
91         entity last_pl = NULL;
92         FOREACH_CLIENT(IS_PLAYER(it) && it.team == tm, {
93                 if (STAT(FROZEN, it) != FROZEN_NORMAL && GetResource(it, RES_HEALTH) >= 1)
94                 {
95                         if (!last_pl)
96                                 last_pl = it;
97                         else
98                                 return NULL;
99                 }
100         });
101         return last_pl;
102 }
103
104 bool freezetag_CheckWinner()
105 {
106         if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0)
107         {
108                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_OVER);
109                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_OVER);
110                 Send_Notification(NOTIF_ALL, NULL, MSG_ANNCE, ANNCE_ROUND_OVER);
111                 
112                 FOREACH_CLIENT(IS_PLAYER(it), {
113                         it.freezetag_frozen_timeout = 0;
114                         it.freezetag_revive_time = 0;
115                         nades_Clear(it);
116                 });
117                 if(autocvar_g_freezetag_round_stop)
118                         game_stopped = true;
119                 round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit);
120                 return true;
121         }
122
123         int winner_team = Team_GetWinnerAliveTeam();
124         if (!winner_team)
125                 return false;
126
127         if(winner_team > 0)
128         {
129                 entity last_pl = freezetag_LastPlayer(winner_team);
130                 if(last_pl) {
131                         Give_Medal(last_pl, DEFENSE);
132                 }
133         
134                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, APP_TEAM_NUM(winner_team, CENTER_ROUND_TEAM_SCORES));
135                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(winner_team, INFO_ROUND_TEAM_SCORES));
136                 TeamScore_AddToTeam(winner_team, ST_FT_ROUNDS, +1);
137                 if(fragsleft > 1) AnnounceScores(winner_team);
138         }
139         else if(winner_team == -1)
140         {
141                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_ROUND_TIED);
142                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_ROUND_TIED);
143                 Send_Notification(NOTIF_ALL, NULL, MSG_ANNCE, ANNCE_ROUND_TIED);
144         }
145
146         FOREACH_CLIENT(IS_PLAYER(it), {
147                 it.freezetag_frozen_timeout = 0;
148                 it.freezetag_revive_time = 0;
149                 nades_Clear(it);
150         });
151
152         if(autocvar_g_freezetag_round_stop)
153                 game_stopped = true;
154         round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit);
155         return true;
156 }
157
158 entity freezetag_LastPlayerForTeam(entity this)
159 {
160         entity last_pl = NULL;
161         FOREACH_CLIENT(IS_PLAYER(it) && it != this && SAME_TEAM(it, this), {
162                 if (STAT(FROZEN, it) != FROZEN_NORMAL && GetResource(it, RES_HEALTH) >= 1)
163                 {
164                         if (!last_pl)
165                                 last_pl = it;
166                         else
167                                 return NULL;
168                 }
169         });
170         return last_pl;
171 }
172
173 void freezetag_LastPlayerForTeam_Notify(entity this)
174 {
175         if(round_handler_IsActive())
176         if(round_handler_IsRoundStarted())
177         {
178                 entity pl = freezetag_LastPlayerForTeam(this);
179                 if(pl) {
180                         Send_Notification(NOTIF_ONE, pl, MSG_CENTER, CENTER_ALONE);
181                         Send_Notification(NOTIF_ONE, pl, MSG_ANNCE, ANNCE_ALONE);
182                 }
183         }
184 }
185
186 void freezetag_Add_Score(entity targ, entity attacker)
187 {
188         if(attacker == targ)
189         {
190                 // you froze your own dumb self
191                 // counted as "suicide" already
192                 GameRules_scoring_add(targ, SCORE, -1);
193         }
194         else if(IS_PLAYER(attacker))
195         {
196                 // got frozen by an enemy
197                 // counted as "kill" and "death" already
198                 GameRules_scoring_add(targ, SCORE, -1);
199                 GameRules_scoring_add(attacker, SCORE, +1);
200         }
201         // else nothing - got frozen by the game type rules themselves
202 }
203
204 // to be called when the player is frozen by freezetag (on death, spectator join etc), gives the score
205 void freezetag_Freeze(entity targ, entity attacker)
206 {
207         if(STAT(FROZEN, targ))
208                 return;
209
210         targ.freezetag_frozen_time = time;
211         if (autocvar_g_freezetag_revive_auto && autocvar_g_freezetag_frozen_maxtime > 0)
212                 targ.freezetag_frozen_timeout = time + autocvar_g_freezetag_frozen_maxtime;
213
214         Freeze(targ, 0, FROZEN_NORMAL, true);
215
216         freezetag_count_alive_players();
217
218         freezetag_Add_Score(targ, attacker);
219 }
220
221 bool freezetag_isEliminated(entity e)
222 {
223         if(IS_PLAYER(e) && (STAT(FROZEN, e) == FROZEN_NORMAL || IS_DEAD(e)))
224                 return true;
225         return false;
226 }
227
228
229 // ================
230 // Bot player logic
231 // ================
232
233 void(entity this) havocbot_role_ft_freeing;
234 void(entity this) havocbot_role_ft_offense;
235
236 void havocbot_goalrating_ft_freeplayers(entity this, float ratingscale, vector org, float sradius)
237 {
238         entity best_pl = NULL;
239         float best_dist2 = FLOAT_MAX;
240         FOREACH_CLIENT(IS_PLAYER(it) && it != this && SAME_TEAM(it, this), {
241                 if (STAT(FROZEN, it) == FROZEN_NORMAL)
242                 {
243                         if(vdist(it.origin - org, >, sradius))
244                                 continue;
245                         navigation_routerating(this, it, ratingscale, 2000);
246                 }
247                 else if (best_dist2
248                         && GetResource(it, RES_HEALTH) < GetResource(this, RES_HEALTH) + 30
249                         && vlen2(it.origin - org) < best_dist2)
250                 {
251                         // If teamate is not frozen still seek them out as fight better
252                         // in a group.
253                         best_dist2 = vlen2(it.origin - org);
254                         if (best_dist2 < 700 ** 2)
255                         {
256                                 best_pl = NULL;
257                                 best_dist2 = 0; // already close to a teammate
258                         }
259                         else
260                                 best_pl = it;
261                 }
262         });
263         if (best_pl)
264                 navigation_routerating(this, best_pl, ratingscale / 2, 2000);
265 }
266
267 void havocbot_role_ft_offense(entity this)
268 {
269         if(IS_DEAD(this))
270                 return;
271
272         if (!this.havocbot_role_timeout)
273                 this.havocbot_role_timeout = time + random() * 10 + 20;
274
275         // Count how many players on team are unfrozen.
276         int unfrozen = 0;
277         FOREACH_CLIENT(IS_PLAYER(it) && SAME_TEAM(it, this) && STAT(FROZEN, it) != FROZEN_NORMAL, {
278                 unfrozen++;
279         });
280
281         // If only one left on team or if role has timed out then start trying to free players.
282         if ((!unfrozen && STAT(FROZEN, this) != FROZEN_NORMAL) || time > this.havocbot_role_timeout)
283         {
284                 LOG_TRACE("changing role to freeing");
285                 this.havocbot_role = havocbot_role_ft_freeing;
286                 this.havocbot_role_timeout = 0;
287                 return;
288         }
289
290         if (navigation_goalrating_timeout(this))
291         {
292                 navigation_goalrating_start(this);
293                 havocbot_goalrating_items(this, 12000, this.origin, 10000);
294                 havocbot_goalrating_enemyplayers(this, 10000, this.origin, 10000);
295                 havocbot_goalrating_ft_freeplayers(this, 9000, this.origin, 10000);
296                 havocbot_goalrating_waypoints(this, 1, this.origin, 3000);
297                 navigation_goalrating_end(this);
298
299                 navigation_goalrating_timeout_set(this);
300         }
301 }
302
303 void havocbot_role_ft_freeing(entity this)
304 {
305         if(IS_DEAD(this))
306                 return;
307
308         if (!this.havocbot_role_timeout)
309                 this.havocbot_role_timeout = time + random() * 10 + 20;
310
311         if (time > this.havocbot_role_timeout)
312         {
313                 LOG_TRACE("changing role to offense");
314                 this.havocbot_role = havocbot_role_ft_offense;
315                 this.havocbot_role_timeout = 0;
316                 return;
317         }
318
319         if (navigation_goalrating_timeout(this))
320         {
321                 navigation_goalrating_start(this);
322                 havocbot_goalrating_items(this, 10000, this.origin, 10000);
323                 havocbot_goalrating_enemyplayers(this, 5000, this.origin, 10000);
324                 havocbot_goalrating_ft_freeplayers(this, 20000, this.origin, 10000);
325                 havocbot_goalrating_waypoints(this, 1, this.origin, 3000);
326                 navigation_goalrating_end(this);
327
328                 navigation_goalrating_timeout_set(this);
329         }
330 }
331
332
333 // ==============
334 // Hook Functions
335 // ==============
336
337 void ft_RemovePlayer(entity this)
338 {
339         if (STAT(FROZEN, this) != FROZEN_NORMAL)
340                 freezetag_LastPlayerForTeam_Notify(this);
341         Unfreeze(this, false);
342
343         SetResourceExplicit(this, RES_HEALTH, 0); // neccessary to correctly count alive players
344         freezetag_count_alive_players();
345 }
346
347 MUTATOR_HOOKFUNCTION(ft, ClientDisconnect)
348 {
349         entity player = M_ARGV(0, entity);
350
351         ft_RemovePlayer(player);
352         return true;
353 }
354
355 MUTATOR_HOOKFUNCTION(ft, MakePlayerObserver)
356 {
357         entity player = M_ARGV(0, entity);
358
359         ft_RemovePlayer(player);
360 }
361
362 MUTATOR_HOOKFUNCTION(ft, PlayerDies)
363 {
364         entity frag_attacker = M_ARGV(1, entity);
365         entity frag_target = M_ARGV(2, entity);
366         float frag_deathtype = M_ARGV(3, float);
367
368         if(round_handler_IsActive())
369         if(round_handler_CountdownRunning())
370         {
371                 if (STAT(FROZEN, frag_target) == FROZEN_NORMAL)
372                         Unfreeze(frag_target, true);
373                 freezetag_count_alive_players();
374                 frag_target.respawn_time = time;
375                 frag_target.respawn_flags |= RESPAWN_FORCE;
376                 return true;
377         }
378
379         frag_target.respawn_time = time + 1;
380         frag_target.respawn_flags |= RESPAWN_FORCE;
381
382         // Cases DEATH_TEAMCHANGE and DEATH_AUTOTEAMCHANGE are needed to fix a bug whe
383         // you succeed changing team through the menu: you both really die (gibbing) and get frozen
384         if(ITEM_DAMAGE_NEEDKILL(frag_deathtype)
385                 || frag_deathtype == DEATH_TEAMCHANGE.m_id || frag_deathtype == DEATH_AUTOTEAMCHANGE.m_id)
386         {
387                 // let the player die, he will be automatically frozen when he respawns
388                 if (STAT(FROZEN, frag_target) != FROZEN_NORMAL)
389                 {
390                         freezetag_Add_Score(frag_target, frag_attacker);
391                         freezetag_count_alive_players();
392                         freezetag_LastPlayerForTeam_Notify(frag_target);
393                         frag_target.freezetag_frozen_timeout = -2; // freeze on respawn
394                 }
395                 else
396                 {
397                         float t = frag_target.freezetag_frozen_timeout;
398                         float t2 = frag_target.freezetag_frozen_time;
399                         Unfreeze(frag_target, false); // remove ice
400                         // keep timeout value so it can be restored when player will be refrozen on respawn
401                         // NOTE this can't be exactly -2 since game starts from time 2
402                         frag_target.freezetag_frozen_timeout = -t;
403                         frag_target.freezetag_frozen_time = t2;
404                 }
405                 return true;
406         }
407
408         if (STAT(FROZEN, frag_target) == FROZEN_NORMAL)
409                 return true;
410
411         freezetag_Freeze(frag_target, frag_attacker);
412         freezetag_LastPlayerForTeam_Notify(frag_target);
413
414         if(frag_attacker == frag_target || frag_attacker == NULL)
415         {
416                 if(IS_PLAYER(frag_target))
417                         Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_FREEZETAG_SELF);
418                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_SELF, frag_target.netname);
419         }
420         else
421         {
422                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_FREEZE, frag_target.netname, frag_attacker.netname);
423         }
424
425         return true;
426 }
427
428 MUTATOR_HOOKFUNCTION(ft, PlayerSpawn)
429 {
430         entity player = M_ARGV(0, entity);
431
432         if(player.freezetag_frozen_timeout == -1) // if PlayerSpawn is called by reset_map_players
433                 return true; // do nothing, round is starting right now
434
435         if(player.freezetag_frozen_timeout <= -2) // player was dead
436         {
437                 float t = player.freezetag_frozen_timeout;
438                 float t2 = player.freezetag_frozen_time;
439                 freezetag_Freeze(player, NULL);
440                 if (t < -2)
441                 {
442                         player.freezetag_frozen_timeout = -t;
443                         player.freezetag_frozen_time = t2;
444                 }
445                 return true;
446         }
447
448         freezetag_count_alive_players();
449
450         if(round_handler_IsActive())
451         if(round_handler_IsRoundStarted())
452         {
453                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_SPAWN_LATE);
454                 freezetag_Freeze(player, NULL);
455         }
456
457         return true;
458 }
459
460 MUTATOR_HOOKFUNCTION(ft, reset_map_players)
461 {
462         FOREACH_CLIENT(IS_PLAYER(it), {
463                 CS(it).killcount = 0;
464                 
465                 if(autocvar_g_freezetag_round_respawn) {
466                         it.freezetag_frozen_timeout = -1;
467                         PutClientInServer(it);
468                 } else {
469                         ResetPlayerResources(it);
470                 }
471                 
472                 it.freezetag_frozen_timeout = 0;
473         });
474         freezetag_count_alive_players();
475         return true;
476 }
477
478 MUTATOR_HOOKFUNCTION(ft, GiveFragsForKill, CBC_ORDER_FIRST)
479 {
480         M_ARGV(2, float) = 0; // no frags counted in Freeze Tag
481         return true;
482 }
483
484 MUTATOR_HOOKFUNCTION(ft, Unfreeze)
485 {
486         entity targ = M_ARGV(0, entity);
487         targ.freezetag_frozen_time = 0;
488         targ.freezetag_frozen_timeout = 0;
489 }
490
491 MUTATOR_HOOKFUNCTION(ft, Damage_Calculate)
492 {
493         entity frag_attacker = M_ARGV(1, entity);
494         entity frag_target = M_ARGV(2, entity);
495         //float frag_deathtype = M_ARGV(3, float);
496         //float frag_damage = M_ARGV(4, float);
497         vector frag_force = M_ARGV(6, vector);
498
499         if (STAT(FROZEN, frag_target) == FROZEN_NORMAL && autocvar_g_freezetag_revive_auto_reducible
500                 && autocvar_g_freezetag_frozen_maxtime > 0 && autocvar_g_freezetag_revive_auto)
501         {
502                 float t = 0;
503                 if ((autocvar_g_freezetag_revive_auto_reducible < 0 || DIFF_TEAM(frag_attacker, frag_target))
504                         && frag_target.freezetag_frozen_timeout > time)
505                 {
506                         if (fabs(autocvar_g_freezetag_revive_auto_reducible) == 1)
507                         {
508                                 float maxforce = autocvar_g_freezetag_revive_auto_reducible_maxforce;
509                                 t = vlen(frag_force);
510                                 // limit hit force considered at once, e.g when you have the Strength
511                                 // powerup but also with weapons that fire multiple projectiles at once (crylink)
512                                 if (frag_target.freezetag_frozen_force + t > maxforce)
513                                 {
514                                         t = max(0, maxforce - frag_target.freezetag_frozen_force);
515                                         frag_target.freezetag_frozen_force = maxforce;
516                                 }
517                                 else
518                                         frag_target.freezetag_frozen_force += t;
519                                 t *= autocvar_g_freezetag_revive_auto_reducible_forcefactor;
520                         }
521                         frag_target.freezetag_frozen_timeout -= t;
522                         if (frag_target.freezetag_frozen_timeout < time)
523                                 frag_target.freezetag_frozen_timeout = time;
524                 }
525         }
526 }
527
528 #ifdef IN_REVIVING_RANGE
529         #undef IN_REVIVING_RANGE
530 #endif
531
532 #define IN_REVIVING_RANGE(player, it, revive_extra_size) \
533         (it != player && !IS_DEAD(it) && SAME_TEAM(it, player) \
534         && boxesoverlap(player.absmin - revive_extra_size, player.absmax + revive_extra_size, it.absmin, it.absmax))
535
536 MUTATOR_HOOKFUNCTION(ft, PlayerPreThink, CBC_ORDER_FIRST)
537 {
538         if(game_stopped)
539                 return true;
540
541         if(round_handler_IsActive())
542         if(!round_handler_IsRoundStarted())
543                 return true;
544
545         entity player = M_ARGV(0, entity);
546         //if (STAT(FROZEN, player) == FROZEN_NORMAL)
547         //if(player.freezetag_frozen_timeout > 0 && time < player.freezetag_frozen_timeout)
548                 //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);
549
550         player.freezetag_frozen_force = 0;
551
552         if (!(frametime && IS_PLAYER(player)))
553                 return true;
554
555         entity revivers_last = NULL;
556         entity revivers_first = NULL;
557
558         bool player_is_reviving = false;
559         int n = 0;
560         vector revive_extra_size = '1 1 1' * autocvar_g_freezetag_revive_extra_size;
561         FOREACH_CLIENT(IS_PLAYER(it), {
562                 // check if player is reviving anyone
563                 if (STAT(FROZEN, it) == FROZEN_NORMAL)
564                 {
565                         if ((STAT(FROZEN, player) == FROZEN_NORMAL))
566                                 continue;
567                         if (!IN_REVIVING_RANGE(player, it, revive_extra_size))
568                                 continue;
569                         player_is_reviving = true;
570                         break;
571                 }
572
573                 if (!(STAT(FROZEN, player) == FROZEN_NORMAL))
574                         continue; // both player and it are NOT frozen
575                 if (!IN_REVIVING_RANGE(player, it, revive_extra_size))
576                         continue;
577
578                 // found a teammate that is reviving player
579                 if (autocvar_g_freezetag_revive_time_to_score > 0 && STAT(FROZEN, player) == FROZEN_NORMAL)
580                 {
581                         it.freezetag_revive_time += frametime / autocvar_g_freezetag_revive_time_to_score;
582                         while (it.freezetag_revive_time > 1)
583                         {
584                                 GameRules_scoring_add(it, SCORE, +1);
585                                 it.freezetag_revive_time -= 1;
586                         }
587                 }
588                 if (revivers_last)
589                         revivers_last.chain = it;
590                 revivers_last = it;
591                 if (!revivers_first)
592                         revivers_first = it;
593                 ++n;
594         });
595         if (revivers_last)
596                 revivers_last.chain = NULL;
597
598         // allow normal revival during automatic revival
599         // (if we wouldn't allow it then freezetag_frozen_timeout should be checked too in the previous loop)
600         //if (STAT(FROZEN, player) == FROZEN_NORMAL) // redundant check
601         if (!n && player.freezetag_frozen_timeout > 0 && time >= player.freezetag_frozen_timeout)
602                 n = -1;
603
604         float base_progress = 0;
605         if  (STAT(FROZEN, player) == FROZEN_NORMAL && autocvar_g_freezetag_revive_auto
606                 && autocvar_g_freezetag_frozen_maxtime > 0 && autocvar_g_freezetag_revive_auto_progress)
607         {
608                 // NOTE if auto-revival is in progress, manual revive speed is reduced so that it always takes the same amount of time
609                 base_progress = bound(0, (1 - (player.freezetag_frozen_timeout - time) / autocvar_g_freezetag_frozen_maxtime), 1);
610         }
611
612         if (!n) // no teammate nearby
613         {
614                 float clearspeed = autocvar_g_freezetag_revive_clearspeed;
615                 if (STAT(FROZEN, player) == FROZEN_NORMAL)
616                 {
617                         if (autocvar_g_freezetag_revive_time_to_score > 0)
618                         {
619                                 if (STAT(REVIVE_PROGRESS, player) > base_progress)
620                                 {
621                                         // reduce auto-revival time based on manual revival progress
622                                         base_progress = STAT(REVIVE_PROGRESS, player);
623                                         player.freezetag_frozen_timeout = time + autocvar_g_freezetag_frozen_maxtime * (1 - STAT(REVIVE_PROGRESS, player));
624                                 }
625                                 // don't clear revive progress, it would allow stacking points
626                                 // by entering and exiting the revival zone many times
627                                 STAT(REVIVE_PROGRESS, player) = base_progress;
628                         }
629                         else
630                                 STAT(REVIVE_PROGRESS, player) = bound(base_progress, STAT(REVIVE_PROGRESS, player) - frametime * clearspeed * (1 - base_progress), 1);
631                 }
632                 else if (!STAT(FROZEN, player) && !player_is_reviving)
633                         STAT(REVIVE_PROGRESS, player) = base_progress; // thawing nobody
634         }
635         else if (STAT(FROZEN, player) == FROZEN_NORMAL) // OK, there is at least one teammate reviving us
636         {
637                 float spd = autocvar_g_freezetag_revive_speed_t2s;
638                 if (autocvar_g_freezetag_revive_time_to_score <= 0)
639                         spd = autocvar_g_freezetag_revive_speed * (1 - base_progress);
640                 STAT(REVIVE_PROGRESS, player) = bound(base_progress, STAT(REVIVE_PROGRESS, player) + frametime * max(1/60, spd), 1);
641
642                 if(STAT(REVIVE_PROGRESS, player) >= 1)
643                 {
644                         float frozen_time = time - player.freezetag_frozen_time;
645                         Unfreeze(player, false);
646                         SetResourceExplicit(player, RES_HEALTH, ((warmup_stage) ? warmup_start_health : start_health));
647                         player.spawnshieldtime = time + autocvar_g_freezetag_revive_spawnshield;
648                         freezetag_count_alive_players();
649
650                         if(n == -1)
651                         {
652                                 if(autocvar_sv_eventlog)
653                                         GameLogEcho(strcat(":ft:autorevival:", ftos(player.playerid)));
654                                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_AUTO_REVIVED, frozen_time);
655                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_AUTO_REVIVED, player.netname, frozen_time);
656                                 return true;
657                         }
658
659                         // EVERY team mate nearby gets a point (even if multiple!)
660                         for(entity it = revivers_first; it; it = it.chain)
661                         {
662                                 GameRules_scoring_add(it, FREEZETAG_REVIVALS, +1);
663                                 if (autocvar_g_freezetag_revive_time_to_score <= 0)
664                                         GameRules_scoring_add(it, SCORE, +1);
665                                 nades_GiveBonus(it, autocvar_g_nades_bonus_score_low);
666                         }
667
668                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_REVIVED, revivers_first.netname);
669                         Send_Notification(NOTIF_ONE, revivers_first, MSG_CENTER, CENTER_FREEZETAG_REVIVE, player.netname);
670                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED, player.netname, revivers_first.netname);
671                         Give_Medal(revivers_first, ASSIST);
672                         if(autocvar_sv_eventlog)
673                         {
674                                 string revivers = "";
675                                 for(entity it = revivers_first; it; it = it.chain)
676                                         revivers = strcat(revivers, ftos(it.playerid), ",");
677                                 revivers = substring(revivers, 0, strlen(revivers) - 1);
678                                 GameLogEcho(strcat(":ft:revival:", ftos(player.playerid), ":", revivers));
679                         }
680                 }
681
682                 for(entity it = revivers_first; it; it = it.chain)
683                         STAT(REVIVE_PROGRESS, it) = STAT(REVIVE_PROGRESS, player);
684         }
685
686         if (STAT(FROZEN, player) == FROZEN_NORMAL)
687         {
688                 entity player_wp = player.waypointsprite_attached;
689                 if (n > 0 || (n == 0 && STAT(REVIVE_PROGRESS, player) > 0.95))
690                 {
691                         WaypointSprite_UpdateSprites(player_wp, WP_Reviving, WP_Null, WP_Null);
692                         WaypointSprite_UpdateTeamRadar(player_wp, RADARICON_WAYPOINT, WP_REVIVING_COLOR);
693                 }
694                 else
695                 {
696                         WaypointSprite_UpdateSprites(player_wp, WP_Frozen, WP_Null, WP_Null);
697                         WaypointSprite_UpdateTeamRadar(player_wp, RADARICON_WAYPOINT, WP_FROZEN_COLOR);
698                 }
699
700                 WaypointSprite_UpdateMaxHealth(player_wp, 1);
701                 WaypointSprite_UpdateHealth(player_wp, STAT(REVIVE_PROGRESS, player));
702         }
703
704         return true;
705 }
706
707 MUTATOR_HOOKFUNCTION(ft, SetStartItems)
708 {
709         start_items &= ~(IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS);
710         if(!cvar("g_use_ammunition"))
711                 start_items |= IT_UNLIMITED_AMMO;
712
713         start_health       = warmup_start_health       = autocvar_g_ft_start_health;
714         start_armorvalue   = warmup_start_armorvalue   = autocvar_g_ft_start_armor;
715         start_ammo_shells  = warmup_start_ammo_shells  = autocvar_g_ft_start_ammo_shells;
716         start_ammo_nails   = warmup_start_ammo_nails   = autocvar_g_ft_start_ammo_nails;
717         start_ammo_rockets = warmup_start_ammo_rockets = autocvar_g_ft_start_ammo_rockets;
718         start_ammo_cells   = warmup_start_ammo_cells   = autocvar_g_ft_start_ammo_cells;
719         start_ammo_plasma  = warmup_start_ammo_plasma  = autocvar_g_ft_start_ammo_plasma;
720         start_ammo_fuel    = warmup_start_ammo_fuel    = autocvar_g_ft_start_ammo_fuel;
721 }
722
723 MUTATOR_HOOKFUNCTION(ft, HavocBot_ChooseRole)
724 {
725         entity bot = M_ARGV(0, entity);
726
727         if (!IS_DEAD(bot))
728         {
729                 if (random() < 0.5)
730                         bot.havocbot_role = havocbot_role_ft_freeing;
731                 else
732                         bot.havocbot_role = havocbot_role_ft_offense;
733         }
734
735         // if bots spawn all at once assign them a more appropriated role after a while
736         if (time < CS(bot).jointime + 1)
737                 bot.havocbot_role_timeout = time + 10 + random() * 10;
738
739         return true;
740 }
741
742 MUTATOR_HOOKFUNCTION(ft, TeamBalance_CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
743 {
744         M_ARGV(0, float) = freezetag_teams;
745         return true;
746 }
747
748 MUTATOR_HOOKFUNCTION(ft, SetWeaponArena)
749 {
750         if(M_ARGV(0, string) == "0" || M_ARGV(0, string) == "")
751                 M_ARGV(0, string) = autocvar_g_freezetag_weaponarena;
752 }
753
754 MUTATOR_HOOKFUNCTION(ft, FragCenterMessage)
755 {
756         entity frag_attacker = M_ARGV(0, entity);
757         entity frag_target = M_ARGV(1, entity);
758         //float frag_deathtype = M_ARGV(2, float);
759         int kill_count_to_attacker = M_ARGV(3, int);
760         int kill_count_to_target = M_ARGV(4, int);
761
762         if(STAT(FROZEN, frag_target) == FROZEN_NORMAL)
763                 return; // target was already frozen, so this is just pushing them off the cliff
764
765         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));
766         Send_Notification(NOTIF_ONE, frag_target, MSG_CHOICE, CHOICE_FRAGGED_FREEZE, frag_attacker.netname, kill_count_to_target,
767                 GetResource(frag_attacker, RES_HEALTH), GetResource(frag_attacker, RES_ARMOR), (IS_BOT_CLIENT(frag_attacker) ? -1 : CS(frag_attacker).ping));
768
769         return true;
770 }
771
772 MUTATOR_HOOKFUNCTION(ft, SV_ParseServerCommand)
773 {
774         string cmd_name = M_ARGV(0, string);
775         if (cmd_name == "shuffleteams")
776                 shuffleteams_on_reset_map = !(round_handler_IsActive() && !round_handler_IsRoundStarted());
777         return false;
778 }
779
780 MUTATOR_HOOKFUNCTION(ft, Scores_CountFragsRemaining)
781 {
782         // announce remaining frags
783         return true;
784 }
785
786 void freezetag_Initialize()
787 {
788         freezetag_teams = autocvar_g_freezetag_teams_override;
789         if(freezetag_teams < 2)
790                 freezetag_teams = cvar("g_freezetag_teams"); // read the cvar directly as it gets written earlier in the same frame
791
792         freezetag_teams = BITS(bound(2, freezetag_teams, 4));
793         GameRules_scoring(freezetag_teams, SFL_SORT_PRIO_PRIMARY, 0, {
794                 field_team(ST_FT_ROUNDS, "rounds", SFL_SORT_PRIO_PRIMARY);
795                 field(SP_FREEZETAG_REVIVALS, "revivals", 0);
796         });
797
798         round_handler_Spawn(freezetag_CheckTeams, freezetag_CheckWinner, func_null);
799         round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit);
800
801         EliminatedPlayers_Init(freezetag_isEliminated);
802 }