]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/common/mutators/mutator/nades/nades.qc
New nade type: Entrap (spawns a large orb for 10 seconds that slows down any enemies...
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / mutators / mutator / nades / nades.qc
index 28aaac5dc17664df2622bcfab6634cc4ed3c66da..ab4e2bc6d4b0648c3daa7e7a81d6fc175b5743e1 100644 (file)
@@ -2,6 +2,13 @@
 
 #ifdef IMPLEMENTATION
 
+#ifdef SVQC
+bool autocvar_g_nades_nade_small;
+float autocvar_g_nades_spread = 0.04;
+#endif
+
+REGISTER_STAT(NADES_SMALL, int, autocvar_g_nades_nade_small)
+
 #ifndef MENUQC
 entity Nade_TrailEffect(int proj, int nade_team)
 {
@@ -31,13 +38,23 @@ entity Nade_TrailEffect(int proj, int nade_team)
 REGISTER_MUTATOR(cl_nades, true);
 MUTATOR_HOOKFUNCTION(cl_nades, HUD_Draw_overlay)
 {
-       if (STAT(HEALING_ORB) <= time) return false;
-       MUTATOR_ARGV(0, vector) = NADE_TYPE_HEAL.m_color;
-       MUTATOR_ARGV(0, float) = STAT(HEALING_ORB_ALPHA);
-       return true;
+       if (STAT(HEALING_ORB) > time)
+       {
+               MUTATOR_ARGV(0, vector) = NADE_TYPE_HEAL.m_color;
+               MUTATOR_ARGV(0, float) = STAT(HEALING_ORB_ALPHA);
+               return true;
+       }
+       if (STAT(ENTRAP_ORB) > time)
+       {
+               MUTATOR_ARGV(0, vector) = NADE_TYPE_ENTRAP.m_color;
+               MUTATOR_ARGV(0, float) = STAT(ENTRAP_ORB_ALPHA);
+               return true;
+       }
+       return false;
 }
 MUTATOR_HOOKFUNCTION(cl_nades, Ent_Projectile)
 {
+    SELFPARAM();
        if (self.cnt == PROJECTILE_NAPALM_FOUNTAIN)
        {
                self.modelindex = 0;
@@ -54,6 +71,7 @@ MUTATOR_HOOKFUNCTION(cl_nades, Ent_Projectile)
 }
 MUTATOR_HOOKFUNCTION(cl_nades, EditProjectile)
 {
+    SELFPARAM();
        if (self.cnt == PROJECTILE_NAPALM_FOUNTAIN)
        {
                loopsound(self, CH_SHOTS_SINGLE, SND(FIREBALL_FLY2), VOL_BASE, ATTEN_NORM);
@@ -63,8 +81,16 @@ MUTATOR_HOOKFUNCTION(cl_nades, EditProjectile)
 
        entity nade_type = Nade_FromProjectile(self.cnt);
        if (nade_type == NADE_TYPE_Null) return;
-       self.mins = '-16 -16 -16';
-       self.maxs = '16 16 16';
+       if(STAT(NADES_SMALL, NULL))
+       {
+               self.mins = '-8 -8 -8';
+               self.maxs = '8 8 8';
+       }
+       else
+       {
+               self.mins = '-16 -16 -16';
+               self.maxs = '16 16 16';
+       }
        self.colormod = nade_type.m_color;
        self.move_movetype = MOVETYPE_BOUNCE;
        self.move_touch = func_null;
@@ -119,10 +145,10 @@ void DrawAmmoNades(vector myPos, vector mySize, bool draw_expanding, float expan
 
 #ifdef SVQC
 
-#include "../../../gamemodes/all.qh"
-#include "../../../monsters/spawn.qh"
-#include "../../../monsters/sv_monsters.qh"
-#include "../../../../server/g_subs.qh"
+#include <common/gamemodes/all.qh>
+#include <common/monsters/spawn.qh>
+#include <common/monsters/sv_monsters.qh>
+#include <server/g_subs.qh>
 
 REGISTER_MUTATOR(nades, cvar("g_nades"));
 
@@ -175,7 +201,7 @@ void napalm_damage(float dist, float damage, float edgedamage, float burntime)
                if(e.takedamage == DAMAGE_AIM)
                if(self.realowner != e || autocvar_g_nades_napalm_selfdamage)
                if(!IS_PLAYER(e) || !self.realowner || DIFF_TEAM(e, self))
-               if(!e.frozen)
+               if(!STAT(FROZEN, e))
                {
                        p = e.origin;
                        p.x += e.mins.x + random() * (e.maxs.x - e.mins.x);
@@ -403,10 +429,10 @@ void nade_ice_think()
        for(e = findradius(self.origin, autocvar_g_nades_nade_radius); e; e = e.chain)
        if(e != self)
        if(!autocvar_g_nades_ice_teamcheck || (DIFF_TEAM(e, self.realowner) || e == self.realowner))
-       if(e.takedamage && e.deadflag == DEAD_NO)
+       if(e.takedamage && !IS_DEAD(e))
        if(e.health > 0)
        if(!e.revival_time || ((time - e.revival_time) >= 1.5))
-       if(!e.frozen)
+       if(!STAT(FROZEN, e))
        if(current_freeze_time > 0)
                nade_ice_freeze(self, e, current_freeze_time);
 }
@@ -485,23 +511,91 @@ void nade_spawn_boom()
        self.realowner.nade_spawnloc = spawnloc;
 }
 
-void nade_heal_think()
+void nades_orb_think()
 {SELFPARAM();
-       if(time >= self.ltime)
+       if(time >= this.ltime)
        {
-               remove(self);
+               remove(this);
                return;
        }
 
-       self.nextthink = time;
+       this.nextthink = time;
 
-       if(time >= self.nade_special_time)
+       if(time >= this.nade_special_time)
        {
-               self.nade_special_time = time+0.25;
-               self.nade_show_particles = 1;
+               this.nade_special_time = time+0.25;
+               this.nade_show_particles = 1;
        }
        else
-               self.nade_show_particles = 0;
+               this.nade_show_particles = 0;
+}
+
+entity nades_spawn_orb(entity own, entity realown, vector org, float orb_ltime, float orb_rad)
+{
+       // 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)
+       orb.owner = own;
+       orb.realowner = realown;
+       setorigin(orb, org);
+
+       orb.orb_lifetime = orb_ltime; // required for timers
+       orb.ltime = time + orb.orb_lifetime;
+       orb.bot_dodge = false;
+       orb.team = realown.team;
+       orb.solid = SOLID_TRIGGER;
+
+       setmodel(orb, MDL_NADE_ORB);
+       orb.orb_radius = orb_rad; // required for fading
+       vector size = '1 1 1' * orb.orb_radius / 2;
+       setsize(orb, -size, size);
+
+       Net_LinkEntity(orb, true, 0, orb_send);
+       orb.SendFlags |= 1;
+
+       orb.think = nades_orb_think;
+       orb.nextthink = time;
+
+       return orb;
+}
+
+void nade_entrap_touch()
+{SELFPARAM();
+       if(DIFF_TEAM(other, self.realowner)) // TODO: what if realowner changes team or disconnects?
+       {
+               if (!isPushable(other))
+                       return;
+
+               float pushdeltatime = time - other.lastpushtime;
+               if (pushdeltatime > 0.15) pushdeltatime = 0;
+               other.lastpushtime = time;
+               if(!pushdeltatime) return;
+
+               // div0: ticrate independent, 1 = identity (not 20)
+#ifdef SVQC
+               other.velocity = other.velocity * pow(autocvar_g_nades_entrap_strength, pushdeltatime);
+
+               UpdateCSQCProjectile(other);
+#elif defined(CSQC)
+               other.move_velocity = other.move_velocity * pow(autocvar_g_nades_entrap_strength, pushdeltatime);
+#endif
+
+               if ( IS_REAL_CLIENT(other) || IS_VEHICLE(other) )
+               {
+                       entity show_tint = (IS_VEHICLE(other)) ? other.owner : other;
+                       STAT(ENTRAP_ORB, show_tint) = time + 0.1;
+                       STAT(ENTRAP_ORB_ALPHA, show_tint) = 0.75 * (self.ltime - time) / self.orb_lifetime;
+               }
+       }
+}
+
+void nade_entrap_boom(entity this)
+{
+       entity healer = nades_spawn_orb(this.owner, this.realowner, this.origin, autocvar_g_nades_entrap_time, autocvar_g_nades_entrap_radius);
+
+       healer.touch = nade_entrap_touch;
+       healer.colormod = NADE_TYPE_ENTRAP.m_color;
 }
 
 void nade_heal_touch()
@@ -509,8 +603,8 @@ void nade_heal_touch()
        float maxhealth;
        float health_factor;
        if(IS_PLAYER(other) || IS_MONSTER(other))
-       if(other.deadflag == DEAD_NO)
-       if(!other.frozen)
+       if(!IS_DEAD(other))
+       if(!STAT(FROZEN, other))
        {
                health_factor = autocvar_g_nades_heal_rate*frametime/2;
                if ( other != self.realowner )
@@ -542,34 +636,16 @@ void nade_heal_touch()
        {
                entity show_red = (IS_VEHICLE(other)) ? other.owner : other;
                show_red.stat_healing_orb = time+0.1;
-               show_red.stat_healing_orb_alpha = 0.75 * (self.ltime - time) / self.healer_lifetime;
+               show_red.stat_healing_orb_alpha = 0.75 * (self.ltime - time) / self.orb_lifetime;
        }
 }
 
-void nade_heal_boom()
-{SELFPARAM();
-       entity healer;
-       healer = spawn();
-       healer.owner = self.owner;
-       healer.realowner = self.realowner;
-       setorigin(healer, self.origin);
-       healer.healer_lifetime = autocvar_g_nades_heal_time; // save the cvar
-       healer.ltime = time + healer.healer_lifetime;
-       healer.team = self.realowner.team;
-       healer.bot_dodge = false;
-       healer.solid = SOLID_TRIGGER;
-       healer.touch = nade_heal_touch;
-
-       setmodel(healer, MDL_NADE_HEAL);
-       healer.healer_radius = autocvar_g_nades_nade_radius;
-       vector size = '1 1 1' * healer.healer_radius / 2;
-       setsize(healer,-size,size);
-
-       Net_LinkEntity(healer, true, 0, healer_send);
+void nade_heal_boom(entity this)
+{
+       entity healer = nades_spawn_orb(this.owner, this.realowner, this.origin, autocvar_g_nades_heal_time, autocvar_g_nades_nade_radius);
 
-       healer.think = nade_heal_think;
-       healer.nextthink = time;
-       healer.SendFlags |= 1;
+       healer.touch = nade_heal_touch;
+       healer.colormod = '1 0 0';
 }
 
 void nade_monster_boom()
@@ -616,6 +692,11 @@ void nade_boom()
                        expef = EFFECT_SPAWN_RED;
                        break;
 
+               case NADE_TYPE_ENTRAP:
+                       nade_blast = false;
+                       expef = EFFECT_SPAWN_YELLOW;
+                       break;
+
                default:
                case NADE_TYPE_NORMAL:
                        expef = EFFECT_NADE_EXPLODE(self.realowner.team);
@@ -644,18 +725,34 @@ void nade_boom()
                case NADE_TYPE_ICE: nade_ice_boom(); break;
                case NADE_TYPE_TRANSLOCATE: nade_translocate_boom(); break;
                case NADE_TYPE_SPAWN: nade_spawn_boom(); break;
-               case NADE_TYPE_HEAL: nade_heal_boom(); break;
+               case NADE_TYPE_HEAL: nade_heal_boom(self); break;
                case NADE_TYPE_MONSTER: nade_monster_boom(); break;
+               case NADE_TYPE_ENTRAP: nade_entrap_boom(self); break;
        }
 
-       entity head;
-       for(head = world; (head = find(head, classname, "grapplinghook")); )
-       if(head.aiment == self)
-               RemoveGrapplingHook(head.realowner);
+       FOREACH_ENTITY_ENT(aiment, self,
+       {
+               if(it.classname == "grapplinghook")
+                       RemoveGrapplingHook(it.realowner);
+       });
 
        remove(self);
 }
 
+void spawn_held_nade(entity player, entity nowner, float ntime, int ntype, string pntype);
+void nade_pickup(entity this, entity thenade)
+{
+       spawn_held_nade(this, thenade.realowner, autocvar_g_nades_pickup_time, thenade.nade_type, thenade.pokenade_type);
+
+       // set refire so player can't even
+       this.nade_refire = time + autocvar_g_nades_nade_refire;
+       this.nade_timer = 0;
+
+       if(this.nade)
+               this.nade.nade_time_primed = thenade.nade_time_primed;
+}
+
+bool CanThrowNade(entity this);
 void nade_touch()
 {SELFPARAM();
        if(other)
@@ -663,6 +760,19 @@ void nade_touch()
 
        if(other == self.realowner)
                return; // no self impacts
+
+       if(autocvar_g_nades_pickup)
+       if(time >= self.spawnshieldtime)
+       if(!other.nade && self.health == self.max_health) // no boosted shot pickups, thank you very much
+       if(!other.frozen)
+       if(CanThrowNade(other)) // prevent some obvious things, like dead players
+       if(IS_REAL_CLIENT(other)) // above checks for IS_PLAYER, don't need to do it here
+       {
+               nade_pickup(other, self);
+               sound(self, CH_SHOTS_SINGLE, SND_Null, VOL_BASE, 0.5 *(ATTEN_LARGE + ATTEN_MAX));
+               remove(self);
+               return;
+       }
        /*float is_weapclip = 0;
        if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_NODRAW)
        if (!(trace_dphitq3surfaceflags & Q3SURFACEFLAG_NONSOLID))
@@ -670,10 +780,11 @@ void nade_touch()
                is_weapclip = 1;*/
        if(ITEM_TOUCH_NEEDKILL()) // || is_weapclip)
        {
-               entity head;
-               for(head = world; (head = find(head, classname, "grapplinghook")); )
-               if(head.aiment == self)
-                       RemoveGrapplingHook(head.realowner);
+               FOREACH_ENTITY_ENT(aiment, self,
+               {
+                       if(it.classname == "grapplinghook")
+                               RemoveGrapplingHook(it.realowner);
+               });
                remove(self);
                return;
        }
@@ -699,16 +810,16 @@ void nade_beep()
        self.nextthink = max(self.wait, time);
 }
 
-void nade_damage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
-{SELFPARAM();
+void nade_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
+{
        if(ITEM_DAMAGE_NEEDKILL(deathtype))
        {
-               self.takedamage = DAMAGE_NO;
-               nade_boom();
+               this.takedamage = DAMAGE_NO;
+               WITHSELF(this, nade_boom());
                return;
        }
 
-       if(self.nade_type == NADE_TYPE_TRANSLOCATE.m_id || self.nade_type == NADE_TYPE_SPAWN.m_id)
+       if(this.nade_type == NADE_TYPE_TRANSLOCATE.m_id || this.nade_type == NADE_TYPE_SPAWN.m_id)
                return;
 
        if (MUTATOR_CALLHOOK(Nade_Damage, DEATH_WEAPONOF(deathtype), force, damage)) {}
@@ -725,46 +836,46 @@ void nade_damage(entity inflictor, entity attacker, float damage, int deathtype,
        else if(DEATH_ISWEAPON(deathtype, WEP_VORTEX) || DEATH_ISWEAPON(deathtype, WEP_VAPORIZER))
        {
                force *= 6;
-               damage = self.max_health * 0.55;
+               damage = this.max_health * 0.55;
        }
        else if(DEATH_ISWEAPON(deathtype, WEP_MACHINEGUN))
-               damage = self.max_health * 0.1;
+               damage = this.max_health * 0.1;
        else if(DEATH_ISWEAPON(deathtype, WEP_SHOCKWAVE) || DEATH_ISWEAPON(deathtype, WEP_SHOTGUN)) // WEAPONTODO
        {
                if(deathtype & HITTYPE_SECONDARY)
                {
-                       damage = self.max_health * 0.1;
+                       damage = this.max_health * 0.1;
                        force *= 10;
                }
                else
-                       damage = self.max_health * 1.15;
+                       damage = this.max_health * 1.15;
        }
 
-       self.velocity += force;
-       UpdateCSQCProjectile(self);
+       this.velocity += force;
+       UpdateCSQCProjectile(this);
 
-       if(damage <= 0 || ((self.flags & FL_ONGROUND) && IS_PLAYER(attacker)))
+       if(damage <= 0 || ((IS_ONGROUND(this)) && IS_PLAYER(attacker)))
                return;
 
-       if(self.health == self.max_health)
+       if(this.health == this.max_health)
        {
-               sound(self, CH_SHOTS_SINGLE, SND_Null, VOL_BASE, 0.5 *(ATTEN_LARGE + ATTEN_MAX));
-               self.nextthink = max(time + autocvar_g_nades_nade_lifetime, time);
-               self.think = nade_beep;
+               sound(this, CH_SHOTS_SINGLE, SND_Null, VOL_BASE, 0.5 *(ATTEN_LARGE + ATTEN_MAX));
+               this.nextthink = max(time + autocvar_g_nades_nade_lifetime, time);
+               this.think = nade_beep;
        }
 
-       self.health -= damage;
+       this.health -= damage;
 
-       if ( self.nade_type != NADE_TYPE_HEAL.m_id || IS_PLAYER(attacker) )
-               self.realowner = attacker;
+       if ( this.nade_type != NADE_TYPE_HEAL.m_id || IS_PLAYER(attacker) )
+               this.realowner = attacker;
 
-       if(self.health <= 0)
-               W_PrepareExplosionByDamage(attacker, nade_boom);
+       if(this.health <= 0)
+               W_PrepareExplosionByDamage(this, attacker, nade_boom);
        else
-               nade_burn_spawn(self);
+               nade_burn_spawn(this);
 }
 
-void toss_nade(entity e, vector _velocity, float _time)
+void toss_nade(entity e, bool set_owner, vector _velocity, float _time)
 {SELFPARAM();
        if(e.nade == world)
                return;
@@ -777,9 +888,9 @@ void toss_nade(entity e, vector _velocity, float _time)
 
        makevectors(e.v_angle);
 
-       W_SetupShot(e, false, false, "", CH_WEAPON_A, 0);
+       W_SetupShot(e, false, false, SND_Null, CH_WEAPON_A, 0);
 
-       Kill_Notification(NOTIF_ONE_ONLY, e, MSG_CENTER_CPID, CPID_NADES);
+       Kill_Notification(NOTIF_ONE_ONLY, e, MSG_CENTER, CPID_NADES);
 
        vector offset = (v_forward * autocvar_g_nades_throw_offset.x)
                                  + (v_right * autocvar_g_nades_throw_offset.y)
@@ -791,14 +902,17 @@ void toss_nade(entity e, vector _velocity, float _time)
        //setmodel(_nade, MDL_PROJECTILE_NADE);
        //setattachment(_nade, world, "");
        PROJECTILE_MAKETRIGGER(_nade);
-       setsize(_nade, '-16 -16 -16', '16 16 16');
+       if(STAT(NADES_SMALL, e))
+               setsize(_nade, '-8 -8 -8', '8 8 8');
+       else
+               setsize(_nade, '-16 -16 -16', '16 16 16');
        _nade.movetype = MOVETYPE_BOUNCE;
 
        tracebox(_nade.origin, _nade.mins, _nade.maxs, _nade.origin, false, _nade);
        if (trace_startsolid)
                setorigin(_nade, e.origin);
 
-       if(self.v_angle.x >= 70 && self.v_angle.x <= 110 && self.BUTTON_CROUCH)
+       if(self.v_angle.x >= 70 && self.v_angle.x <= 110 && PHYS_INPUT_BUTTON_CROUCH(self))
                _nade.velocity = '0 0 100';
        else if(autocvar_g_nades_nade_newton_style == 1)
                _nade.velocity = e.velocity + _velocity;
@@ -807,7 +921,11 @@ void toss_nade(entity e, vector _velocity, float _time)
        else
                _nade.velocity = W_CalculateProjectileVelocity(e.velocity, _velocity, true);
 
+       if(set_owner)
+               _nade.realowner = e;
+
        _nade.touch = nade_touch;
+       _nade.spawnshieldtime = time + 0.1; // prevent instantly picking up again
        _nade.health = autocvar_g_nades_nade_health;
        _nade.max_health = _nade.health;
        _nade.takedamage = DAMAGE_AIM;
@@ -849,8 +967,8 @@ void nades_GiveBonus(entity player, float score)
        if (autocvar_g_nades_bonus)
        if (IS_REAL_CLIENT(player))
        if (IS_PLAYER(player) && player.bonus_nades < autocvar_g_nades_bonus_max)
-       if (player.frozen == 0)
-       if (player.deadflag == DEAD_NO)
+       if (STAT(FROZEN, player) == 0)
+       if (!IS_DEAD(player))
        {
                if ( player.bonus_nade_score < 1 )
                        player.bonus_nade_score += score/autocvar_g_nades_bonus_score_max;
@@ -873,13 +991,14 @@ void nades_RemoveBonus(entity player)
 
 MUTATOR_HOOKFUNCTION(nades, PutClientInServer)
 {
+    SELFPARAM();
        nades_RemoveBonus(self);
 }
 
 float nade_customize()
 {SELFPARAM();
        //if(IS_SPEC(other)) { return false; }
-       if(other == self.realowner || (IS_SPEC(other) && other.enemy == self.realowner))
+       if(other == self.exteriormodeltoclient || (IS_SPEC(other) && other.enemy == self.exteriormodeltoclient))
        {
                // somewhat hide the model, but keep the glow
                //self.effects = 0;
@@ -898,6 +1017,42 @@ float nade_customize()
        return true;
 }
 
+void spawn_held_nade(entity player, entity nowner, float ntime, int ntype, string pntype)
+{
+       entity n = new(nade), fn = new(fake_nade);
+
+       n.nade_type = bound(1, ntype, Nades_COUNT);
+       n.pokenade_type = pntype;
+
+       setmodel(n, MDL_PROJECTILE_NADE);
+       //setattachment(n, player, "bip01 l hand");
+       n.exteriormodeltoclient = player;
+       n.customizeentityforclient = nade_customize;
+       n.traileffectnum = _particleeffectnum(Nade_TrailEffect(Nades_from(n.nade_type).m_projectile[false], player.team).eent_eff_name);
+       n.colormod = Nades_from(n.nade_type).m_color;
+       n.realowner = nowner;
+       n.colormap = player.colormap;
+       n.glowmod = player.glowmod;
+       n.wait = time + max(0, ntime);
+       n.nade_time_primed = time;
+       n.think = nade_beep;
+       n.nextthink = max(n.wait - 3, time);
+       n.projectiledeathtype = DEATH_NADE.m_id;
+
+       setmodel(fn, MDL_NADE_VIEW);
+       .entity weaponentity = weaponentities[0]; // TODO: unhardcode
+       setattachment(fn, player.(weaponentity), "");
+       fn.realowner = fn.owner = player;
+       fn.colormod = Nades_from(n.nade_type).m_color;
+       fn.colormap = player.colormap;
+       fn.glowmod = player.glowmod;
+       fn.think = SUB_Remove_self;
+       fn.nextthink = n.wait;
+
+       player.nade = n;
+       player.fake_nade = fn;
+}
+
 void nade_prime()
 {SELFPARAM();
        if(autocvar_g_nades_bonus_only)
@@ -910,71 +1065,44 @@ void nade_prime()
        if(self.fake_nade)
                remove(self.fake_nade);
 
-       entity n = new(nade), fn = new(fake_nade);
+       int ntype;
+       string pntype = self.pokenade_type;
 
        if(self.items & ITEM_Strength.m_itemid && autocvar_g_nades_bonus_onstrength)
-               n.nade_type = self.nade_type;
+               ntype = self.nade_type;
        else if (self.bonus_nades >= 1)
        {
-               n.nade_type = self.nade_type;
-               n.pokenade_type = self.pokenade_type;
+               ntype = self.nade_type;
+               pntype = self.pokenade_type;
                self.bonus_nades -= 1;
        }
        else
        {
-               n.nade_type = ((autocvar_g_nades_client_select) ? self.cvar_cl_nade_type : autocvar_g_nades_nade_type);
-               n.pokenade_type = ((autocvar_g_nades_client_select) ? self.cvar_cl_pokenade_type : autocvar_g_nades_pokenade_monster_type);
+               ntype = ((autocvar_g_nades_client_select) ? self.cvar_cl_nade_type : autocvar_g_nades_nade_type);
+               pntype = ((autocvar_g_nades_client_select) ? self.cvar_cl_pokenade_type : autocvar_g_nades_pokenade_monster_type);
        }
 
-       n.nade_type = bound(1, n.nade_type, Nades_COUNT);
-
-       setmodel(n, MDL_PROJECTILE_NADE);
-       //setattachment(n, self, "bip01 l hand");
-       n.exteriormodeltoclient = self;
-       n.customizeentityforclient = nade_customize;
-       n.traileffectnum = _particleeffectnum(Nade_TrailEffect(Nades_from(n.nade_type).m_projectile[false], self.team).eent_eff_name);
-       n.colormod = Nades_from(n.nade_type).m_color;
-       n.realowner = self;
-       n.colormap = self.colormap;
-       n.glowmod = self.glowmod;
-       n.wait = time + autocvar_g_nades_nade_lifetime;
-       n.nade_time_primed = time;
-       n.think = nade_beep;
-       n.nextthink = max(n.wait - 3, time);
-       n.projectiledeathtype = DEATH_NADE.m_id;
-
-       setmodel(fn, MDL_NADE_VIEW);
-       .entity weaponentity = weaponentities[0]; // TODO: unhardcode
-       setattachment(fn, self.(weaponentity), "");
-       fn.realowner = fn.owner = self;
-       fn.colormod = Nades_from(n.nade_type).m_color;
-       fn.colormap = self.colormap;
-       fn.glowmod = self.glowmod;
-       fn.think = SUB_Remove_self;
-       fn.nextthink = n.wait;
-
-       self.nade = n;
-       self.fake_nade = fn;
+       spawn_held_nade(self, self, autocvar_g_nades_nade_lifetime, ntype, pntype);
 }
 
-float CanThrowNade()
-{SELFPARAM();
-       if(self.vehicle)
+bool CanThrowNade(entity this)
+{
+       if(this.vehicle)
                return false;
 
        if(gameover)
                return false;
 
-       if(self.deadflag != DEAD_NO)
+       if(IS_DEAD(this))
                return false;
 
        if (!autocvar_g_nades)
                return false; // allow turning them off mid match
 
-       if(forbidWeaponUse(self))
+       if(forbidWeaponUse(this))
                return false;
 
-       if (!IS_PLAYER(self))
+       if (!IS_PLAYER(this))
                return false;
 
        return true;
@@ -984,7 +1112,7 @@ float CanThrowNade()
 
 void nades_CheckThrow()
 {SELFPARAM();
-       if(!CanThrowNade())
+       if(!CanThrowNade(self))
                return;
 
        entity held_nade = self.nade;
@@ -1006,7 +1134,9 @@ void nades_CheckThrow()
                        float _force = time - held_nade.nade_time_primed;
                        _force /= autocvar_g_nades_nade_lifetime;
                        _force = autocvar_g_nades_nade_minforce + (_force * (autocvar_g_nades_nade_maxforce - autocvar_g_nades_nade_minforce));
-                       toss_nade(self, (v_forward * 0.75 + v_up * 0.2 + v_right * 0.05) * _force, 0);
+                       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);
+                       toss_nade(self, true, dir * _force, 0);
                }
        }
 }
@@ -1025,7 +1155,7 @@ void nades_Clear(entity player)
 MUTATOR_HOOKFUNCTION(nades, VehicleEnter)
 {
        if(vh_player.nade)
-               toss_nade(vh_player, '0 0 100', max(vh_player.nade.wait, time + 0.05));
+               toss_nade(vh_player, true, '0 0 100', max(vh_player.nade.wait, time + 0.05));
 
        return false;
 }
@@ -1044,10 +1174,10 @@ CLASS(NadeOffhand, OffhandWeapon)
                        held_nade.angles_y = player.angles.y;
 
                        if (time + 0.1 >= held_nade.wait)
-                               toss_nade(player, '0 0 0', time + 0.05);
+                               toss_nade(player, false, '0 0 0', time + 0.05);
                }
 
-        if (!CanThrowNade()) return;
+        if (!CanThrowNade(player)) return;
         if (!(time > player.nade_refire)) return;
                if (key_pressed) {
                        if (!held_nade) {
@@ -1060,7 +1190,9 @@ CLASS(NadeOffhand, OffhandWeapon)
                                float _force = time - held_nade.nade_time_primed;
                                _force /= autocvar_g_nades_nade_lifetime;
                                _force = autocvar_g_nades_nade_minforce + (_force * (autocvar_g_nades_nade_maxforce - autocvar_g_nades_nade_minforce));
-                               toss_nade(player, (v_forward * 0.7 + v_up * 0.2 + v_right * 0.1) * _force, 0);
+                               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);
+                               toss_nade(player, false, dir * _force, 0);
                        }
                }
     }
@@ -1069,6 +1201,7 @@ NadeOffhand OFFHAND_NADE; STATIC_INIT(OFFHAND_NADE) { OFFHAND_NADE = NEW(NadeOff
 
 MUTATOR_HOOKFUNCTION(nades, ForbidThrowCurrentWeapon, CBC_ORDER_LAST)
 {
+    SELFPARAM();
        if (self.offhand != OFFHAND_NADE || (self.weapons & WEPSET(HOOK)) || autocvar_g_nades_override_dropweapon) {
                nades_CheckThrow();
                return true;
@@ -1129,23 +1262,22 @@ MUTATOR_HOOKFUNCTION(nades, PlayerPreThink)
        {
                vector revive_extra_size = '1 1 1' * autocvar_g_freezetag_revive_extra_size;
                n = 0;
-               FOR_EACH_PLAYER(other) if(self != other)
-               {
-                       if(other.deadflag == DEAD_NO)
-                       if(other.frozen == 0)
-                       if(SAME_TEAM(other, self))
-                       if(boxesoverlap(self.absmin - revive_extra_size, self.absmax + revive_extra_size, other.absmin, other.absmax))
+               FOREACH_CLIENT(IS_PLAYER(it) && it != self, LAMBDA(
+                       if(!IS_DEAD(it))
+                       if(STAT(FROZEN, it) == 0)
+                       if(SAME_TEAM(it, self))
+                       if(boxesoverlap(self.absmin - revive_extra_size, self.absmax + revive_extra_size, it.absmin, it.absmax))
                        {
                                if(!o)
-                                       o = other;
-                               if(self.frozen == 1)
-                                       other.reviving = true;
+                                       o = it;
+                               if(STAT(FROZEN, self) == 1)
+                                       it.reviving = true;
                                ++n;
                        }
-               }
+               ));
        }
 
-       if(n && self.frozen == 3) // OK, there is at least one teammate reviving us
+       if(n && STAT(FROZEN, self) == 3) // OK, there is at least one teammate reviving us
        {
                self.revive_progress = bound(0, self.revive_progress + frametime * max(1/60, autocvar_g_freezetag_revive_speed), 1);
                self.health = max(1, self.revive_progress * start_health);
@@ -1158,16 +1290,25 @@ MUTATOR_HOOKFUNCTION(nades, PlayerPreThink)
                        Send_Notification(NOTIF_ONE, o, MSG_CENTER, CENTER_FREEZETAG_REVIVE, self.netname);
                }
 
-               FOR_EACH_PLAYER(other) if(other.reviving)
-               {
+               FOREACH_CLIENT(IS_PLAYER(it) && it.reviving, LAMBDA(
                        other.revive_progress = self.revive_progress;
                        other.reviving = false;
-               }
+               ));
        }
 
        return false;
 }
 
+MUTATOR_HOOKFUNCTION(nades, PlayerPhysics)
+{SELFPARAM();
+       if (STAT(ENTRAP_ORB, this) > time)
+       {
+               this.stat_sv_maxspeed *= autocvar_g_nades_entrap_speed;
+               this.stat_sv_airspeedlimit_nonqw *= autocvar_g_nades_entrap_speed;
+       }
+       return false;
+}
+
 MUTATOR_HOOKFUNCTION(nades, PlayerSpawn)
 {SELFPARAM();
        if(autocvar_g_nades_spawn)
@@ -1200,8 +1341,8 @@ MUTATOR_HOOKFUNCTION(nades, PlayerSpawn)
 MUTATOR_HOOKFUNCTION(nades, PlayerDies, CBC_ORDER_LAST)
 {
        if(frag_target.nade)
-       if(!frag_target.frozen || !autocvar_g_freezetag_revive_nade)
-               toss_nade(frag_target, '0 0 100', max(frag_target.nade.wait, time + 0.05));
+       if(!STAT(FROZEN, frag_target) || !autocvar_g_freezetag_revive_nade)
+               toss_nade(frag_target, true, '0 0 100', max(frag_target.nade.wait, time + 0.05));
 
        float killcount_bonus = ((frag_attacker.killcount >= 1) ? bound(0, autocvar_g_nades_bonus_score_minor * frag_attacker.killcount, autocvar_g_nades_bonus_score_medium) : autocvar_g_nades_bonus_score_minor);
 
@@ -1233,7 +1374,7 @@ MUTATOR_HOOKFUNCTION(nades, PlayerDies, CBC_ORDER_LAST)
 
 MUTATOR_HOOKFUNCTION(nades, PlayerDamage_Calculate)
 {
-       if(frag_target.frozen)
+       if(STAT(FROZEN, frag_target))
        if(autocvar_g_freezetag_revive_nade)
        if(frag_attacker == frag_target)
        if(frag_deathtype == DEATH_NADE.m_id)
@@ -1252,10 +1393,10 @@ MUTATOR_HOOKFUNCTION(nades, PlayerDamage_Calculate)
 }
 
 MUTATOR_HOOKFUNCTION(nades, MonsterDies)
-{SELFPARAM();
+{
        if(IS_PLAYER(frag_attacker))
-       if(DIFF_TEAM(frag_attacker, self))
-       if(!(self.spawnflags & MONSTERFLAG_SPAWNED))
+       if(DIFF_TEAM(frag_attacker, frag_target))
+       if(!(frag_target.spawnflags & MONSTERFLAG_SPAWNED))
                nades_GiveBonus(frag_attacker, autocvar_g_nades_bonus_score_minor);
 
        return false;
@@ -1264,7 +1405,7 @@ MUTATOR_HOOKFUNCTION(nades, MonsterDies)
 MUTATOR_HOOKFUNCTION(nades, DropSpecialItems)
 {
        if(frag_target.nade)
-               toss_nade(frag_target, '0 0 0', time + 0.05);
+               toss_nade(frag_target, true, '0 0 0', time + 0.05);
 
        return false;
 }
@@ -1289,16 +1430,13 @@ MUTATOR_HOOKFUNCTION(nades, SpectateCopy)
        self.bonus_nade_score = other.bonus_nade_score;
        self.stat_healing_orb = other.stat_healing_orb;
        self.stat_healing_orb_alpha = other.stat_healing_orb_alpha;
+       STAT(ENTRAP_ORB, this) = STAT(ENTRAP_ORB, other);
+       STAT(ENTRAP_ORB_ALPHA, this) = STAT(ENTRAP_ORB_ALPHA, other);
        return false;
 }
 
-MUTATOR_HOOKFUNCTION(nades, GetCvars)
-{
-       GetCvars_handleFloat(get_cvars_s, get_cvars_f, cvar_cl_nade_type, "cl_nade_type");
-       GetCvars_handleString(get_cvars_s, get_cvars_f, cvar_cl_pokenade_type, "cl_pokenade_type");
-
-       return false;
-}
+REPLICATE(cvar_cl_nade_type, int, "cl_nade_type");
+REPLICATE(cvar_cl_pokenade_type, string, "cl_pokenade_type");
 
 MUTATOR_HOOKFUNCTION(nades, BuildMutatorsString)
 {