]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/nades/nades.qc
Merge branch 'master' into Lyberta/StandaloneOverkillWeapons
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / mutators / mutator / nades / nades.qc
1 #include "nades.qh"
2
3 #include "../overkill/okmachinegun.qh"
4
5 #ifdef SVQC
6 bool autocvar_g_nades_nade_small;
7 float autocvar_g_nades_spread = 0.04;
8 #endif
9
10 REGISTER_STAT(NADES_SMALL, int, autocvar_g_nades_nade_small)
11
12 #ifdef GAMEQC
13 entity Nade_TrailEffect(int proj, int nade_team)
14 {
15     switch (proj)
16     {
17         case PROJECTILE_NADE:       return EFFECT_NADE_TRAIL(nade_team);
18         case PROJECTILE_NADE_BURN:  return EFFECT_NADE_TRAIL_BURN(nade_team);
19     }
20
21     FOREACH(Nades, true, {
22         for (int j = 0; j < 2; j++)
23         {
24             if (it.m_projectile[j] == proj)
25             {
26                 string trail = it.m_trail[j].eent_eff_name;
27                 if (trail) return it.m_trail[j];
28                 break;
29             }
30         }
31     });
32
33     return EFFECT_Null;
34 }
35 #endif
36
37 #ifdef CSQC
38 REGISTER_MUTATOR(cl_nades, true);
39 MUTATOR_HOOKFUNCTION(cl_nades, HUD_Draw_overlay)
40 {
41         if (STAT(HEALING_ORB) > time)
42         {
43                 M_ARGV(0, vector) = NADE_TYPE_HEAL.m_color;
44                 M_ARGV(1, float) = STAT(HEALING_ORB_ALPHA);
45                 return true;
46         }
47         if (STAT(ENTRAP_ORB) > time)
48         {
49                 M_ARGV(0, vector) = NADE_TYPE_ENTRAP.m_color;
50                 M_ARGV(1, float) = STAT(ENTRAP_ORB_ALPHA);
51                 return true;
52         }
53         return false;
54 }
55 MUTATOR_HOOKFUNCTION(cl_nades, Ent_Projectile)
56 {
57         entity proj = M_ARGV(0, entity);
58
59         if (proj.cnt == PROJECTILE_NAPALM_FOUNTAIN)
60         {
61                 proj.modelindex = 0;
62                 proj.traileffect = EFFECT_FIREBALL.m_id;
63                 return true;
64         }
65         if (Nade_FromProjectile(proj.cnt) != NADE_TYPE_Null)
66         {
67                 setmodel(proj, MDL_PROJECTILE_NADE);
68                 entity trail = Nade_TrailEffect(proj.cnt, proj.team);
69                 if (trail.eent_eff_name) proj.traileffect = trail.m_id;
70                 return true;
71         }
72 }
73 MUTATOR_HOOKFUNCTION(cl_nades, EditProjectile)
74 {
75         entity proj = M_ARGV(0, entity);
76
77         if (proj.cnt == PROJECTILE_NAPALM_FOUNTAIN)
78         {
79                 loopsound(proj, CH_SHOTS_SINGLE, SND_FIREBALL_FLY2, VOL_BASE, ATTEN_NORM);
80                 proj.mins = '-16 -16 -16';
81                 proj.maxs = '16 16 16';
82         }
83
84         entity nade_type = Nade_FromProjectile(proj.cnt);
85         if (nade_type == NADE_TYPE_Null) return;
86         if(STAT(NADES_SMALL))
87         {
88                 proj.mins = '-8 -8 -8';
89                 proj.maxs = '8 8 8';
90         }
91         else
92         {
93                 proj.mins = '-16 -16 -16';
94                 proj.maxs = '16 16 16';
95         }
96         proj.colormod = nade_type.m_color;
97         set_movetype(proj, MOVETYPE_BOUNCE);
98         settouch(proj, func_null);
99         proj.scale = 1.5;
100         proj.avelocity = randomvec() * 720;
101
102         if (nade_type == NADE_TYPE_TRANSLOCATE || nade_type == NADE_TYPE_SPAWN)
103                 proj.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
104         else
105                 proj.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY;
106 }
107 bool Projectile_isnade(int p)
108 {
109         return Nade_FromProjectile(p) != NADE_TYPE_Null;
110 }
111 void DrawAmmoNades(vector myPos, vector mySize, bool draw_expanding, float expand_time)
112 {
113         float bonusNades    = STAT(NADE_BONUS);
114         float bonusProgress = STAT(NADE_BONUS_SCORE);
115         float bonusType     = STAT(NADE_BONUS_TYPE);
116         Nade def = Nades_from(bonusType);
117         vector nadeColor    = def.m_color;
118         string nadeIcon     = def.m_icon;
119
120         vector iconPos, textPos;
121
122         if(autocvar_hud_panel_ammo_iconalign)
123         {
124                 iconPos = myPos + eX * 2 * mySize.y;
125                 textPos = myPos;
126         }
127         else
128         {
129                 iconPos = myPos;
130                 textPos = myPos + eX * mySize.y;
131         }
132
133         if(bonusNades > 0 || bonusProgress > 0)
134         {
135                 DrawNadeProgressBar(myPos, mySize, bonusProgress, nadeColor);
136
137                 if(autocvar_hud_panel_ammo_text)
138                         drawstring_aspect(textPos, ftos(bonusNades), vec2((2/3) * mySize.x, mySize.y), '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
139
140                 if(draw_expanding)
141                         drawpic_aspect_skin_expanding(iconPos, nadeIcon, '1 1 0' * mySize.y, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL, expand_time);
142
143                 drawpic_aspect_skin(iconPos, nadeIcon, '1 1 0' * mySize.y, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
144         }
145 }
146 #endif
147
148 #ifdef SVQC
149
150 #include <common/gamemodes/_mod.qh>
151 #include <common/monsters/sv_spawn.qh>
152 #include <common/monsters/sv_monsters.qh>
153 #include <server/g_subs.qh>
154
155 REGISTER_MUTATOR(nades, autocvar_g_nades);
156
157 .float nade_time_primed;
158 .float nade_lifetime;
159
160 .entity nade_spawnloc;
161
162
163 void nade_timer_think(entity this)
164 {
165         this.skin = 8 - (this.owner.wait - time) / (this.owner.nade_lifetime / 10);
166         this.nextthink = time;
167         if(!this.owner || wasfreed(this.owner))
168                 delete(this);
169 }
170
171 void nade_burn_spawn(entity _nade)
172 {
173         CSQCProjectile(_nade, true, Nades_from(_nade.nade_type).m_projectile[true], true);
174 }
175
176 void nade_spawn(entity _nade)
177 {
178         entity timer = new(nade_timer);
179         setmodel(timer, MDL_NADE_TIMER);
180         setattachment(timer, _nade, "");
181         timer.colormap = _nade.colormap;
182         timer.glowmod = _nade.glowmod;
183         setthink(timer, nade_timer_think);
184         timer.nextthink = time;
185         timer.wait = _nade.wait;
186         timer.owner = _nade;
187         timer.skin = 10;
188
189         _nade.effects |= EF_LOWPRECISION;
190
191         CSQCProjectile(_nade, true, Nades_from(_nade.nade_type).m_projectile[false], true);
192 }
193
194 void napalm_damage(entity this, float dist, float damage, float edgedamage, float burntime)
195 {
196         entity e;
197         float d;
198         vector p;
199
200         if ( damage < 0 )
201                 return;
202
203         RandomSelection_Init();
204         for(e = WarpZone_FindRadius(this.origin, dist, true); e; e = e.chain)
205                 if(e.takedamage == DAMAGE_AIM)
206                 if(this.realowner != e || autocvar_g_nades_napalm_selfdamage)
207                 if(!IS_PLAYER(e) || !this.realowner || DIFF_TEAM(e, this))
208                 if(!STAT(FROZEN, e))
209                 {
210                         p = e.origin;
211                         p.x += e.mins.x + random() * (e.maxs.x - e.mins.x);
212                         p.y += e.mins.y + random() * (e.maxs.y - e.mins.y);
213                         p.z += e.mins.z + random() * (e.maxs.z - e.mins.z);
214                         d = vlen(WarpZone_UnTransformOrigin(e, this.origin) - p);
215                         if(d < dist)
216                         {
217                                 e.fireball_impactvec = p;
218                                 RandomSelection_AddEnt(e, 1 / (1 + d), !Fire_IsBurning(e));
219                         }
220                 }
221         if(RandomSelection_chosen_ent)
222         {
223                 d = vlen(WarpZone_UnTransformOrigin(RandomSelection_chosen_ent, this.origin) - RandomSelection_chosen_ent.fireball_impactvec);
224                 d = damage + (edgedamage - damage) * (d / dist);
225                 Fire_AddDamage(RandomSelection_chosen_ent, this.realowner, d * burntime, burntime, this.projectiledeathtype);
226                 //trailparticles(this, particleeffectnum(EFFECT_FIREBALL_LASER), this.origin, RandomSelection_chosen_ent.fireball_impactvec);
227                 Send_Effect(EFFECT_FIREBALL_LASER, this.origin, RandomSelection_chosen_ent.fireball_impactvec - this.origin, 1);
228         }
229 }
230
231
232 void napalm_ball_think(entity this)
233 {
234         if(round_handler_IsActive())
235         if(!round_handler_IsRoundStarted())
236         {
237                 delete(this);
238                 return;
239         }
240
241         if(time > this.pushltime)
242         {
243                 delete(this);
244                 return;
245         }
246
247         vector midpoint = ((this.absmin + this.absmax) * 0.5);
248         if(pointcontents(midpoint) == CONTENT_WATER)
249         {
250                 this.velocity = this.velocity * 0.5;
251
252                 if(pointcontents(midpoint + '0 0 16') == CONTENT_WATER)
253                         { this.velocity_z = 200; }
254         }
255
256         this.angles = vectoangles(this.velocity);
257
258         napalm_damage(this, autocvar_g_nades_napalm_ball_radius,autocvar_g_nades_napalm_ball_damage,
259                                   autocvar_g_nades_napalm_ball_damage,autocvar_g_nades_napalm_burntime);
260
261         this.nextthink = time + 0.1;
262 }
263
264
265 void nade_napalm_ball(entity this)
266 {
267         entity proj;
268         vector kick;
269
270         spamsound(this, CH_SHOTS, SND_FIREBALL_FIRE, VOL_BASE, ATTEN_NORM);
271
272         proj = new(grenade);
273         proj.owner = this.owner;
274         proj.realowner = this.realowner;
275         proj.team = this.owner.team;
276         proj.bot_dodge = true;
277         proj.bot_dodgerating = autocvar_g_nades_napalm_ball_damage;
278         set_movetype(proj, MOVETYPE_BOUNCE);
279         proj.projectiledeathtype = DEATH_NADE_NAPALM.m_id;
280         PROJECTILE_MAKETRIGGER(proj);
281         setmodel(proj, MDL_Null);
282         proj.scale = 1;//0.5;
283         setsize(proj, '-4 -4 -4', '4 4 4');
284         setorigin(proj, this.origin);
285         setthink(proj, napalm_ball_think);
286         proj.nextthink = time;
287         proj.damageforcescale = autocvar_g_nades_napalm_ball_damageforcescale;
288         proj.effects = EF_LOWPRECISION | EF_FLAME;
289
290         kick.x =(random() - 0.5) * 2 * autocvar_g_nades_napalm_ball_spread;
291         kick.y = (random() - 0.5) * 2 * autocvar_g_nades_napalm_ball_spread;
292         kick.z = (random()/2+0.5) * autocvar_g_nades_napalm_ball_spread;
293         proj.velocity = kick;
294
295         proj.pushltime = time + autocvar_g_nades_napalm_ball_lifetime;
296
297         proj.angles = vectoangles(proj.velocity);
298         proj.flags = FL_PROJECTILE;
299         IL_PUSH(g_projectiles, proj);
300         IL_PUSH(g_bot_dodge, proj);
301         proj.missile_flags = MIF_SPLASH | MIF_PROXY | MIF_ARC;
302
303         //CSQCProjectile(proj, true, PROJECTILE_NAPALM_FIRE, true);
304 }
305
306
307 void napalm_fountain_think(entity this)
308 {
309
310         if(round_handler_IsActive())
311         if(!round_handler_IsRoundStarted())
312         {
313                 delete(this);
314                 return;
315         }
316
317         if(time >= this.ltime)
318         {
319                 delete(this);
320                 return;
321         }
322
323         vector midpoint = ((this.absmin + this.absmax) * 0.5);
324         if(pointcontents(midpoint) == CONTENT_WATER)
325         {
326                 this.velocity = this.velocity * 0.5;
327
328                 if(pointcontents(midpoint + '0 0 16') == CONTENT_WATER)
329                         { this.velocity_z = 200; }
330
331                 UpdateCSQCProjectile(this);
332         }
333
334         napalm_damage(this, autocvar_g_nades_napalm_fountain_radius, autocvar_g_nades_napalm_fountain_damage,
335                 autocvar_g_nades_napalm_fountain_edgedamage, autocvar_g_nades_napalm_burntime);
336
337         this.nextthink = time + 0.1;
338         if(time >= this.nade_special_time)
339         {
340                 this.nade_special_time = time + autocvar_g_nades_napalm_fountain_delay;
341                 nade_napalm_ball(this);
342         }
343 }
344
345 void nade_napalm_boom(entity this)
346 {
347         entity fountain;
348         int c;
349         for (c = 0; c < autocvar_g_nades_napalm_ball_count; c++)
350                 nade_napalm_ball(this);
351
352
353         fountain = spawn();
354         fountain.owner = this.owner;
355         fountain.realowner = this.realowner;
356         fountain.origin = this.origin;
357         fountain.flags = FL_PROJECTILE;
358         IL_PUSH(g_projectiles, fountain);
359         IL_PUSH(g_bot_dodge, fountain);
360         setorigin(fountain, fountain.origin);
361         setthink(fountain, napalm_fountain_think);
362         fountain.nextthink = time;
363         fountain.ltime = time + autocvar_g_nades_napalm_fountain_lifetime;
364         fountain.pushltime = fountain.ltime;
365         fountain.team = this.team;
366         set_movetype(fountain, MOVETYPE_TOSS);
367         fountain.projectiledeathtype = DEATH_NADE_NAPALM.m_id;
368         fountain.bot_dodge = true;
369         fountain.bot_dodgerating = autocvar_g_nades_napalm_fountain_damage;
370         fountain.nade_special_time = time;
371         setsize(fountain, '-16 -16 -16', '16 16 16');
372         CSQCProjectile(fountain, true, PROJECTILE_NAPALM_FOUNTAIN, true);
373 }
374
375 void nade_ice_freeze(entity freezefield, entity frost_target, float freeze_time)
376 {
377         frost_target.frozen_by = freezefield.realowner;
378         Send_Effect(EFFECT_ELECTRO_IMPACT, frost_target.origin, '0 0 0', 1);
379         Freeze(frost_target, 1/freeze_time, 3, false);
380
381         Drop_Special_Items(frost_target);
382 }
383
384 void nade_ice_think(entity this)
385 {
386         if(round_handler_IsActive())
387         if(!round_handler_IsRoundStarted())
388         {
389                 delete(this);
390                 return;
391         }
392
393         if(time >= this.ltime)
394         {
395                 if ( autocvar_g_nades_ice_explode )
396                 {
397                         entity expef = EFFECT_NADE_EXPLODE(this.realowner.team);
398                         Send_Effect(expef, this.origin + '0 0 1', '0 0 0', 1);
399                         sound(this, CH_SHOTS, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_NORM);
400
401                         RadiusDamage(this, this.realowner, autocvar_g_nades_nade_damage, autocvar_g_nades_nade_edgedamage,
402                                 autocvar_g_nades_nade_radius, this, NULL, autocvar_g_nades_nade_force, this.projectiledeathtype, DMG_NOWEP, this.enemy);
403                         Damage_DamageInfo(this.origin, autocvar_g_nades_nade_damage, autocvar_g_nades_nade_edgedamage,
404                                 autocvar_g_nades_nade_radius, '1 1 1' * autocvar_g_nades_nade_force, this.projectiledeathtype, 0, this);
405                 }
406                 delete(this);
407                 return;
408         }
409
410
411         this.nextthink = time+0.1;
412
413         // gaussian
414         float randomr;
415         randomr = random();
416         randomr = exp(-5*randomr*randomr)*autocvar_g_nades_nade_radius;
417         float randomw;
418         randomw = random()*M_PI*2;
419         vector randomp;
420         randomp.x = randomr*cos(randomw);
421         randomp.y = randomr*sin(randomw);
422         randomp.z = 1;
423         Send_Effect(EFFECT_ELECTRO_MUZZLEFLASH, this.origin + randomp, '0 0 0', 1);
424
425         if(time >= this.nade_special_time)
426         {
427                 this.nade_special_time = time+0.7;
428
429                 Send_Effect(EFFECT_ELECTRO_IMPACT, this.origin, '0 0 0', 1);
430                 Send_Effect(EFFECT_ICEFIELD, this.origin, '0 0 0', 1);
431         }
432
433
434         float current_freeze_time = this.ltime - time - 0.1;
435
436         FOREACH_ENTITY_RADIUS(this.origin, autocvar_g_nades_nade_radius, it != this && it.takedamage && !IS_DEAD(it) && GetResourceAmount(it, RESOURCE_HEALTH) > 0 && current_freeze_time > 0,
437         {
438                 if(!autocvar_g_nades_ice_teamcheck || (DIFF_TEAM(it, this.realowner) || it == this.realowner))
439                 if(!it.revival_time || ((time - it.revival_time) >= 1.5))
440                 if(!STAT(FROZEN, it))
441                         nade_ice_freeze(this, it, current_freeze_time);
442         });
443 }
444
445 void nade_ice_boom(entity this)
446 {
447         entity fountain;
448         fountain = spawn();
449         fountain.owner = this.owner;
450         fountain.realowner = this.realowner;
451         fountain.origin = this.origin;
452         setorigin(fountain, fountain.origin);
453         setthink(fountain, nade_ice_think);
454         fountain.nextthink = time;
455         fountain.ltime = time + autocvar_g_nades_ice_freeze_time;
456         fountain.pushltime = fountain.wait = fountain.ltime;
457         fountain.team = this.team;
458         set_movetype(fountain, MOVETYPE_TOSS);
459         fountain.projectiledeathtype = DEATH_NADE_ICE.m_id;
460         fountain.bot_dodge = false;
461         setsize(fountain, '-16 -16 -16', '16 16 16');
462         fountain.nade_special_time = time+0.3;
463         fountain.angles = this.angles;
464
465         if ( autocvar_g_nades_ice_explode )
466         {
467                 setmodel(fountain, MDL_PROJECTILE_GRENADE);
468                 entity timer = new(nade_timer);
469                 setmodel(timer, MDL_NADE_TIMER);
470                 setattachment(timer, fountain, "");
471                 timer.colormap = this.colormap;
472                 timer.glowmod = this.glowmod;
473                 setthink(timer, nade_timer_think);
474                 timer.nextthink = time;
475                 timer.wait = fountain.ltime;
476                 timer.owner = fountain;
477                 timer.skin = 10;
478         }
479         else
480                 setmodel(fountain, MDL_Null);
481 }
482
483 void nade_translocate_boom(entity this)
484 {
485         if(this.realowner.vehicle)
486                 return;
487
488         vector locout = this.origin + '0 0 1' * (1 - this.realowner.mins.z - 24);
489         tracebox(locout, this.realowner.mins, this.realowner.maxs, locout, MOVE_NOMONSTERS, this.realowner);
490         locout = trace_endpos;
491
492         makevectors(this.realowner.angles);
493
494         MUTATOR_CALLHOOK(PortalTeleport, this.realowner);
495
496         TeleportPlayer(this, this.realowner, locout, this.realowner.angles, v_forward * vlen(this.realowner.velocity), '0 0 0', '0 0 0', TELEPORT_FLAGS_TELEPORTER);
497 }
498
499 void nade_spawn_boom(entity this)
500 {
501         entity spawnloc = spawn();
502         setorigin(spawnloc, this.origin);
503         setsize(spawnloc, this.realowner.mins, this.realowner.maxs);
504         set_movetype(spawnloc, MOVETYPE_NONE);
505         spawnloc.solid = SOLID_NOT;
506         spawnloc.drawonlytoclient = this.realowner;
507         spawnloc.effects = EF_STARDUST;
508         spawnloc.cnt = autocvar_g_nades_spawn_count;
509
510         if(this.realowner.nade_spawnloc)
511         {
512                 delete(this.realowner.nade_spawnloc);
513                 this.realowner.nade_spawnloc = NULL;
514         }
515
516         this.realowner.nade_spawnloc = spawnloc;
517 }
518
519 void nades_orb_think(entity this)
520 {
521         if(time >= this.ltime)
522         {
523                 delete(this);
524                 return;
525         }
526
527         this.nextthink = time;
528
529         if(time >= this.nade_special_time)
530         {
531                 this.nade_special_time = time+0.25;
532                 this.nade_show_particles = 1;
533         }
534         else
535                 this.nade_show_particles = 0;
536 }
537
538 entity nades_spawn_orb(entity own, entity realown, vector org, float orb_ltime, float orb_rad)
539 {
540         // NOTE: this function merely places an orb
541         // you must add a custom touch function to the returned entity if desired
542         // also set .colormod if you wish to have it colorized
543         entity orb = spawn(); // Net_LinkEntity sets the classname (TODO)
544         orb.owner = own;
545         orb.realowner = realown;
546         setorigin(orb, org);
547
548         orb.orb_lifetime = orb_ltime; // required for timers
549         orb.ltime = time + orb.orb_lifetime;
550         orb.bot_dodge = false;
551         orb.team = realown.team;
552         orb.solid = SOLID_TRIGGER;
553
554         setmodel(orb, MDL_NADE_ORB);
555         orb.skin = 1;
556         orb.orb_radius = orb_rad; // required for fading
557         vector size = '1 1 1' * orb.orb_radius / 2;
558         setsize(orb, -size, size);
559
560         Net_LinkEntity(orb, true, 0, orb_send);
561         orb.SendFlags |= 1;
562
563         setthink(orb, nades_orb_think);
564         orb.nextthink = time;
565
566         return orb;
567 }
568
569 void nade_entrap_touch(entity this, entity toucher)
570 {
571         if(DIFF_TEAM(toucher, this.realowner)) // TODO: what if realowner changes team or disconnects?
572         {
573                 if (!isPushable(toucher))
574                         return;
575
576                 float pushdeltatime = time - toucher.lastpushtime;
577                 if (pushdeltatime > 0.15) pushdeltatime = 0;
578                 toucher.lastpushtime = time;
579                 if(!pushdeltatime) return;
580
581                 // div0: ticrate independent, 1 = identity (not 20)
582                 toucher.velocity = toucher.velocity * (autocvar_g_nades_entrap_strength ** pushdeltatime);
583
584         #ifdef SVQC
585                 UpdateCSQCProjectile(toucher);
586         #endif
587         }
588
589         if ( IS_REAL_CLIENT(toucher) || IS_VEHICLE(toucher) || IS_MONSTER(toucher) )
590         {
591                 entity show_tint = (IS_VEHICLE(toucher)) ? toucher.owner : toucher;
592                 STAT(ENTRAP_ORB, show_tint) = time + 0.1;
593
594                 float tint_alpha = 0.75;
595                 if(SAME_TEAM(toucher, this.realowner))
596                         tint_alpha = 0.45;
597                 STAT(ENTRAP_ORB_ALPHA, show_tint) = tint_alpha * (this.ltime - time) / this.orb_lifetime;
598         }
599 }
600
601 void nade_entrap_boom(entity this)
602 {
603         entity orb = nades_spawn_orb(this.owner, this.realowner, this.origin, autocvar_g_nades_entrap_time, autocvar_g_nades_entrap_radius);
604
605         settouch(orb, nade_entrap_touch);
606         orb.colormod = NADE_TYPE_ENTRAP.m_color;
607 }
608
609 void nade_heal_touch(entity this, entity toucher)
610 {
611         float maxhealth;
612         float health_factor;
613         if(IS_PLAYER(toucher) || IS_MONSTER(toucher))
614         if(!IS_DEAD(toucher))
615         if(!STAT(FROZEN, toucher))
616         {
617                 health_factor = autocvar_g_nades_heal_rate*frametime/2;
618                 if ( toucher != this.realowner )
619                 {
620                         if ( SAME_TEAM(toucher,this) )
621                                 health_factor *= autocvar_g_nades_heal_friend;
622                         else
623                                 health_factor *= autocvar_g_nades_heal_foe;
624                 }
625                 if ( health_factor > 0 )
626                 {
627                         maxhealth = (IS_MONSTER(toucher)) ? toucher.max_health : g_pickup_healthmega_max;
628                         float hp = GetResourceAmount(toucher, RESOURCE_HEALTH);
629                         if (hp < maxhealth)
630                         {
631                                 if (this.nade_show_particles)
632                                 {
633                                         Send_Effect(EFFECT_HEALING, toucher.origin, '0 0 0', 1);
634                                 }
635                                 GiveResourceWithLimit(toucher, RESOURCE_HEALTH, health_factor, maxhealth);
636                         }
637                 }
638                 else if ( health_factor < 0 )
639                 {
640                         Damage(toucher,this,this.realowner,-health_factor,DEATH_NADE_HEAL.m_id,DMG_NOWEP,toucher.origin,'0 0 0');
641                 }
642
643         }
644
645         if ( IS_REAL_CLIENT(toucher) || IS_VEHICLE(toucher) )
646         {
647                 entity show_red = (IS_VEHICLE(toucher)) ? toucher.owner : toucher;
648                 show_red.stat_healing_orb = time+0.1;
649                 show_red.stat_healing_orb_alpha = 0.75 * (this.ltime - time) / this.orb_lifetime;
650         }
651 }
652
653 void nade_heal_boom(entity this)
654 {
655         entity orb = nades_spawn_orb(this.owner, this.realowner, this.origin, autocvar_g_nades_heal_time, autocvar_g_nades_nade_radius);
656
657         settouch(orb, nade_heal_touch);
658         orb.colormod = '1 0 0';
659 }
660
661 void nade_monster_boom(entity this)
662 {
663         entity e = spawnmonster(spawn(), this.pokenade_type, 0, this.realowner, this.realowner, this.origin, false, false, 1);
664
665         if(autocvar_g_nades_pokenade_monster_lifetime > 0)
666                 e.monster_lifetime = time + autocvar_g_nades_pokenade_monster_lifetime;
667         e.monster_skill = MONSTER_SKILL_INSANE;
668 }
669
670 void nade_boom(entity this)
671 {
672         entity expef = NULL;
673         bool nade_blast = true;
674
675         switch ( Nades_from(this.nade_type) )
676         {
677                 case NADE_TYPE_NAPALM:
678                         nade_blast = autocvar_g_nades_napalm_blast;
679                         expef = EFFECT_EXPLOSION_MEDIUM;
680                         break;
681                 case NADE_TYPE_ICE:
682                         nade_blast = false;
683                         expef = EFFECT_ELECTRO_COMBO; // hookbomb_explode electro_combo bigplasma_impact
684                         break;
685                 case NADE_TYPE_TRANSLOCATE:
686                         nade_blast = false;
687                         break;
688                 case NADE_TYPE_MONSTER:
689                 case NADE_TYPE_SPAWN:
690                         nade_blast = false;
691                         switch(this.realowner.team)
692                         {
693                                 case NUM_TEAM_1: expef = EFFECT_SPAWN_RED; break;
694                                 case NUM_TEAM_2: expef = EFFECT_SPAWN_BLUE; break;
695                                 case NUM_TEAM_3: expef = EFFECT_SPAWN_YELLOW; break;
696                                 case NUM_TEAM_4: expef = EFFECT_SPAWN_PINK; break;
697                                 default: expef = EFFECT_SPAWN_NEUTRAL; break;
698                         }
699                         break;
700                 case NADE_TYPE_HEAL:
701                         nade_blast = false;
702                         expef = EFFECT_SPAWN_RED;
703                         break;
704
705                 case NADE_TYPE_ENTRAP:
706                         nade_blast = false;
707                         expef = EFFECT_SPAWN_YELLOW;
708                         break;
709
710                 default:
711                 case NADE_TYPE_NORMAL:
712                         expef = EFFECT_NADE_EXPLODE(this.realowner.team);
713                         break;
714         }
715
716         if(expef)
717                 Send_Effect(expef, findbetterlocation(this.origin, 8), '0 0 0', 1);
718
719         sound(this, CH_SHOTS_SINGLE, SND_Null, VOL_BASE, ATTEN_NORM);
720         sound(this, CH_SHOTS, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_NORM);
721
722         this.event_damage = func_null; // prevent somehow calling damage in the next call
723
724         if(nade_blast)
725         {
726                 RadiusDamage(this, this.realowner, autocvar_g_nades_nade_damage, autocvar_g_nades_nade_edgedamage,
727                                  autocvar_g_nades_nade_radius, this, NULL, autocvar_g_nades_nade_force, this.projectiledeathtype, DMG_NOWEP, this.enemy);
728                 Damage_DamageInfo(this.origin, autocvar_g_nades_nade_damage, autocvar_g_nades_nade_edgedamage, autocvar_g_nades_nade_radius, '1 1 1' * autocvar_g_nades_nade_force, this.projectiledeathtype, 0, this);
729         }
730
731         if(this.takedamage)
732         switch ( Nades_from(this.nade_type) )
733         {
734                 case NADE_TYPE_NAPALM: nade_napalm_boom(this); break;
735                 case NADE_TYPE_ICE: nade_ice_boom(this); break;
736                 case NADE_TYPE_TRANSLOCATE: nade_translocate_boom(this); break;
737                 case NADE_TYPE_SPAWN: nade_spawn_boom(this); break;
738                 case NADE_TYPE_HEAL: nade_heal_boom(this); break;
739                 case NADE_TYPE_MONSTER: nade_monster_boom(this); break;
740                 case NADE_TYPE_ENTRAP: nade_entrap_boom(this); break;
741         }
742
743         IL_EACH(g_projectiles, it.classname == "grapplinghook" && it.aiment == this,
744         {
745                 RemoveHook(it);
746         });
747
748         delete(this);
749 }
750
751 void spawn_held_nade(entity player, entity nowner, float ntime, int ntype, string pntype);
752 void nade_pickup(entity this, entity thenade)
753 {
754         spawn_held_nade(this, thenade.realowner, autocvar_g_nades_pickup_time, thenade.nade_type, thenade.pokenade_type);
755
756         // set refire so player can't even
757         this.nade_refire = time + autocvar_g_nades_nade_refire;
758         this.nade_timer = 0;
759
760         if(this.nade)
761                 this.nade.nade_time_primed = thenade.nade_time_primed;
762 }
763
764 bool CanThrowNade(entity this);
765 void nade_touch(entity this, entity toucher)
766 {
767         if(toucher)
768                 UpdateCSQCProjectile(this);
769
770         if(toucher == this.realowner)
771                 return; // no this impacts
772
773         if(autocvar_g_nades_pickup)
774         if(time >= this.spawnshieldtime)
775         if(!toucher.nade && GetResourceAmount(this, RESOURCE_HEALTH) == this.max_health) // no boosted shot pickups, thank you very much
776         if(CanThrowNade(toucher)) // prevent some obvious things, like dead players
777         if(IS_REAL_CLIENT(toucher)) // above checks for IS_PLAYER, don't need to do it here
778         {
779                 nade_pickup(toucher, this);
780                 sound(this, CH_SHOTS_SINGLE, SND_Null, VOL_BASE, 0.5 *(ATTEN_LARGE + ATTEN_MAX));
781                 delete(this);
782                 return;
783         }
784         /*float is_weapclip = 0;
785         if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_NODRAW)
786         if (!(trace_dphitq3surfaceflags & Q3SURFACEFLAG_NONSOLID))
787         if (!(trace_dphitcontents & DPCONTENTS_OPAQUE))
788                 is_weapclip = 1;*/
789         if(ITEM_TOUCH_NEEDKILL()) // || is_weapclip)
790         {
791                 IL_EACH(g_projectiles, it.classname == "grapplinghook" && it.aiment == this,
792                 {
793                         RemoveHook(it);
794                 });
795                 delete(this);
796                 return;
797         }
798
799         PROJECTILE_TOUCH(this, toucher);
800
801         //setsize(this, '-2 -2 -2', '2 2 2');
802         //UpdateCSQCProjectile(this);
803         if(GetResourceAmount(this, RESOURCE_HEALTH) == this.max_health)
804         {
805                 spamsound(this, CH_SHOTS, SND_GRENADE_BOUNCE_RANDOM(), VOL_BASE, ATTEN_NORM);
806                 return;
807         }
808
809         this.enemy = toucher;
810         nade_boom(this);
811 }
812
813 void nade_beep(entity this)
814 {
815         sound(this, CH_SHOTS_SINGLE, SND_NADE_BEEP, VOL_BASE, 0.5 *(ATTEN_LARGE + ATTEN_MAX));
816         setthink(this, nade_boom);
817         this.nextthink = max(this.wait, time);
818 }
819
820 void nade_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
821 {
822         if(ITEM_DAMAGE_NEEDKILL(deathtype))
823         {
824                 this.takedamage = DAMAGE_NO;
825                 nade_boom(this);
826                 return;
827         }
828
829         if(this.nade_type == NADE_TYPE_TRANSLOCATE.m_id || this.nade_type == NADE_TYPE_SPAWN.m_id)
830                 return;
831
832         if (MUTATOR_CALLHOOK(Nade_Damage, this, DEATH_WEAPONOF(deathtype), force, damage)) {}
833         else if(DEATH_ISWEAPON(deathtype, WEP_BLASTER))
834         {
835                 force *= 1.5;
836                 damage = 0;
837         }
838         else if(DEATH_ISWEAPON(deathtype, WEP_VAPORIZER) && (deathtype & HITTYPE_SECONDARY))
839         {
840                 force *= 0.5; // too much
841                 damage = 0;
842         }
843         else if(DEATH_ISWEAPON(deathtype, WEP_VORTEX) || DEATH_ISWEAPON(deathtype, WEP_VAPORIZER) || DEATH_ISWEAPON(deathtype, WEP_OVERKILL_NEX))
844         {
845                 force *= 6;
846                 damage = this.max_health * 0.55;
847         }
848         else if(DEATH_ISWEAPON(deathtype, WEP_MACHINEGUN) || DEATH_ISWEAPON(deathtype, WEP_OVERKILL_MACHINEGUN))
849                 damage = this.max_health * 0.1;
850         else if(DEATH_ISWEAPON(deathtype, WEP_SHOCKWAVE) || DEATH_ISWEAPON(deathtype, WEP_SHOTGUN)) // WEAPONTODO
851         {
852                 if(deathtype & HITTYPE_SECONDARY)
853                 {
854                         damage = this.max_health * 0.1;
855                         force *= 10;
856                 }
857                 else
858                         damage = this.max_health * 1.15;
859         }
860
861         this.velocity += force;
862         UpdateCSQCProjectile(this);
863
864         if(damage <= 0 || ((IS_ONGROUND(this)) && IS_PLAYER(attacker)))
865                 return;
866
867         float hp = GetResourceAmount(this, RESOURCE_HEALTH);
868         if(hp == this.max_health)
869         {
870                 sound(this, CH_SHOTS_SINGLE, SND_Null, VOL_BASE, 0.5 *(ATTEN_LARGE + ATTEN_MAX));
871                 this.nextthink = max(time + this.nade_lifetime, time);
872                 setthink(this, nade_beep);
873         }
874
875         hp -= damage;
876         SetResourceAmount(this, RESOURCE_HEALTH, hp);
877
878
879         if ( this.nade_type != NADE_TYPE_HEAL.m_id || IS_PLAYER(attacker) )
880                 this.realowner = attacker;
881
882         if(hp <= 0)
883                 W_PrepareExplosionByDamage(this, attacker, nade_boom);
884         else
885                 nade_burn_spawn(this);
886 }
887
888 void toss_nade(entity e, bool set_owner, vector _velocity, float _time)
889 {
890         if(e.nade == NULL)
891                 return;
892
893         entity _nade = e.nade;
894         e.nade = NULL;
895
896         delete(e.fake_nade);
897         e.fake_nade = NULL;
898
899         Kill_Notification(NOTIF_ONE_ONLY, e, MSG_CENTER, CPID_NADES);
900
901         makevectors(e.v_angle);
902
903         // NOTE: always throw from first weapon entity?
904         W_SetupShot(e, _nade.weaponentity_fld, false, false, SND_Null, CH_WEAPON_A, 0, DEATH_NADE.m_id);
905
906         vector offset = (v_forward * autocvar_g_nades_throw_offset.x)
907                       + (v_right * autocvar_g_nades_throw_offset.y)
908                       + (v_up * autocvar_g_nades_throw_offset.z);
909
910         setorigin(_nade, w_shotorg + offset);
911         //setmodel(_nade, MDL_PROJECTILE_NADE);
912         //setattachment(_nade, NULL, "");
913         PROJECTILE_MAKETRIGGER(_nade);
914         if(STAT(NADES_SMALL, e))
915                 setsize(_nade, '-8 -8 -8', '8 8 8');
916         else
917                 setsize(_nade, '-16 -16 -16', '16 16 16');
918         set_movetype(_nade, MOVETYPE_BOUNCE);
919
920         tracebox(_nade.origin, _nade.mins, _nade.maxs, _nade.origin, MOVE_NOMONSTERS, _nade);
921         if (trace_startsolid)
922                 setorigin(_nade, e.origin);
923
924         if(e.v_angle.x >= 70 && e.v_angle.x <= 110 && PHYS_INPUT_BUTTON_CROUCH(e))
925                 _nade.velocity = '0 0 100';
926         else if(autocvar_g_nades_nade_newton_style == 1)
927                 _nade.velocity = e.velocity + _velocity;
928         else if(autocvar_g_nades_nade_newton_style == 2)
929                 _nade.velocity = _velocity;
930         else
931                 _nade.velocity = W_CalculateProjectileVelocity(e, e.velocity, _velocity, true);
932
933         if(set_owner)
934                 _nade.realowner = e;
935
936         settouch(_nade, nade_touch);
937         _nade.spawnshieldtime = time + 0.1; // prevent instantly picking up again
938         SetResourceAmount(_nade, RESOURCE_HEALTH, autocvar_g_nades_nade_health);
939         _nade.max_health = _nade.health;
940         _nade.takedamage = DAMAGE_AIM;
941         _nade.event_damage = nade_damage;
942         setcefc(_nade, func_null);
943         _nade.exteriormodeltoclient = NULL;
944         _nade.traileffectnum = 0;
945         _nade.teleportable = true;
946         _nade.pushable = true;
947         _nade.gravity = 1;
948         _nade.missile_flags = MIF_SPLASH | MIF_ARC;
949         _nade.damagedbycontents = true;
950         IL_PUSH(g_damagedbycontents, _nade);
951         _nade.angles = vectoangles(_nade.velocity);
952         _nade.flags = FL_PROJECTILE;
953         IL_PUSH(g_projectiles, _nade);
954         IL_PUSH(g_bot_dodge, _nade);
955         _nade.projectiledeathtype = DEATH_NADE.m_id;
956         _nade.toss_time = time;
957         _nade.solid = SOLID_CORPSE; //((_nade.nade_type == NADE_TYPE_TRANSLOCATE) ? SOLID_CORPSE : SOLID_BBOX);
958
959         if(_nade.nade_type == NADE_TYPE_TRANSLOCATE.m_id || _nade.nade_type == NADE_TYPE_SPAWN.m_id)
960                 _nade.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP;
961         else
962                 _nade.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY;
963
964         nade_spawn(_nade);
965
966         if(_time)
967         {
968                 setthink(_nade, nade_boom);
969                 _nade.nextthink = _time;
970         }
971
972         e.nade_refire = time + autocvar_g_nades_nade_refire;
973         e.nade_timer = 0;
974 }
975
976 void nades_GiveBonus(entity player, float score)
977 {
978         if (autocvar_g_nades)
979         if (autocvar_g_nades_bonus)
980         if (IS_REAL_CLIENT(player))
981         if (IS_PLAYER(player) && player.bonus_nades < autocvar_g_nades_bonus_max)
982         if (STAT(FROZEN, player) == 0)
983         if (!IS_DEAD(player))
984         {
985                 if ( player.bonus_nade_score < 1 )
986                         player.bonus_nade_score += score/autocvar_g_nades_bonus_score_max;
987
988                 if ( player.bonus_nade_score >= 1 )
989                 {
990                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_NADE_BONUS);
991                         play2(player, SND(KH_ALARM));
992                         player.bonus_nades++;
993                         player.bonus_nade_score -= 1;
994                 }
995         }
996 }
997
998 /** Remove all bonus nades from a player */
999 void nades_RemoveBonus(entity player)
1000 {
1001         player.bonus_nades = player.bonus_nade_score = 0;
1002 }
1003
1004 MUTATOR_HOOKFUNCTION(nades, PutClientInServer)
1005 {
1006     entity player = M_ARGV(0, entity);
1007
1008         nades_RemoveBonus(player);
1009 }
1010
1011 bool nade_customize(entity this, entity client)
1012 {
1013         //if(IS_SPEC(client)) { return false; }
1014         if(client == this.exteriormodeltoclient || (IS_SPEC(client) && client.enemy == this.exteriormodeltoclient))
1015         {
1016                 // somewhat hide the model, but keep the glow
1017                 //this.effects = 0;
1018                 if(this.traileffectnum)
1019                         this.traileffectnum = 0;
1020                 this.alpha = -1;
1021         }
1022         else
1023         {
1024                 //this.effects = EF_ADDITIVE | EF_FULLBRIGHT | EF_LOWPRECISION;
1025                 if(!this.traileffectnum)
1026                         this.traileffectnum = _particleeffectnum(Nade_TrailEffect(Nades_from(this.nade_type).m_projectile[false], this.team).eent_eff_name);
1027                 this.alpha = 1;
1028         }
1029
1030         return true;
1031 }
1032
1033 void spawn_held_nade(entity player, entity nowner, float ntime, int ntype, string pntype)
1034 {
1035         entity n = new(nade), fn = new(fake_nade);
1036
1037         n.nade_type = max(1, ntype);
1038         n.pokenade_type = pntype;
1039
1040         if(Nades_from(n.nade_type) == NADE_TYPE_Null)
1041                 n.nade_type = NADE_TYPE_NORMAL.m_id;
1042
1043         .entity weaponentity = weaponentities[0]; // TODO: unhardcode
1044
1045         setmodel(n, MDL_PROJECTILE_NADE);
1046         //setattachment(n, player, "bip01 l hand");
1047         n.exteriormodeltoclient = player;
1048         setcefc(n, nade_customize);
1049         n.traileffectnum = _particleeffectnum(Nade_TrailEffect(Nades_from(n.nade_type).m_projectile[false], player.team).eent_eff_name);
1050         n.colormod = Nades_from(n.nade_type).m_color;
1051         n.realowner = nowner;
1052         n.colormap = player.colormap;
1053         n.glowmod = player.glowmod;
1054         n.wait = time + max(0, ntime);
1055         n.nade_time_primed = time;
1056         setthink(n, nade_beep);
1057         n.nextthink = max(n.wait - 3, time);
1058         n.projectiledeathtype = DEATH_NADE.m_id;
1059         n.weaponentity_fld = weaponentity;
1060         n.nade_lifetime = ntime;
1061
1062         setmodel(fn, MDL_NADE_VIEW);
1063         setattachment(fn, player.(weaponentity), "");
1064         fn.realowner = fn.owner = player;
1065         fn.colormod = Nades_from(n.nade_type).m_color;
1066         fn.colormap = player.colormap;
1067         fn.glowmod = player.glowmod;
1068         setthink(fn, SUB_Remove);
1069         fn.nextthink = n.wait;
1070         fn.weaponentity_fld = weaponentity;
1071
1072         player.nade = n;
1073         player.fake_nade = fn;
1074 }
1075
1076 void nade_prime(entity this)
1077 {
1078         if(autocvar_g_nades_bonus_only)
1079         if(!this.bonus_nades)
1080                 return; // only allow bonus nades
1081
1082         if(this.nade)
1083                 delete(this.nade);
1084
1085         if(this.fake_nade)
1086                 delete(this.fake_nade);
1087
1088         int ntype;
1089         string pntype = this.pokenade_type;
1090
1091         if(this.items & ITEM_Strength.m_itemid && autocvar_g_nades_bonus_onstrength)
1092                 ntype = this.nade_type;
1093         else if (this.bonus_nades >= 1)
1094         {
1095                 ntype = this.nade_type;
1096                 pntype = this.pokenade_type;
1097                 this.bonus_nades -= 1;
1098         }
1099         else
1100         {
1101                 ntype = ((autocvar_g_nades_client_select) ? CS(this).cvar_cl_nade_type : autocvar_g_nades_nade_type);
1102                 pntype = ((autocvar_g_nades_client_select) ? CS(this).cvar_cl_pokenade_type : autocvar_g_nades_pokenade_monster_type);
1103         }
1104
1105         spawn_held_nade(this, this, autocvar_g_nades_nade_lifetime, ntype, pntype);
1106 }
1107
1108 bool CanThrowNade(entity this)
1109 {
1110         if(this.vehicle)
1111                 return false;
1112
1113         if(IS_DEAD(this))
1114                 return false;
1115
1116         if (!autocvar_g_nades)
1117                 return false; // allow turning them off mid match
1118
1119         if(forbidWeaponUse(this))
1120                 return false;
1121
1122         if (!IS_PLAYER(this))
1123                 return false;
1124
1125         return true;
1126 }
1127
1128 .bool nade_altbutton;
1129
1130 void nades_CheckThrow(entity this)
1131 {
1132         if(!CanThrowNade(this))
1133                 return;
1134
1135         entity held_nade = this.nade;
1136         if (!held_nade)
1137         {
1138                 this.nade_altbutton = true;
1139                 if(time > this.nade_refire)
1140                 {
1141                         Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_NADE_THROW);
1142                         nade_prime(this);
1143                         this.nade_refire = time + autocvar_g_nades_nade_refire;
1144                 }
1145         }
1146         else
1147         {
1148                 this.nade_altbutton = false;
1149                 if (time >= held_nade.nade_time_primed + 1) {
1150                         makevectors(this.v_angle);
1151                         float _force = time - held_nade.nade_time_primed;
1152                         _force /= autocvar_g_nades_nade_lifetime;
1153                         _force = autocvar_g_nades_nade_minforce + (_force * (autocvar_g_nades_nade_maxforce - autocvar_g_nades_nade_minforce));
1154                         vector dir = (v_forward * 0.75 + v_up * 0.2 + v_right * 0.05);
1155                         dir = W_CalculateSpread(dir, autocvar_g_nades_spread, g_weaponspreadfactor, autocvar_g_projectiles_spread_style);
1156                         toss_nade(this, true, dir * _force, 0);
1157                 }
1158         }
1159 }
1160
1161 void nades_Clear(entity player)
1162 {
1163         if(player.nade)
1164                 delete(player.nade);
1165         if(player.fake_nade)
1166                 delete(player.fake_nade);
1167
1168         player.nade = player.fake_nade = NULL;
1169         player.nade_timer = 0;
1170 }
1171
1172 MUTATOR_HOOKFUNCTION(nades, VehicleEnter)
1173 {
1174         entity player = M_ARGV(0, entity);
1175
1176         if(player.nade)
1177                 toss_nade(player, true, '0 0 100', max(player.nade.wait, time + 0.05));
1178 }
1179
1180 CLASS(NadeOffhand, OffhandWeapon)
1181     METHOD(NadeOffhand, offhand_think, void(NadeOffhand this, entity player, bool key_pressed))
1182     {
1183         entity held_nade = player.nade;
1184
1185         if (!CanThrowNade(player)) return;
1186         if (!(time > player.nade_refire)) return;
1187                 if (key_pressed) {
1188                         if (!held_nade) {
1189                                 nade_prime(player);
1190                                 held_nade = player.nade;
1191                         }
1192                 } else if (time >= held_nade.nade_time_primed + 1) {
1193                         if (held_nade) {
1194                                 makevectors(player.v_angle);
1195                                 float _force = time - held_nade.nade_time_primed;
1196                                 _force /= autocvar_g_nades_nade_lifetime;
1197                                 _force = autocvar_g_nades_nade_minforce + (_force * (autocvar_g_nades_nade_maxforce - autocvar_g_nades_nade_minforce));
1198                                 vector dir = (v_forward * 0.7 + v_up * 0.2 + v_right * 0.1);
1199                                 dir = W_CalculateSpread(dir, autocvar_g_nades_spread, g_weaponspreadfactor, autocvar_g_projectiles_spread_style);
1200                                 toss_nade(player, false, dir * _force, 0);
1201                         }
1202                 }
1203     }
1204 ENDCLASS(NadeOffhand)
1205 NadeOffhand OFFHAND_NADE; STATIC_INIT(OFFHAND_NADE) { OFFHAND_NADE = NEW(NadeOffhand); }
1206
1207 MUTATOR_HOOKFUNCTION(nades, ForbidThrowCurrentWeapon, CBC_ORDER_LAST)
1208 {
1209     entity player = M_ARGV(0, entity);
1210
1211         if (player.offhand != OFFHAND_NADE || (player.weapons & WEPSET(HOOK)) || autocvar_g_nades_override_dropweapon) {
1212                 nades_CheckThrow(player);
1213                 return true;
1214         }
1215 }
1216
1217 MUTATOR_HOOKFUNCTION(nades, PlayerPreThink)
1218 {
1219         entity player = M_ARGV(0, entity);
1220
1221         if (!IS_PLAYER(player)) { return; }
1222
1223         if (player.nade && (player.offhand != OFFHAND_NADE || (player.weapons & WEPSET(HOOK)))) OFFHAND_NADE.offhand_think(OFFHAND_NADE, player, player.nade_altbutton);
1224
1225         entity held_nade = player.nade;
1226         if (held_nade)
1227         {
1228                 player.nade_timer = bound(0, (time - held_nade.nade_time_primed) / held_nade.nade_lifetime, 1);
1229                 // LOG_TRACEF("%d %d", player.nade_timer, time - held_nade.nade_time_primed);
1230                 makevectors(player.angles);
1231                 held_nade.velocity = player.velocity;
1232                 setorigin(held_nade, player.origin + player.view_ofs + v_forward * 8 + v_right * -8 + v_up * 0);
1233                 held_nade.angles_y = player.angles.y;
1234
1235                 if (time + 0.1 >= held_nade.wait)
1236                         toss_nade(player, false, '0 0 0', time + 0.05);
1237         }
1238
1239         if(IS_PLAYER(player))
1240         {
1241                 if ( autocvar_g_nades_bonus && autocvar_g_nades )
1242                 {
1243                         entity key;
1244                         float key_count = 0;
1245                         FOR_EACH_KH_KEY(key) if(key.owner == player) { ++key_count; }
1246
1247                         float time_score;
1248                         if(GameRules_scoring_is_vip(player))
1249                                 time_score = autocvar_g_nades_bonus_score_time_flagcarrier;
1250                         else
1251                                 time_score = autocvar_g_nades_bonus_score_time;
1252
1253                         if(key_count)
1254                                 time_score = autocvar_g_nades_bonus_score_time_flagcarrier * key_count; // multiply by the number of keys the player is holding
1255
1256                         if(autocvar_g_nades_bonus_client_select)
1257                         {
1258                                 player.nade_type = CS(player).cvar_cl_nade_type;
1259                                 player.pokenade_type = CS(player).cvar_cl_pokenade_type;
1260                         }
1261                         else
1262                         {
1263                                 player.nade_type = autocvar_g_nades_bonus_type;
1264                                 player.pokenade_type = autocvar_g_nades_pokenade_monster_type;
1265                         }
1266
1267                         player.nade_type = bound(1, player.nade_type, Nades_COUNT);
1268
1269                         if(player.bonus_nade_score >= 0 && autocvar_g_nades_bonus_score_max)
1270                                 nades_GiveBonus(player, time_score / autocvar_g_nades_bonus_score_max);
1271                 }
1272                 else
1273                 {
1274                         player.bonus_nades = player.bonus_nade_score = 0;
1275                 }
1276         }
1277
1278         int n = 0;
1279         entity o = NULL;
1280         if(player.freezetag_frozen_timeout > 0 && time >= player.freezetag_frozen_timeout)
1281                 n = -1;
1282         else if(STAT(FROZEN, player) == 3)
1283         {
1284                 vector revive_extra_size = '1 1 1' * autocvar_g_freezetag_revive_extra_size;
1285                 n = 0;
1286                 FOREACH_CLIENT(IS_PLAYER(it) && it != player, {
1287                         if(!IS_DEAD(it) && STAT(FROZEN, it) == 0 && SAME_TEAM(it, player))
1288                         if(boxesoverlap(player.absmin - revive_extra_size, player.absmax + revive_extra_size, it.absmin, it.absmax))
1289                         {
1290                                 if(!o)
1291                                         o = it;
1292                                 it.reviving = true;
1293                                 ++n;
1294                         }
1295                 });
1296         }
1297
1298         if(n > 0 && STAT(FROZEN, player) == 3) // OK, there is at least one teammate reviving us
1299         {
1300                 player.revive_progress = bound(0, player.revive_progress + frametime * max(1/60, autocvar_g_freezetag_revive_speed), 1);
1301                 SetResourceAmount(player, RESOURCE_HEALTH, max(1, player.revive_progress * start_health));
1302
1303                 if(player.revive_progress >= 1)
1304                 {
1305                         Unfreeze(player);
1306
1307                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_REVIVED, o.netname);
1308                         Send_Notification(NOTIF_ONE, o, MSG_CENTER, CENTER_FREEZETAG_REVIVE, player.netname);
1309                 }
1310
1311                 FOREACH_CLIENT(IS_PLAYER(it) && it.reviving, {
1312                         it.revive_progress = player.revive_progress;
1313                         it.reviving = false;
1314                 });
1315         }
1316 }
1317
1318 MUTATOR_HOOKFUNCTION(nades, PlayerPhysics)
1319 {
1320         entity player = M_ARGV(0, entity);
1321
1322         if (STAT(ENTRAP_ORB, player) > time)
1323         {
1324                 player.stat_sv_maxspeed *= autocvar_g_nades_entrap_speed;
1325                 player.stat_sv_airspeedlimit_nonqw *= autocvar_g_nades_entrap_speed;
1326         }
1327 }
1328
1329 MUTATOR_HOOKFUNCTION(nades, MonsterMove)
1330 {
1331     entity mon = M_ARGV(0, entity);
1332
1333         if (STAT(ENTRAP_ORB, mon) > time)
1334         {
1335                 M_ARGV(1, float) *= autocvar_g_nades_entrap_speed; // run speed
1336                 M_ARGV(2, float) *= autocvar_g_nades_entrap_speed; // walk speed
1337         }
1338 }
1339
1340 MUTATOR_HOOKFUNCTION(nades, PlayerSpawn)
1341 {
1342         entity player = M_ARGV(0, entity);
1343
1344         if(autocvar_g_nades_spawn)
1345                 player.nade_refire = time + autocvar_g_spawnshieldtime;
1346         else
1347                 player.nade_refire  = time + autocvar_g_nades_nade_refire;
1348
1349         if(autocvar_g_nades_bonus_client_select)
1350                 player.nade_type = CS(player).cvar_cl_nade_type;
1351
1352         player.nade_timer = 0;
1353
1354         if (!player.offhand) player.offhand = OFFHAND_NADE;
1355
1356         if(player.nade_spawnloc)
1357         {
1358                 setorigin(player, player.nade_spawnloc.origin);
1359                 player.nade_spawnloc.cnt -= 1;
1360
1361                 if(player.nade_spawnloc.cnt <= 0)
1362                 {
1363                         delete(player.nade_spawnloc);
1364                         player.nade_spawnloc = NULL;
1365                 }
1366         }
1367 }
1368
1369 MUTATOR_HOOKFUNCTION(nades, PlayerDies, CBC_ORDER_LAST)
1370 {
1371         entity frag_attacker = M_ARGV(1, entity);
1372         entity frag_target = M_ARGV(2, entity);
1373
1374         if(frag_target.nade)
1375         if(!STAT(FROZEN, frag_target) || !autocvar_g_freezetag_revive_nade)
1376                 toss_nade(frag_target, true, '0 0 100', max(frag_target.nade.wait, time + 0.05));
1377
1378         if(IS_PLAYER(frag_attacker))
1379         {
1380                 float killcount_bonus = ((CS(frag_attacker).killcount >= 1) ? bound(0, autocvar_g_nades_bonus_score_minor * CS(frag_attacker).killcount, autocvar_g_nades_bonus_score_medium) : autocvar_g_nades_bonus_score_minor);
1381
1382                 if (SAME_TEAM(frag_attacker, frag_target) || frag_attacker == frag_target)
1383                         nades_RemoveBonus(frag_attacker);
1384                 else if(GameRules_scoring_is_vip(frag_target))
1385                         nades_GiveBonus(frag_attacker, autocvar_g_nades_bonus_score_medium);
1386                 else if(autocvar_g_nades_bonus_score_spree && CS(frag_attacker).killcount > 1)
1387                 {
1388                         #define SPREE_ITEM(counta,countb,center,normal,gentle) \
1389                                 case counta: { nades_GiveBonus(frag_attacker, autocvar_g_nades_bonus_score_spree); break; }
1390                         switch(CS(frag_attacker).killcount)
1391                         {
1392                                 KILL_SPREE_LIST
1393                                 default: nades_GiveBonus(frag_attacker, autocvar_g_nades_bonus_score_minor); break;
1394                         }
1395                         #undef SPREE_ITEM
1396                 }
1397                 else
1398                         nades_GiveBonus(frag_attacker, killcount_bonus);
1399         }
1400
1401         nades_RemoveBonus(frag_target);
1402 }
1403
1404 MUTATOR_HOOKFUNCTION(nades, Damage_Calculate)
1405 {
1406         entity frag_inflictor = M_ARGV(0, entity);
1407         entity frag_attacker = M_ARGV(1, entity);
1408         entity frag_target = M_ARGV(2, entity);
1409         float frag_deathtype = M_ARGV(3, float);
1410
1411         if(STAT(FROZEN, frag_target))
1412         if(autocvar_g_freezetag_revive_nade)
1413         if(frag_attacker == frag_target)
1414         if(frag_deathtype == DEATH_NADE.m_id)
1415         if(time - frag_inflictor.toss_time <= 0.1)
1416         {
1417                 Unfreeze(frag_target);
1418                 SetResourceAmount(frag_target, RESOURCE_HEALTH, autocvar_g_freezetag_revive_nade_health);
1419                 Send_Effect(EFFECT_ICEORGLASS, frag_target.origin, '0 0 0', 3);
1420                 M_ARGV(4, float) = 0;
1421                 M_ARGV(6, vector) = '0 0 0';
1422                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_NADE, frag_target.netname);
1423                 Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
1424         }
1425 }
1426
1427 MUTATOR_HOOKFUNCTION(nades, MonsterDies)
1428 {
1429         entity frag_target = M_ARGV(0, entity);
1430         entity frag_attacker = M_ARGV(1, entity);
1431
1432         if(IS_PLAYER(frag_attacker))
1433         if(DIFF_TEAM(frag_attacker, frag_target))
1434         if(!(frag_target.spawnflags & MONSTERFLAG_SPAWNED))
1435                 nades_GiveBonus(frag_attacker, autocvar_g_nades_bonus_score_minor);
1436 }
1437
1438 MUTATOR_HOOKFUNCTION(nades, DropSpecialItems)
1439 {
1440         entity frag_target = M_ARGV(0, entity);
1441
1442         if(frag_target.nade)
1443                 toss_nade(frag_target, true, '0 0 0', time + 0.05);
1444 }
1445
1446 void nades_RemovePlayer(entity this)
1447 {
1448         nades_Clear(this);
1449         nades_RemoveBonus(this);
1450 }
1451
1452 MUTATOR_HOOKFUNCTION(nades, MakePlayerObserver) { entity player = M_ARGV(0, entity); nades_RemovePlayer(player); }
1453 MUTATOR_HOOKFUNCTION(nades, ClientDisconnect) { entity player = M_ARGV(0, entity); nades_RemovePlayer(player); }
1454 MUTATOR_HOOKFUNCTION(nades, reset_map_global)
1455 {
1456         FOREACH_CLIENT(IS_PLAYER(it),
1457         {
1458                 nades_RemovePlayer(it);
1459         });
1460 }
1461
1462 MUTATOR_HOOKFUNCTION(nades, SpectateCopy)
1463 {
1464         entity spectatee = M_ARGV(0, entity);
1465         entity client = M_ARGV(1, entity);
1466
1467         client.nade_timer = spectatee.nade_timer;
1468         client.nade_type = spectatee.nade_type;
1469         client.pokenade_type = spectatee.pokenade_type;
1470         client.bonus_nades = spectatee.bonus_nades;
1471         client.bonus_nade_score = spectatee.bonus_nade_score;
1472         client.stat_healing_orb = spectatee.stat_healing_orb;
1473         client.stat_healing_orb_alpha = spectatee.stat_healing_orb_alpha;
1474         STAT(ENTRAP_ORB, client) = STAT(ENTRAP_ORB, spectatee);
1475         STAT(ENTRAP_ORB_ALPHA, client) = STAT(ENTRAP_ORB_ALPHA, spectatee);
1476 }
1477
1478 REPLICATE(cvar_cl_nade_type, int, "cl_nade_type");
1479 REPLICATE(cvar_cl_pokenade_type, string, "cl_pokenade_type");
1480
1481 MUTATOR_HOOKFUNCTION(nades, BuildMutatorsString)
1482 {
1483         M_ARGV(0, string) = strcat(M_ARGV(0, string), ":Nades");
1484 }
1485
1486 MUTATOR_HOOKFUNCTION(nades, BuildGameplayTipsString)
1487 {
1488         M_ARGV(0, string) = strcat(M_ARGV(0, string), "\n\n^3nades^8 are enabled, press 'g' to use them\n");
1489 }
1490
1491 #endif