]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/common/mutators/mutator/nades/nades.qc
Merge branch 'master' into terencehill/welcome_dialog_translatable
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / mutators / mutator / nades / nades.qc
index 510fcf92e19345ca2d371d574f598af356a849df..5ea4cb49cfb9a4d4deb6802d8bf8e82d1cad3491 100644 (file)
@@ -1,6 +1,7 @@
 #include "nades.qh"
 
 #include "../overkill/okmachinegun.qh"
+#include "../overkill/okshotgun.qh"
 
 #ifdef SVQC
 bool autocvar_g_nades_nade_small;
@@ -10,6 +11,7 @@ float autocvar_g_nades_spread = 0.04;
 REGISTER_STAT(NADES_SMALL, int, autocvar_g_nades_nade_small)
 
 #ifdef GAMEQC
+
 REPLICATE(cvar_cl_nade_type, int, "cl_nade_type");
 REPLICATE(cvar_cl_pokenade_type, string, "cl_pokenade_type");
 
@@ -115,6 +117,17 @@ MUTATOR_HOOKFUNCTION(cl_nades, EditProjectile)
        else
                proj.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY;
 }
+
+MUTATOR_HOOKFUNCTION(cl_nades, BuildGameplayTipsString)
+{
+       if (mut_is_active(MUT_NADES))
+       {
+               string key = getcommandkey(_("drop weapon / throw nade"), "dropweapon");
+               M_ARGV(0, string) = strcat(M_ARGV(0, string),
+                       "\n\n", sprintf(_("^3nades^8 are enabled, press ^3%s^8 to use them"), key), "\n");
+       }
+}
+
 bool Projectile_isnade(int p)
 {
        return Nade_FromProjectile(p) != NADE_TYPE_Null;
@@ -124,7 +137,7 @@ void DrawAmmoNades(vector myPos, vector mySize, bool draw_expanding, float expan
        float bonusNades    = STAT(NADE_BONUS);
        float bonusProgress = STAT(NADE_BONUS_SCORE);
        float bonusType     = STAT(NADE_BONUS_TYPE);
-       Nade def = Nades_from(bonusType);
+       Nade def = REGISTRY_GET(Nades, bonusType);
        vector nadeColor    = def.m_color;
        string nadeIcon     = def.m_icon;
 
@@ -162,8 +175,6 @@ void DrawAmmoNades(vector myPos, vector mySize, bool draw_expanding, float expan
 #include <common/monsters/sv_spawn.qh>
 #include <common/monsters/sv_monsters.qh>
 
-REGISTER_MUTATOR(nades, autocvar_g_nades);
-
 .float nade_time_primed;
 .float nade_lifetime;
 
@@ -180,7 +191,7 @@ void nade_timer_think(entity this)
 
 void nade_burn_spawn(entity _nade)
 {
-       CSQCProjectile(_nade, true, Nades_from(STAT(NADE_BONUS_TYPE, _nade)).m_projectile[true], true);
+       CSQCProjectile(_nade, true, REGISTRY_GET(Nades, STAT(NADE_BONUS_TYPE, _nade)).m_projectile[true], true);
 }
 
 void nade_spawn(entity _nade)
@@ -198,7 +209,7 @@ void nade_spawn(entity _nade)
 
        _nade.effects |= EF_LOWPRECISION;
 
-       CSQCProjectile(_nade, true, Nades_from(STAT(NADE_BONUS_TYPE, _nade)).m_projectile[false], true);
+       CSQCProjectile(_nade, true, REGISTRY_GET(Nades, STAT(NADE_BONUS_TYPE, _nade)).m_projectile[false], true);
 }
 
 void napalm_damage(entity this, float dist, float damage, float edgedamage, float burntime)
@@ -225,7 +236,7 @@ void napalm_damage(entity this, float dist, float damage, float edgedamage, floa
                        if(d < dist)
                        {
                                e.fireball_impactvec = p;
-                               RandomSelection_AddEnt(e, 1 / (1 + d), !Fire_IsBurning(e));
+                               RandomSelection_AddEnt(e, 1 / (1 + d), !StatusEffects_active(STATUSEFFECT_Burning, e));
                        }
                }
        if(RandomSelection_chosen_ent)
@@ -354,13 +365,10 @@ void napalm_fountain_think(entity this)
 
 void nade_napalm_boom(entity this)
 {
-       entity fountain;
-       int c;
-       for (c = 0; c < autocvar_g_nades_napalm_ball_count; c++)
+       for (int c = 0; c < autocvar_g_nades_napalm_ball_count; c++)
                nade_napalm_ball(this);
 
-
-       fountain = spawn();
+       entity fountain = new(nade_napalm_fountain);
        fountain.owner = this.owner;
        fountain.realowner = this.realowner;
        fountain.origin = this.origin;
@@ -454,8 +462,7 @@ void nade_ice_think(entity this)
 
 void nade_ice_boom(entity this)
 {
-       entity fountain;
-       fountain = spawn();
+       entity fountain = new(nade_ice_fountain);
        fountain.owner = this.owner;
        fountain.realowner = this.realowner;
        fountain.origin = this.origin;
@@ -508,22 +515,20 @@ void nade_translocate_boom(entity this)
 
 void nade_spawn_boom(entity this)
 {
-       entity spawnloc = spawn();
+       entity player = this.realowner;
+       entity spawnloc = new(nade_spawn_loc);
        setorigin(spawnloc, this.origin);
-       setsize(spawnloc, this.realowner.mins, this.realowner.maxs);
+       setsize(spawnloc, player.mins, player.maxs);
        set_movetype(spawnloc, MOVETYPE_NONE);
        spawnloc.solid = SOLID_NOT;
-       spawnloc.drawonlytoclient = this.realowner;
+       spawnloc.drawonlytoclient = player;
        spawnloc.effects = EF_STARDUST;
        spawnloc.cnt = autocvar_g_nades_spawn_count;
 
-       if(this.realowner.nade_spawnloc)
-       {
-               delete(this.realowner.nade_spawnloc);
-               this.realowner.nade_spawnloc = NULL;
-       }
+       if(player.nade_spawnloc)
+               delete(player.nade_spawnloc);
 
-       this.realowner.nade_spawnloc = spawnloc;
+       player.nade_spawnloc = spawnloc;
 }
 
 void nades_orb_think(entity this)
@@ -550,7 +555,7 @@ entity nades_spawn_orb(entity own, entity realown, vector org, float orb_ltime,
        // NOTE: this function merely places an orb
        // you must add a custom touch function to the returned entity if desired
        // also set .colormod if you wish to have it colorized
-       entity orb = spawn(); // Net_LinkEntity sets the classname (TODO)
+       entity orb = new(nades_spawn_orb);
        orb.owner = own;
        orb.realowner = realown;
        setorigin(orb, org);
@@ -596,9 +601,9 @@ void nade_entrap_touch(entity this, entity toucher)
        #endif
        }
 
-       if ( IS_REAL_CLIENT(toucher) || IS_VEHICLE(toucher) || IS_MONSTER(toucher) )
+       if ( IS_REAL_CLIENT(toucher) || (IS_VEHICLE(toucher) && toucher.owner) )
        {
-               entity show_tint = (IS_VEHICLE(toucher)) ? toucher.owner : toucher;
+               entity show_tint = (IS_VEHICLE(toucher) && toucher.owner) ? toucher.owner : toucher;
                STAT(ENTRAP_ORB, show_tint) = time + 0.1;
 
                float tint_alpha = 0.75;
@@ -652,9 +657,9 @@ void nade_heal_touch(entity this, entity toucher)
 
        }
 
-       if ( IS_REAL_CLIENT(toucher) || IS_VEHICLE(toucher) )
+       if ( IS_REAL_CLIENT(toucher) || (IS_VEHICLE(toucher) && toucher.owner) )
        {
-               entity show_red = (IS_VEHICLE(toucher)) ? toucher.owner : toucher;
+               entity show_red = (IS_VEHICLE(toucher) && toucher.owner) ? toucher.owner : toucher;
                STAT(HEALING_ORB, show_red) = time+0.1;
                STAT(HEALING_ORB_ALPHA, show_red) = 0.75 * (this.ltime - time) / this.orb_lifetime;
        }
@@ -670,7 +675,9 @@ void nade_heal_boom(entity this)
 
 void nade_monster_boom(entity this)
 {
-       entity e = spawnmonster(spawn(), this.pokenade_type, 0, this.realowner, this.realowner, this.origin, false, false, 1);
+       entity e = spawn();
+       e.noalign = true; // don't drop to floor
+       e = spawnmonster(e, this.pokenade_type, MON_Null, this.realowner, this.realowner, this.origin, false, false, 1);
 
        if(autocvar_g_nades_pokenade_monster_lifetime > 0)
                e.monster_lifetime = time + autocvar_g_nades_pokenade_monster_lifetime;
@@ -679,9 +686,9 @@ void nade_monster_boom(entity this)
 
 void nade_veil_touch(entity this, entity toucher)
 {
-       if ( IS_REAL_CLIENT(toucher) || IS_VEHICLE(toucher) || IS_MONSTER(toucher) )
+       if ( IS_REAL_CLIENT(toucher) || (IS_VEHICLE(toucher) && toucher.owner) )
        {
-               entity show_tint = (IS_VEHICLE(toucher)) ? toucher.owner : toucher;
+               entity show_tint = (IS_VEHICLE(toucher) && toucher.owner) ? toucher.owner : toucher;
 
                float tint_alpha = 0.75;
                if(SAME_TEAM(toucher, this.realowner))
@@ -711,7 +718,7 @@ void nade_boom(entity this)
        entity expef = NULL;
        bool nade_blast = true;
 
-       switch ( Nades_from(STAT(NADE_BONUS_TYPE, this)) )
+       switch ( REGISTRY_GET(Nades, STAT(NADE_BONUS_TYPE, this)) )
        {
                case NADE_TYPE_NAPALM:
                        nade_blast = autocvar_g_nades_napalm_blast;
@@ -773,7 +780,7 @@ void nade_boom(entity this)
        }
 
        if(this.takedamage)
-       switch ( Nades_from(STAT(NADE_BONUS_TYPE, this)) )
+       switch ( REGISTRY_GET(Nades, STAT(NADE_BONUS_TYPE, this)) )
        {
                case NADE_TYPE_NAPALM: nade_napalm_boom(this); break;
                case NADE_TYPE_ICE: nade_ice_boom(this); break;
@@ -892,7 +899,7 @@ void nade_damage(entity this, entity inflictor, entity attacker, float damage, i
        }
        else if(DEATH_ISWEAPON(deathtype, WEP_MACHINEGUN) || DEATH_ISWEAPON(deathtype, WEP_OVERKILL_MACHINEGUN))
                damage = this.max_health * 0.1;
-       else if(DEATH_ISWEAPON(deathtype, WEP_SHOCKWAVE) || DEATH_ISWEAPON(deathtype, WEP_SHOTGUN)) // WEAPONTODO
+       else if(DEATH_ISWEAPON(deathtype, WEP_SHOCKWAVE) || DEATH_ISWEAPON(deathtype, WEP_SHOTGUN) || DEATH_ISWEAPON(deathtype, WEP_OVERKILL_SHOTGUN)) // WEAPONTODO
        {
                if(!(deathtype & HITTYPE_SECONDARY))
                        damage = this.max_health * 1.15;
@@ -941,7 +948,8 @@ void toss_nade(entity e, bool set_owner, vector _velocity, float _time)
        entity _nade = e.nade;
        e.nade = NULL;
 
-       delete(e.fake_nade);
+       if(e.fake_nade)
+               delete(e.fake_nade);
        e.fake_nade = NULL;
 
        Kill_Notification(NOTIF_ONE_ONLY, e, MSG_CENTER, CPID_NADES);
@@ -1071,7 +1079,10 @@ bool nade_customize(entity this, entity client)
        {
                //this.effects = EF_ADDITIVE | EF_FULLBRIGHT | EF_LOWPRECISION;
                if(!this.traileffectnum)
-                       this.traileffectnum = _particleeffectnum(Nade_TrailEffect(Nades_from(STAT(NADE_BONUS_TYPE, this)).m_projectile[false], this.team).eent_eff_name);
+               {
+                       entity nade = REGISTRY_GET(Nades, STAT(NADE_BONUS_TYPE, this));
+                       this.traileffectnum = _particleeffectnum(Nade_TrailEffect(nade.m_projectile[false], this.team).eent_eff_name);
+               }
                this.alpha = 1;
        }
 
@@ -1085,7 +1096,7 @@ void spawn_held_nade(entity player, entity nowner, float ntime, int ntype, strin
        STAT(NADE_BONUS_TYPE, n) = max(1, ntype);
        n.pokenade_type = pntype;
 
-       if(Nades_from(STAT(NADE_BONUS_TYPE, n)) == NADE_TYPE_Null)
+       if(REGISTRY_GET(Nades, STAT(NADE_BONUS_TYPE, n)) == NADE_TYPE_Null)
                STAT(NADE_BONUS_TYPE, n) = NADE_TYPE_NORMAL.m_id;
 
        .entity weaponentity = weaponentities[0]; // TODO: unhardcode
@@ -1094,8 +1105,8 @@ void spawn_held_nade(entity player, entity nowner, float ntime, int ntype, strin
        //setattachment(n, player, "bip01 l hand");
        n.exteriormodeltoclient = player;
        setcefc(n, nade_customize);
-       n.traileffectnum = _particleeffectnum(Nade_TrailEffect(Nades_from(STAT(NADE_BONUS_TYPE, n)).m_projectile[false], player.team).eent_eff_name);
-       n.colormod = Nades_from(STAT(NADE_BONUS_TYPE, n)).m_color;
+       n.traileffectnum = _particleeffectnum(Nade_TrailEffect(REGISTRY_GET(Nades, STAT(NADE_BONUS_TYPE, n)).m_projectile[false], player.team).eent_eff_name);
+       n.colormod = REGISTRY_GET(Nades, STAT(NADE_BONUS_TYPE, n)).m_color;
        n.realowner = nowner;
        n.colormap = player.colormap;
        n.glowmod = player.glowmod;
@@ -1106,19 +1117,19 @@ void spawn_held_nade(entity player, entity nowner, float ntime, int ntype, strin
        n.projectiledeathtype = DEATH_NADE.m_id;
        n.weaponentity_fld = weaponentity;
        n.nade_lifetime = ntime;
-       n.alpha = Nades_from(STAT(NADE_BONUS_TYPE, n)).m_alpha;
+       n.alpha = REGISTRY_GET(Nades, STAT(NADE_BONUS_TYPE, n)).m_alpha;
 
        setmodel(fn, MDL_NADE_VIEW);
        //setattachment(fn, player.(weaponentity), "");
        fn.viewmodelforclient = player;
        fn.realowner = fn.owner = player;
-       fn.colormod = Nades_from(STAT(NADE_BONUS_TYPE, n)).m_color;
+       fn.colormod = REGISTRY_GET(Nades, STAT(NADE_BONUS_TYPE, n)).m_color;
        fn.colormap = player.colormap;
        fn.glowmod = player.glowmod;
        setthink(fn, SUB_Remove);
        fn.nextthink = n.wait;
        fn.weaponentity_fld = weaponentity;
-       fn.alpha = Nades_from(STAT(NADE_BONUS_TYPE, n)).m_alpha;
+       fn.alpha = REGISTRY_GET(Nades, STAT(NADE_BONUS_TYPE, n)).m_alpha;
 
        player.nade = n;
        player.fake_nade = fn;
@@ -1126,20 +1137,22 @@ void spawn_held_nade(entity player, entity nowner, float ntime, int ntype, strin
 
 void nade_prime(entity this)
 {
-       if(autocvar_g_nades_bonus_only)
-       if(!STAT(NADE_BONUS, this))
+       if(autocvar_g_nades_bonus_only && !STAT(NADE_BONUS, this))
                return; // only allow bonus nades
 
+       // TODO: handle old nade if it exists?
        if(this.nade)
                delete(this.nade);
+       this.nade = NULL;
 
        if(this.fake_nade)
                delete(this.fake_nade);
+       this.fake_nade = NULL;
 
        int ntype;
        string pntype = this.pokenade_type;
 
-       if((this.items & ITEM_Strength.m_itemid) && autocvar_g_nades_bonus_onstrength)
+       if(StatusEffects_active(STATUSEFFECT_Strength, this) && autocvar_g_nades_bonus_onstrength)
                ntype = STAT(NADE_BONUS_TYPE, this);
        else if (STAT(NADE_BONUS, this) >= 1)
        {
@@ -1149,8 +1162,8 @@ void nade_prime(entity this)
        }
        else
        {
-               ntype = ((autocvar_g_nades_client_select) ? CS(this).cvar_cl_nade_type : autocvar_g_nades_nade_type);
-               pntype = ((autocvar_g_nades_client_select) ? CS(this).cvar_cl_pokenade_type : autocvar_g_nades_pokenade_monster_type);
+               ntype = ((autocvar_g_nades_client_select) ? CS_CVAR(this).cvar_cl_nade_type : autocvar_g_nades_nade_type);
+               pntype = ((autocvar_g_nades_client_select) ? CS_CVAR(this).cvar_cl_pokenade_type : autocvar_g_nades_pokenade_monster_type);
        }
 
        spawn_held_nade(this, this, autocvar_g_nades_nade_lifetime, ntype, pntype);
@@ -1158,22 +1171,7 @@ void nade_prime(entity this)
 
 bool CanThrowNade(entity this)
 {
-       if(this.vehicle)
-               return false;
-
-       if(IS_DEAD(this))
-               return false;
-
-       if (!autocvar_g_nades)
-               return false; // allow turning them off mid match
-
-       if (weaponLocked(this))
-               return false;
-
-       if (!IS_PLAYER(this))
-               return false;
-
-       return true;
+       return !(this.vehicle || !autocvar_g_nades || IS_DEAD(this) || !IS_PLAYER(this) || weaponLocked(this));
 }
 
 .bool nade_altbutton;
@@ -1202,7 +1200,7 @@ void nades_CheckThrow(entity this)
                        _force /= autocvar_g_nades_nade_lifetime;
                        _force = autocvar_g_nades_nade_minforce + (_force * (autocvar_g_nades_nade_maxforce - autocvar_g_nades_nade_minforce));
                        vector dir = (v_forward * 0.75 + v_up * 0.2 + v_right * 0.05);
-                       dir = W_CalculateSpread(dir, autocvar_g_nades_spread, g_weaponspreadfactor, autocvar_g_projectiles_spread_style);
+                       dir = W_CalculateSpread(dir, autocvar_g_nades_spread, autocvar_g_weaponspreadfactor, autocvar_g_projectiles_spread_style);
                        toss_nade(this, true, dir * _force, 0);
                }
        }
@@ -1246,14 +1244,21 @@ CLASS(NadeOffhand, OffhandWeapon)
                                _force /= autocvar_g_nades_nade_lifetime;
                                _force = autocvar_g_nades_nade_minforce + (_force * (autocvar_g_nades_nade_maxforce - autocvar_g_nades_nade_minforce));
                                vector dir = (v_forward * 0.7 + v_up * 0.2 + v_right * 0.1);
-                               dir = W_CalculateSpread(dir, autocvar_g_nades_spread, g_weaponspreadfactor, autocvar_g_projectiles_spread_style);
+                               dir = W_CalculateSpread(dir, autocvar_g_nades_spread, autocvar_g_weaponspreadfactor, autocvar_g_projectiles_spread_style);
                                toss_nade(player, false, dir * _force, 0);
                        }
                }
     }
 ENDCLASS(NadeOffhand)
 NadeOffhand OFFHAND_NADE;
-STATIC_INIT(OFFHAND_NADE) { OFFHAND_NADE = NEW(NadeOffhand); }
+REGISTER_MUTATOR(nades, autocvar_g_nades)
+{
+       MUTATOR_ONADD
+       {
+               OFFHAND_NADE = NEW(NadeOffhand);
+       }
+       return 0;
+}
 
 MUTATOR_HOOKFUNCTION(nades, ForbidThrowCurrentWeapon, CBC_ORDER_LAST)
 {
@@ -1265,13 +1270,13 @@ MUTATOR_HOOKFUNCTION(nades, ForbidThrowCurrentWeapon, CBC_ORDER_LAST)
        }
 }
 
-#ifdef IS_REVIVING
-       #undef IS_REVIVING
+#ifdef IN_REVIVING_RANGE
+       #undef IN_REVIVING_RANGE
 #endif
 
 // returns true if player is reviving it
-#define IS_REVIVING(player, it, revive_extra_size) \
-       (it != player && !STAT(FROZEN, it) && !IS_DEAD(it) && SAME_TEAM(it, player) \
+#define IN_REVIVING_RANGE(player, it, revive_extra_size) \
+       (it != player && !IS_DEAD(it) && SAME_TEAM(it, player) \
        && boxesoverlap(player.absmin - revive_extra_size, player.absmax + revive_extra_size, it.absmin, it.absmax))
 
 MUTATOR_HOOKFUNCTION(nades, PlayerPreThink)
@@ -1280,7 +1285,8 @@ MUTATOR_HOOKFUNCTION(nades, PlayerPreThink)
 
        if (!IS_PLAYER(player)) { return; }
 
-       if (player.nade && (player.offhand != OFFHAND_NADE || (STAT(WEAPONS, player) & WEPSET(HOOK)))) OFFHAND_NADE.offhand_think(OFFHAND_NADE, player, player.nade_altbutton);
+       if (player.nade && (player.offhand != OFFHAND_NADE || (STAT(WEAPONS, player) & WEPSET(HOOK))))
+               OFFHAND_NADE.offhand_think(OFFHAND_NADE, player, player.nade_altbutton);
 
        entity held_nade = player.nade;
        if (held_nade)
@@ -1318,8 +1324,8 @@ MUTATOR_HOOKFUNCTION(nades, PlayerPreThink)
 
                        if(autocvar_g_nades_bonus_client_select)
                        {
-                               STAT(NADE_BONUS_TYPE, player) = CS(player).cvar_cl_nade_type;
-                               player.pokenade_type = CS(player).cvar_cl_pokenade_type;
+                               STAT(NADE_BONUS_TYPE, player) = CS_CVAR(player).cvar_cl_nade_type;
+                               player.pokenade_type = CS_CVAR(player).cvar_cl_pokenade_type;
                        }
                        else
                        {
@@ -1347,46 +1353,62 @@ MUTATOR_HOOKFUNCTION(nades, PlayerPreThink)
                }
        }
 
-       if (frametime && IS_PLAYER(player))
-       {
-               int n = 0;
+       if (!(frametime && IS_PLAYER(player)))
+               return true;
 
-               IntrusiveList reviving_players = NULL;
+       entity revivers_last = NULL;
+       entity revivers_first = NULL;
 
-               if(player.freezetag_frozen_timeout > 0 && time >= player.freezetag_frozen_timeout)
-                       n = -1;
-               else if (STAT(FROZEN, player) == FROZEN_TEMP_DYING)
+       bool player_is_reviving = false;
+       int n = 0;
+       vector revive_extra_size = '1 1 1' * autocvar_g_freezetag_revive_extra_size;
+       FOREACH_CLIENT(IS_PLAYER(it) && IN_REVIVING_RANGE(player, it, revive_extra_size), {
+               // check if player is reviving anyone
+               if (STAT(FROZEN, it) == FROZEN_TEMP_DYING)
                {
-                       vector revive_extra_size = '1 1 1' * autocvar_g_freezetag_revive_extra_size;
-                       n = 0;
-                       FOREACH_CLIENT(IS_PLAYER(it) && IS_REVIVING(player, it, revive_extra_size), {
-                               if (!reviving_players)
-                                       reviving_players = IL_NEW();
-                               IL_PUSH(reviving_players, it);
-                               ++n;
-                       });
+                       if ((STAT(FROZEN, player) == FROZEN_TEMP_DYING))
+                               continue;
+                       if (!IN_REVIVING_RANGE(player, it, revive_extra_size))
+                               continue;
+                       player_is_reviving = true;
+                       break;
                }
 
-               if (n > 0 && STAT(FROZEN, player) == FROZEN_TEMP_DYING) // OK, there is at least one teammate reviving us
-               {
-                       STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) + frametime * max(1/60, autocvar_g_freezetag_revive_speed), 1);
-                       SetResource(player, RES_HEALTH, max(1, STAT(REVIVE_PROGRESS, player) * start_health));
+               if (!(STAT(FROZEN, player) == FROZEN_TEMP_DYING))
+                       continue; // both player and it are NOT frozen
+               if (revivers_last)
+                       revivers_last.chain = it;
+               revivers_last = it;
+               if (!revivers_first)
+                       revivers_first = it;
+               ++n;
+       });
+       if (revivers_last)
+               revivers_last.chain = NULL;
 
-                       if(STAT(REVIVE_PROGRESS, player) >= 1)
-                       {
-                               Unfreeze(player, false);
+       if (!n) // no teammate nearby
+       {
+               // freezetag already resets revive progress
+               if (!g_freezetag && !STAT(FROZEN, player) && !player_is_reviving)
+                       STAT(REVIVE_PROGRESS, player) = 0; // thawing nobody
+       }
+       else if (n > 0 && STAT(FROZEN, player) == FROZEN_TEMP_DYING) // OK, there is at least one teammate reviving us
+       {
+               STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) + frametime * max(1/60, autocvar_g_freezetag_revive_speed), 1);
+               // undo what PlayerPreThink did
+               STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) + frametime * player.revive_speed, 1);
+               SetResource(player, RES_HEALTH, max(1, STAT(REVIVE_PROGRESS, player) * start_health));
 
-                               entity first = IL_FIRST(reviving_players);
-                               Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_REVIVED, first.netname);
-                               Send_Notification(NOTIF_ONE, first, MSG_CENTER, CENTER_FREEZETAG_REVIVE, player.netname);
-                       }
+               if(STAT(REVIVE_PROGRESS, player) >= 1)
+               {
+                       Unfreeze(player, false);
 
-                       IL_EACH(reviving_players, true, {
-                               STAT(REVIVE_PROGRESS, it) = STAT(REVIVE_PROGRESS, player);
-                       });
+                       Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_REVIVED, revivers_first.netname);
+                       Send_Notification(NOTIF_ONE, revivers_first, MSG_CENTER, CENTER_FREEZETAG_REVIVE, player.netname);
                }
-               if (reviving_players)
-                       IL_DELETE(reviving_players);
+
+               for(entity it = revivers_first; it; it = it.chain)
+                       STAT(REVIVE_PROGRESS, it) = STAT(REVIVE_PROGRESS, player);
        }
 }
 
@@ -1426,7 +1448,7 @@ MUTATOR_HOOKFUNCTION(nades, PlayerSpawn)
                player.nade_refire  = time + autocvar_g_nades_nade_refire;
 
        if(autocvar_g_nades_bonus_client_select)
-               STAT(NADE_BONUS_TYPE, player) = CS(player).cvar_cl_nade_type;
+               STAT(NADE_BONUS_TYPE, player) = CS_CVAR(player).cvar_cl_nade_type;
 
        STAT(NADE_TIMER, player) = 0;
 
@@ -1553,14 +1575,14 @@ MUTATOR_HOOKFUNCTION(nades, SpectateCopy)
        STAT(VEIL_ORB_ALPHA, client) = STAT(VEIL_ORB_ALPHA, spectatee);
 }
 
-MUTATOR_HOOKFUNCTION(nades, BuildMutatorsString)
+MUTATOR_HOOKFUNCTION(nades, BuildMutatorsPrettyString)
 {
-       M_ARGV(0, string) = strcat(M_ARGV(0, string), ":Nades");
+       M_ARGV(0, string) = strcat(M_ARGV(0, string), "Nades");
 }
 
-MUTATOR_HOOKFUNCTION(nades, BuildGameplayTipsString)
+MUTATOR_HOOKFUNCTION(nades, BuildMutatorsString)
 {
-       M_ARGV(0, string) = strcat(M_ARGV(0, string), "\n\n^3nades^8 are enabled, press 'g' (dropweapon) to use them\n");
+       M_ARGV(0, string) = strcat(M_ARGV(0, string), ":Nades");
 }
 
 #endif