]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/buffs/buffs.qc
Further reduce magnet buff's range, also fix it
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / mutators / mutator / buffs / buffs.qc
1 #ifndef MUTATOR_BUFFS_H
2 #define MUTATOR_BUFFS_H
3
4 #include "../instagib/module.inc"
5
6 bool  autocvar_g_buffs_effects;
7 float autocvar_g_buffs_waypoint_distance;
8 bool autocvar_g_buffs_randomize;
9 float autocvar_g_buffs_random_lifetime;
10 bool autocvar_g_buffs_random_location;
11 int autocvar_g_buffs_random_location_attempts;
12 int autocvar_g_buffs_spawn_count;
13 bool autocvar_g_buffs_replace_powerups;
14 float autocvar_g_buffs_cooldown_activate;
15 float autocvar_g_buffs_cooldown_respawn;
16 float autocvar_g_buffs_resistance_blockpercent;
17 float autocvar_g_buffs_medic_survive_chance;
18 float autocvar_g_buffs_medic_survive_health;
19 float autocvar_g_buffs_medic_rot;
20 float autocvar_g_buffs_medic_max;
21 float autocvar_g_buffs_medic_regen;
22 float autocvar_g_buffs_medic_heal_amount = 15;
23 float autocvar_g_buffs_medic_heal_delay = 1;
24 float autocvar_g_buffs_medic_heal_range = 400;
25 float autocvar_g_buffs_vengeance_damage_multiplier;
26 float autocvar_g_buffs_bash_force;
27 float autocvar_g_buffs_bash_force_self;
28 float autocvar_g_buffs_disability_slowtime;
29 float autocvar_g_buffs_disability_speed;
30 float autocvar_g_buffs_disability_rate;
31 float autocvar_g_buffs_disability_weaponspeed;
32 float autocvar_g_buffs_speed_speed;
33 float autocvar_g_buffs_speed_rate;
34 float autocvar_g_buffs_speed_weaponspeed;
35 float autocvar_g_buffs_speed_damage_take;
36 float autocvar_g_buffs_speed_regen;
37 float autocvar_g_buffs_vampire_damage_steal;
38 float autocvar_g_buffs_invisible_alpha;
39 float autocvar_g_buffs_flight_gravity;
40 float autocvar_g_buffs_jump_height;
41 float autocvar_g_buffs_inferno_burntime_factor;
42 float autocvar_g_buffs_inferno_burntime_min_time;
43 float autocvar_g_buffs_inferno_burntime_target_damage;
44 float autocvar_g_buffs_inferno_burntime_target_time;
45 float autocvar_g_buffs_inferno_damagemultiplier;
46 float autocvar_g_buffs_swapper_range;
47 float autocvar_g_buffs_magnet_range_item;
48 float autocvar_g_buffs_magnet_range_buff = 200;
49
50 // ammo
51 .float buff_ammo_prev_infitems;
52 .int buff_ammo_prev_clipload;
53 // invisible
54 .float buff_invisible_prev_alpha;
55 // flight
56 .float buff_flight_prev_gravity;
57 // medic
58 .float buff_medic_healtime;
59 // disability
60 .float buff_disability_time;
61 .float buff_disability_effect_time;
62 // common buff variables
63 .float buff_effect_delay;
64
65 // buff definitions
66 .float buff_active;
67 .float buff_activetime;
68 .float buff_activetime_updated;
69 .entity buff_waypoint;
70 .int oldbuffs; // for updating effects
71 .entity buff_model; // controls effects (TODO: make csqc)
72
73 const vector BUFF_MIN = ('-16 -16 -20');
74 const vector BUFF_MAX = ('16 16 20');
75
76 // client side options
77 .float cvar_cl_buffs_autoreplace;
78 #endif
79
80 #ifdef IMPLEMENTATION
81
82 #include <common/triggers/target/music.qh>
83 #include <common/gamemodes/all.qh>
84
85 .float buff_time = _STAT(BUFF_TIME);
86 void buffs_DelayedInit();
87
88 REGISTER_MUTATOR(buffs, cvar("g_buffs"))
89 {
90         MUTATOR_ONADD
91         {
92                 InitializeEntity(world, buffs_DelayedInit, INITPRIO_FINDTARGET);
93         }
94 }
95
96 entity buff_FirstFromFlags(int _buffs)
97 {
98         if (flags)
99         {
100                 FOREACH(Buffs, it.m_itemid & _buffs, LAMBDA(return it));
101         }
102         return BUFF_Null;
103 }
104
105 bool buffs_BuffModel_Customize()
106 {SELFPARAM();
107         entity player, myowner;
108         bool same_team;
109
110         player = WaypointSprite_getviewentity(other);
111         myowner = self.owner;
112         same_team = (SAME_TEAM(player, myowner) || SAME_TEAM(player, myowner));
113
114         if(myowner.alpha <= 0.5 && !same_team && myowner.alpha != 0)
115                 return false;
116
117         if(MUTATOR_CALLHOOK(BuffModel_Customize, self, player))
118                 return false;
119
120         if(player == myowner || (IS_SPEC(other) && other.enemy == myowner))
121         {
122                 // somewhat hide the model, but keep the glow
123                 self.effects = 0;
124                 self.alpha = -1;
125         }
126         else
127         {
128                 self.effects = EF_FULLBRIGHT | EF_LOWPRECISION;
129                 self.alpha = 1;
130         }
131         return true;
132 }
133
134 void buffs_BuffModel_Spawn(entity player)
135 {
136         player.buff_model = spawn();
137         setmodel(player.buff_model, MDL_BUFF);
138         setsize(player.buff_model, '0 0 -40', '0 0 40');
139         setattachment(player.buff_model, player, "");
140         setorigin(player.buff_model, '0 0 1' * (player.buff_model.maxs.z * 1));
141         player.buff_model.owner = player;
142         player.buff_model.scale = 0.7;
143         player.buff_model.pflags = PFLAGS_FULLDYNAMIC;
144         player.buff_model.light_lev = 200;
145         player.buff_model.customizeentityforclient = buffs_BuffModel_Customize;
146 }
147
148 vector buff_GlowColor(entity buff)
149 {
150         //if(buff.team) { return Team_ColorRGB(buff.team); }
151         return buff.m_color;
152 }
153
154 void buff_Effect(entity player, string eff)
155 {SELFPARAM();
156         if(!autocvar_g_buffs_effects) { return; }
157
158         if(time >= self.buff_effect_delay)
159         {
160                 Send_Effect_(eff, player.origin + ((player.mins + player.maxs) * 0.5), '0 0 0', 1);
161                 self.buff_effect_delay = time + 0.05; // prevent spam
162         }
163 }
164
165 // buff item
166 float buff_Waypoint_visible_for_player(entity plr)
167 {SELFPARAM();
168         if(!self.owner.buff_active && !self.owner.buff_activetime)
169                 return false;
170
171         if (plr.buffs)
172         {
173                 return plr.cvar_cl_buffs_autoreplace == false || plr.buffs != self.owner.buffs;
174         }
175
176         return WaypointSprite_visible_for_player(plr);
177 }
178
179 void buff_Waypoint_Spawn(entity e)
180 {
181         entity buff = buff_FirstFromFlags(e.buffs);
182         entity wp = WaypointSprite_Spawn(WP_Buff, 0, autocvar_g_buffs_waypoint_distance, e, '0 0 1' * e.maxs.z, world, e.team, e, buff_waypoint, true, RADARICON_Buff);
183         wp.wp_extra = buff.m_id;
184         WaypointSprite_UpdateTeamRadar(e.buff_waypoint, RADARICON_Buff, e.glowmod);
185         e.buff_waypoint.waypointsprite_visible_for_player = buff_Waypoint_visible_for_player;
186 }
187
188 void buff_SetCooldown(float cd)
189 {SELFPARAM();
190         cd = max(0, cd);
191
192         if(!self.buff_waypoint)
193                 buff_Waypoint_Spawn(self);
194
195         WaypointSprite_UpdateBuildFinished(self.buff_waypoint, time + cd);
196         self.buff_activetime = cd;
197         self.buff_active = !cd;
198 }
199
200 void buff_Respawn(entity this)
201 {
202         if(gameover) { return; }
203
204         vector oldbufforigin = this.origin;
205         this.velocity = '0 0 200';
206
207         if(!MoveToRandomMapLocation(this, DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_PLAYERCLIP, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_BODY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY,
208                 ((autocvar_g_buffs_random_location_attempts > 0) ? autocvar_g_buffs_random_location_attempts : 10), 1024, 256))
209         {
210                 entity spot = SelectSpawnPoint(true);
211                 setorigin(this, spot.origin);
212                 this.velocity = ((randomvec() * 100) + '0 0 200');
213                 this.angles = spot.angles;
214         }
215
216         tracebox(this.origin, this.mins * 1.5, this.maxs * 1.5, this.origin, MOVE_NOMONSTERS, this);
217
218         setorigin(this, trace_endpos); // attempt to unstick
219
220         this.movetype = MOVETYPE_TOSS;
221
222         makevectors(this.angles);
223         this.angles = '0 0 0';
224         if(autocvar_g_buffs_random_lifetime > 0)
225                 this.lifetime = time + autocvar_g_buffs_random_lifetime;
226
227         Send_Effect(EFFECT_ELECTRO_COMBO, oldbufforigin + ((this.mins + this.maxs) * 0.5), '0 0 0', 1);
228         Send_Effect(EFFECT_ELECTRO_COMBO, CENTER_OR_VIEWOFS(this), '0 0 0', 1);
229
230         WaypointSprite_Ping(this.buff_waypoint);
231
232         sound(this, CH_TRIGGER, SND_KA_RESPAWN, VOL_BASE, ATTEN_NONE); // ATTEN_NONE (it's a sound intended to be heard anywhere)
233 }
234
235 void buff_Touch()
236 {SELFPARAM();
237         if(gameover) { return; }
238
239         if(ITEM_TOUCH_NEEDKILL())
240         {
241                 buff_Respawn(self);
242                 return;
243         }
244
245         if((self.team && DIFF_TEAM(other, self))
246         || (STAT(FROZEN, other))
247         || (other.vehicle)
248         || (!self.buff_active)
249         )
250         {
251                 // can't touch this
252                 return;
253         }
254
255         if(MUTATOR_CALLHOOK(BuffTouch, self, other))
256                 return;
257
258         if(!IS_PLAYER(other))
259                 return; // incase mutator changed other
260
261         if (other.buffs)
262         {
263                 if (other.cvar_cl_buffs_autoreplace && other.buffs != self.buffs)
264                 {
265                         int buffid = buff_FirstFromFlags(other.buffs).m_id;
266                         //Send_Notification(NOTIF_ONE, other, MSG_MULTI, ITEM_BUFF_DROP, other.buffs);
267                         Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ITEM_BUFF_LOST, other.netname, buffid);
268
269                         other.buffs = 0;
270                         //sound(other, CH_TRIGGER, SND_BUFF_LOST, VOL_BASE, ATTN_NORM);
271                 }
272                 else { return; } // do nothing
273         }
274
275         self.owner = other;
276         self.buff_active = false;
277         self.lifetime = 0;
278         int buffid = buff_FirstFromFlags(self.buffs).m_id;
279         Send_Notification(NOTIF_ONE, other, MSG_MULTI, ITEM_BUFF_GOT, buffid);
280         Send_Notification(NOTIF_ALL_EXCEPT, other, MSG_INFO, INFO_ITEM_BUFF, other.netname, buffid);
281
282         Send_Effect(EFFECT_ITEM_PICKUP, CENTER_OR_VIEWOFS(self), '0 0 0', 1);
283         sound(other, CH_TRIGGER, SND_SHIELD_RESPAWN, VOL_BASE, ATTN_NORM);
284         other.buffs |= (self.buffs);
285 }
286
287 float buff_Available(entity buff)
288 {
289         if (buff == BUFF_Null)
290                 return false;
291         if (buff == BUFF_AMMO && ((start_items & IT_UNLIMITED_WEAPON_AMMO) || (start_items & IT_UNLIMITED_AMMO) || (cvar("g_melee_only"))))
292                 return false;
293         if (buff == BUFF_VAMPIRE && cvar("g_vampire"))
294                 return false;
295         return cvar(strcat("g_buffs_", buff.m_name));
296 }
297
298 .int buff_seencount;
299
300 void buff_NewType(entity ent, float cb)
301 {
302         RandomSelection_Init();
303         FOREACH(Buffs, buff_Available(it), LAMBDA(
304                 it.buff_seencount += 1;
305                 // if it's already been chosen, give it a lower priority
306                 RandomSelection_Add(world, it.m_itemid, string_null, 1, max(0.2, 1 / it.buff_seencount));
307         ));
308         ent.buffs = RandomSelection_chosen_float;
309 }
310
311 void buff_Think()
312 {SELFPARAM();
313         if(self.buffs != self.oldbuffs)
314         {
315                 entity buff = buff_FirstFromFlags(self.buffs);
316                 self.color = buff.m_color;
317                 self.glowmod = buff_GlowColor(buff);
318                 self.skin = buff.m_skin;
319
320                 setmodel(self, MDL_BUFF);
321
322                 if(self.buff_waypoint)
323                 {
324                         //WaypointSprite_Disown(self.buff_waypoint, 1);
325                         WaypointSprite_Kill(self.buff_waypoint);
326                         buff_Waypoint_Spawn(self);
327                         if(self.buff_activetime)
328                                 WaypointSprite_UpdateBuildFinished(self.buff_waypoint, time + self.buff_activetime - frametime);
329                 }
330
331                 self.oldbuffs = self.buffs;
332         }
333
334         if(!gameover)
335         if((round_handler_IsActive() && !round_handler_IsRoundStarted()) || time >= game_starttime)
336         if(!self.buff_activetime_updated)
337         {
338                 buff_SetCooldown(self.buff_activetime);
339                 self.buff_activetime_updated = true;
340         }
341
342         if(!self.buff_active && !self.buff_activetime)
343         if(!self.owner || STAT(FROZEN, self.owner) || IS_DEAD(self.owner) || !self.owner.iscreature || !(self.owner.buffs & self.buffs))
344         {
345                 buff_SetCooldown(autocvar_g_buffs_cooldown_respawn + frametime);
346                 self.owner = world;
347                 if(autocvar_g_buffs_randomize)
348                         buff_NewType(self, self.buffs);
349
350                 if(autocvar_g_buffs_random_location || (self.spawnflags & 64))
351                         buff_Respawn(self);
352         }
353
354         if(self.buff_activetime)
355         if(!gameover)
356         if((round_handler_IsActive() && !round_handler_IsRoundStarted()) || time >= game_starttime)
357         {
358                 self.buff_activetime = max(0, self.buff_activetime - frametime);
359
360                 if(!self.buff_activetime)
361                 {
362                         self.buff_active = true;
363                         sound(self, CH_TRIGGER, SND_STRENGTH_RESPAWN, VOL_BASE, ATTN_NORM);
364                         Send_Effect(EFFECT_ITEM_RESPAWN, CENTER_OR_VIEWOFS(self), '0 0 0', 1);
365                 }
366         }
367
368         if(self.buff_active)
369         {
370                 if(self.team && !self.buff_waypoint)
371                         buff_Waypoint_Spawn(self);
372
373                 if(self.lifetime)
374                 if(time >= self.lifetime)
375                         buff_Respawn(self);
376         }
377
378         self.nextthink = time;
379         //self.angles_y = time * 110.1;
380 }
381
382 void buff_Waypoint_Reset()
383 {SELFPARAM();
384         WaypointSprite_Kill(self.buff_waypoint);
385
386         if(self.buff_activetime) { buff_Waypoint_Spawn(self); }
387 }
388
389 void buff_Reset(entity this)
390 {
391         if(autocvar_g_buffs_randomize)
392                 buff_NewType(this, this.buffs);
393         this.owner = world;
394         buff_SetCooldown(autocvar_g_buffs_cooldown_activate);
395         buff_Waypoint_Reset();
396         this.buff_activetime_updated = false;
397
398         if(autocvar_g_buffs_random_location || (this.spawnflags & 64))
399                 buff_Respawn(this);
400 }
401
402 float buff_Customize()
403 {SELFPARAM();
404         entity player = WaypointSprite_getviewentity(other);
405         if(!self.buff_active || (self.team && DIFF_TEAM(player, self)))
406         {
407                 self.alpha = 0.3;
408                 if(self.effects & EF_FULLBRIGHT) { self.effects &= ~(EF_FULLBRIGHT); }
409                 self.pflags = 0;
410         }
411         else
412         {
413                 self.alpha = 1;
414                 if(!(self.effects & EF_FULLBRIGHT)) { self.effects |= EF_FULLBRIGHT; }
415                 self.light_lev = 220 + 36 * sin(time);
416                 self.pflags = PFLAGS_FULLDYNAMIC;
417         }
418         return true;
419 }
420
421 void buff_Init(entity ent)
422 {SELFPARAM();
423         if(!cvar("g_buffs")) { remove(ent); return; }
424
425         if(!teamplay && ent.team) { ent.team = 0; }
426
427         entity buff = buff_FirstFromFlags(self.buffs);
428
429         setself(ent);
430         if(!self.buffs || buff_Available(buff))
431                 buff_NewType(self, 0);
432
433         self.classname = "item_buff";
434         self.solid = SOLID_TRIGGER;
435         self.flags = FL_ITEM;
436         self.think = buff_Think;
437         self.touch = buff_Touch;
438         self.reset = buff_Reset;
439         self.nextthink = time + 0.1;
440         self.gravity = 1;
441         self.movetype = MOVETYPE_TOSS;
442         self.scale = 1;
443         self.skin = buff.m_skin;
444         self.effects = EF_FULLBRIGHT | EF_STARDUST | EF_NOSHADOW;
445         self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY;
446         self.customizeentityforclient = buff_Customize;
447         //self.gravity = 100;
448         self.color = buff.m_color;
449         self.glowmod = buff_GlowColor(self);
450         buff_SetCooldown(autocvar_g_buffs_cooldown_activate + game_starttime);
451         self.buff_active = !self.buff_activetime;
452         self.pflags = PFLAGS_FULLDYNAMIC;
453
454         if(self.spawnflags & 1)
455                 self.noalign = true;
456
457         if(self.noalign)
458                 self.movetype = MOVETYPE_NONE; // reset by random location
459
460         setmodel(self, MDL_BUFF);
461         setsize(self, BUFF_MIN, BUFF_MAX);
462
463         if(cvar("g_buffs_random_location") || (self.spawnflags & 64))
464                 buff_Respawn(self);
465
466         setself(this);
467 }
468
469 void buff_Init_Compat(entity ent, entity replacement)
470 {
471         if (ent.spawnflags & 2)
472                 ent.team = NUM_TEAM_1;
473         else if (ent.spawnflags & 4)
474                 ent.team = NUM_TEAM_2;
475
476         ent.buffs = replacement.m_itemid;
477
478         buff_Init(ent);
479 }
480
481 void buff_SpawnReplacement(entity ent, entity old)
482 {
483         setorigin(ent, old.origin);
484         ent.angles = old.angles;
485         ent.noalign = (old.noalign || (old.spawnflags & 1));
486
487         buff_Init(ent);
488 }
489
490 void buff_Vengeance_DelayedDamage()
491 {SELFPARAM();
492         if(self.enemy)
493                 Damage(self.enemy, self.owner, self.owner, self.dmg, DEATH_BUFF.m_id, self.enemy.origin, '0 0 0');
494
495         remove(self);
496         return;
497 }
498
499 // note: only really useful in teamplay
500 void buff_Medic_Heal(entity this)
501 {
502         FOREACH_CLIENT(IS_PLAYER(it) && it != this && vdist(it.origin - this.origin, <=, autocvar_g_buffs_medic_heal_range),
503         {
504                 if(SAME_TEAM(it, this))
505                 if(it.health < autocvar_g_balance_health_regenstable)
506                 {
507                         Send_Effect(EFFECT_HEALING, it.origin, '0 0 0', 1);
508                         it.health = bound(0, it.health + autocvar_g_buffs_medic_heal_amount, autocvar_g_balance_health_regenstable);
509                 }
510         });
511 }
512
513 float buff_Inferno_CalculateTime(float x, float offset_x, float offset_y, float intersect_x, float intersect_y, float base)
514 {
515         return offset_y + (intersect_y - offset_y) * logn(((x - offset_x) * ((base - 1) / intersect_x)) + 1, base);
516 }
517
518 // mutator hooks
519 MUTATOR_HOOKFUNCTION(buffs, PlayerDamage_SplitHealthArmor)
520 {
521         if(frag_deathtype == DEATH_BUFF.m_id) { return false; }
522
523         if(frag_target.buffs & BUFF_RESISTANCE.m_itemid)
524         {
525                 vector v = healtharmor_applydamage(50, autocvar_g_buffs_resistance_blockpercent, frag_deathtype, frag_damage);
526                 damage_take = v.x;
527                 damage_save = v.y;
528         }
529
530         return false;
531 }
532
533 MUTATOR_HOOKFUNCTION(buffs, PlayerDamage_Calculate)
534 {
535         if(frag_deathtype == DEATH_BUFF.m_id) { return false; }
536
537         if(frag_target.buffs & BUFF_SPEED.m_itemid)
538         if(frag_target != frag_attacker)
539                 frag_damage *= autocvar_g_buffs_speed_damage_take;
540
541         if(frag_target.buffs & BUFF_MEDIC.m_itemid)
542         if((frag_target.health - frag_damage) <= 0)
543         if(!ITEM_DAMAGE_NEEDKILL(frag_deathtype))
544         if(frag_attacker)
545         if(random() <= autocvar_g_buffs_medic_survive_chance)
546                 frag_damage = max(5, frag_target.health - autocvar_g_buffs_medic_survive_health);
547
548         if(frag_target.buffs & BUFF_JUMP.m_itemid)
549         if(frag_deathtype == DEATH_FALL.m_id)
550                 frag_damage = 0;
551
552         if(frag_target.buffs & BUFF_VENGEANCE.m_itemid)
553         if(frag_attacker)
554         if(frag_attacker != frag_target)
555         if(!ITEM_DAMAGE_NEEDKILL(frag_deathtype))
556         {
557                 entity dmgent = spawn();
558
559                 dmgent.dmg = frag_damage * autocvar_g_buffs_vengeance_damage_multiplier;
560                 dmgent.enemy = frag_attacker;
561                 dmgent.owner = frag_target;
562                 dmgent.think = buff_Vengeance_DelayedDamage;
563                 dmgent.nextthink = time + 0.1;
564         }
565
566         if(frag_target.buffs & BUFF_BASH.m_itemid)
567         if(frag_attacker != frag_target)
568                 frag_force = '0 0 0';
569
570         if(frag_attacker.buffs & BUFF_BASH.m_itemid)
571         if(frag_force)
572         if(frag_attacker == frag_target)
573                 frag_force *= autocvar_g_buffs_bash_force_self;
574         else
575                 frag_force *= autocvar_g_buffs_bash_force;
576
577         if(frag_attacker.buffs & BUFF_DISABILITY.m_itemid)
578         if(frag_target != frag_attacker)
579                 frag_target.buff_disability_time = time + autocvar_g_buffs_disability_slowtime;
580
581         if(frag_target.buffs & BUFF_INFERNO.m_itemid)
582         {
583                 if(frag_deathtype == DEATH_FIRE.m_id)
584                         frag_damage = 0;
585                 if(frag_deathtype == DEATH_LAVA.m_id)
586                         frag_damage *= 0.5; // TODO: cvarize?
587         }
588
589         if(frag_attacker.buffs & BUFF_INFERNO.m_itemid)
590         if(frag_target != frag_attacker) {
591                 float btime = buff_Inferno_CalculateTime(
592                         frag_damage,
593                         0,
594                         autocvar_g_buffs_inferno_burntime_min_time,
595                         autocvar_g_buffs_inferno_burntime_target_damage,
596                         autocvar_g_buffs_inferno_burntime_target_time,
597                         autocvar_g_buffs_inferno_burntime_factor
598                 );
599                 Fire_AddDamage(frag_target, frag_attacker, (frag_damage * autocvar_g_buffs_inferno_damagemultiplier), btime, DEATH_BUFF.m_id);
600         }
601
602         // this... is ridiculous (TODO: fix!)
603         if(frag_attacker.buffs & BUFF_VAMPIRE.m_itemid)
604         if(!frag_target.vehicle)
605         if(DEATH_WEAPONOF(frag_deathtype) != WEP_ARC)
606         if(!ITEM_DAMAGE_NEEDKILL(frag_deathtype))
607         if(!IS_DEAD(frag_target))
608         if(IS_PLAYER(frag_target) || IS_MONSTER(frag_target))
609         if(frag_attacker != frag_target)
610         if(!STAT(FROZEN, frag_target))
611         if(frag_target.takedamage)
612         if(DIFF_TEAM(frag_attacker, frag_target))
613         {
614                 frag_attacker.health = bound(0, frag_attacker.health + bound(0, frag_damage * autocvar_g_buffs_vampire_damage_steal, frag_target.health), g_pickup_healthsmall_max);
615                 if(frag_target.armorvalue)
616                         frag_attacker.armorvalue = bound(0, frag_attacker.armorvalue + bound(0, frag_damage * autocvar_g_buffs_vampire_damage_steal, frag_target.armorvalue), g_pickup_armorsmall_max);
617         }
618
619         return false;
620 }
621
622 MUTATOR_HOOKFUNCTION(buffs,PlayerSpawn)
623 {SELFPARAM();
624         self.buffs = 0;
625         // reset timers here to prevent them continuing after re-spawn
626         self.buff_disability_time = 0;
627         self.buff_disability_effect_time = 0;
628         return false;
629 }
630
631 .float stat_sv_maxspeed;
632 .float stat_sv_airspeedlimit_nonqw;
633 .float stat_sv_jumpvelocity;
634
635 MUTATOR_HOOKFUNCTION(buffs, PlayerPhysics)
636 {SELFPARAM();
637         if(self.buffs & BUFF_SPEED.m_itemid)
638         {
639                 self.stat_sv_maxspeed *= autocvar_g_buffs_speed_speed;
640                 self.stat_sv_airspeedlimit_nonqw *= autocvar_g_buffs_speed_speed;
641         }
642
643         if(time < self.buff_disability_time)
644         {
645                 self.stat_sv_maxspeed *= autocvar_g_buffs_disability_speed;
646                 self.stat_sv_airspeedlimit_nonqw *= autocvar_g_buffs_disability_speed;
647         }
648
649         if(self.buffs & BUFF_JUMP.m_itemid)
650         {
651                 // automatically reset, no need to worry
652                 self.stat_sv_jumpvelocity = autocvar_g_buffs_jump_height;
653         }
654
655         return false;
656 }
657
658 MUTATOR_HOOKFUNCTION(buffs, PlayerJump)
659 {SELFPARAM();
660         if(self.buffs & BUFF_JUMP.m_itemid)
661                 player_jumpheight = autocvar_g_buffs_jump_height;
662
663         return false;
664 }
665
666 MUTATOR_HOOKFUNCTION(buffs, MonsterMove)
667 {SELFPARAM();
668         if(time < self.buff_disability_time)
669         {
670                 monster_speed_walk *= autocvar_g_buffs_disability_speed;
671                 monster_speed_run *= autocvar_g_buffs_disability_speed;
672         }
673
674         return false;
675 }
676
677 MUTATOR_HOOKFUNCTION(buffs, PlayerDies)
678 {
679         if(frag_target.buffs)
680         {
681                 int buffid = buff_FirstFromFlags(frag_target.buffs).m_id;
682                 Send_Notification(NOTIF_ALL_EXCEPT, frag_target, MSG_INFO, INFO_ITEM_BUFF_LOST, frag_target.netname, buffid);
683                 frag_target.buffs = 0;
684
685                 if(frag_target.buff_model)
686                 {
687                         remove(frag_target.buff_model);
688                         frag_target.buff_model = world;
689                 }
690         }
691         return false;
692 }
693
694 MUTATOR_HOOKFUNCTION(buffs, PlayerUseKey, CBC_ORDER_FIRST)
695 {SELFPARAM();
696         if(MUTATOR_RETURNVALUE || gameover) { return false; }
697         if(self.buffs)
698         {
699                 int buffid = buff_FirstFromFlags(self.buffs).m_id;
700                 Send_Notification(NOTIF_ONE, self, MSG_MULTI, ITEM_BUFF_DROP, buffid);
701                 Send_Notification(NOTIF_ALL_EXCEPT, self, MSG_INFO, INFO_ITEM_BUFF_LOST, self.netname, buffid);
702
703                 self.buffs = 0;
704                 sound(self, CH_TRIGGER, SND_BUFF_LOST, VOL_BASE, ATTN_NORM);
705                 return true;
706         }
707         return false;
708 }
709
710 MUTATOR_HOOKFUNCTION(buffs, ForbidThrowCurrentWeapon)
711 {SELFPARAM();
712         if(MUTATOR_RETURNVALUE || gameover) { return false; }
713
714         if(self.buffs & BUFF_SWAPPER.m_itemid)
715         {
716                 float best_distance = autocvar_g_buffs_swapper_range;
717                 entity closest = world;
718                 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
719                         if(!IS_DEAD(it) && !STAT(FROZEN, it) && !it.vehicle)
720                         if(DIFF_TEAM(it, self))
721                         {
722                                 float test = vlen2(self.origin - it.origin);
723                                 if(test <= best_distance * best_distance)
724                                 {
725                                         best_distance = sqrt(test);
726                                         closest = it;
727                                 }
728                         }
729                 ));
730
731                 if(closest)
732                 {
733                         vector my_org, my_vel, my_ang, their_org, their_vel, their_ang;
734
735                         my_org = self.origin;
736                         my_vel = self.velocity;
737                         my_ang = self.angles;
738                         their_org = closest.origin;
739                         their_vel = closest.velocity;
740                         their_ang = closest.angles;
741
742                         Drop_Special_Items(closest);
743
744                         MUTATOR_CALLHOOK(PortalTeleport, self); // initiate flag dropper
745
746                         setorigin(self, their_org);
747                         setorigin(closest, my_org);
748
749                         closest.velocity = my_vel;
750                         closest.angles = my_ang;
751                         closest.fixangle = true;
752                         closest.oldorigin = my_org;
753                         closest.oldvelocity = my_vel;
754                         self.velocity = their_vel;
755                         self.angles = their_ang;
756                         self.fixangle = true;
757                         self.oldorigin = their_org;
758                         self.oldvelocity = their_vel;
759
760                         // set pusher so self gets the kill if they fall into void
761                         closest.pusher = self;
762                         closest.pushltime = time + autocvar_g_maxpushtime;
763                         closest.istypefrag = closest.BUTTON_CHAT;
764
765                         Send_Effect(EFFECT_ELECTRO_COMBO, their_org, '0 0 0', 1);
766                         Send_Effect(EFFECT_ELECTRO_COMBO, my_org, '0 0 0', 1);
767
768                         sound(self, CH_TRIGGER, SND_KA_RESPAWN, VOL_BASE, ATTEN_NORM);
769                         sound(closest, CH_TRIGGER, SND_KA_RESPAWN, VOL_BASE, ATTEN_NORM);
770
771                         // TODO: add a counter to handle how many times one can teleport, and a delay to prevent spam
772                         self.buffs = 0;
773                         return true;
774                 }
775         }
776         return false;
777 }
778
779 bool buffs_RemovePlayer(entity player)
780 {
781         if(player.buff_model)
782         {
783                 remove(player.buff_model);
784                 player.buff_model = world;
785         }
786
787         // also reset timers here to prevent them continuing after spectating
788         player.buff_disability_time = 0;
789         player.buff_disability_effect_time = 0;
790
791         return false;
792 }
793 MUTATOR_HOOKFUNCTION(buffs, MakePlayerObserver) { return buffs_RemovePlayer(self); }
794 MUTATOR_HOOKFUNCTION(buffs, ClientDisconnect) { return buffs_RemovePlayer(self); }
795
796 MUTATOR_HOOKFUNCTION(buffs, CustomizeWaypoint)
797 {SELFPARAM();
798         entity e = WaypointSprite_getviewentity(other);
799
800         // if you have the invisibility powerup, sprites ALWAYS are restricted to your team
801         // but only apply this to real players, not to spectators
802         if((self.owner.flags & FL_CLIENT) && (self.owner.buffs & BUFF_INVISIBLE.m_itemid) && (e == other))
803         if(DIFF_TEAM(self.owner, e))
804                 return true;
805
806         return false;
807 }
808
809 MUTATOR_HOOKFUNCTION(buffs, OnEntityPreSpawn, CBC_ORDER_LAST)
810 {SELFPARAM();
811         if (self.classname == "item_flight" && cvar("g_buffs") && cvar("g_buffs_flight"))
812         {
813                 buff_Init_Compat(self, BUFF_FLIGHT);
814                 return true;
815         }
816         if(autocvar_g_buffs_replace_powerups)
817         switch(self.classname)
818         {
819                 case "item_strength":
820                 case "item_invincible":
821                 {
822                         entity e = spawn();
823                         buff_SpawnReplacement(e, self);
824                         return true;
825                 }
826         }
827         return false;
828 }
829
830 MUTATOR_HOOKFUNCTION(buffs, WeaponRateFactor)
831 {SELFPARAM();
832         if(self.buffs & BUFF_SPEED.m_itemid)
833                 weapon_rate *= autocvar_g_buffs_speed_rate;
834
835         if(time < self.buff_disability_time)
836                 weapon_rate *= autocvar_g_buffs_disability_rate;
837
838         return false;
839 }
840
841 MUTATOR_HOOKFUNCTION(buffs, WeaponSpeedFactor)
842 {SELFPARAM();
843         if(self.buffs & BUFF_SPEED.m_itemid)
844                 ret_float *= autocvar_g_buffs_speed_weaponspeed;
845
846         if(time < self.buff_disability_time)
847                 ret_float *= autocvar_g_buffs_disability_weaponspeed;
848
849         return false;
850 }
851
852 MUTATOR_HOOKFUNCTION(buffs, PlayerPreThink)
853 {SELFPARAM();
854         if(gameover || IS_DEAD(self)) { return false; }
855
856         if(time < self.buff_disability_time)
857         if(time >= self.buff_disability_effect_time)
858         {
859                 Send_Effect(EFFECT_SMOKING, self.origin + ((self.mins + self.maxs) * 0.5), '0 0 0', 1);
860                 self.buff_disability_effect_time = time + 0.5;
861         }
862
863         // handle buff lost status
864         // 1: notify everyone else
865         // 2: notify carrier as well
866         int buff_lost = 0;
867
868         if(self.buff_time)
869         if(time >= self.buff_time)
870                 buff_lost = 2;
871
872         if(STAT(FROZEN, self)) { buff_lost = 1; }
873
874         if(buff_lost)
875         {
876                 if(self.buffs)
877                 {
878                         int buffid = buff_FirstFromFlags(self.buffs).m_id;
879                         Send_Notification(NOTIF_ALL_EXCEPT, self, MSG_INFO, INFO_ITEM_BUFF_LOST, self.netname, buffid);
880                         if(buff_lost >= 2)
881                         {
882                                 Send_Notification(NOTIF_ONE, self, MSG_MULTI, ITEM_BUFF_DROP, buffid); // TODO: special timeout message?
883                                 sound(self, CH_TRIGGER, SND_BUFF_LOST, VOL_BASE, ATTN_NORM);
884                         }
885                         self.buffs = 0;
886                 }
887         }
888
889         if(self.buffs & BUFF_MAGNET.m_itemid)
890         {
891                 vector pickup_size;
892                 FOREACH_ENTITY_FLAGS(flags, FL_ITEM,
893                 {
894                         if(it.buffs)
895                                 pickup_size = '1 1 1' * autocvar_g_buffs_magnet_range_buff;
896                         else
897                                 pickup_size = '1 1 1' * autocvar_g_buffs_magnet_range_item;
898
899                         if(boxesoverlap(self.absmin - pickup_size, self.absmax + pickup_size, it.absmin, it.absmax))
900                         {
901                                 if(it.touch)
902                                 {
903                                         entity oldother = other;
904                                         other = self;
905                                         WITH(entity, self, it, it.touch());
906
907                                         other = oldother;
908                                 }
909                         }
910                 });
911         }
912
913         if(self.buffs & BUFF_AMMO.m_itemid)
914         if(self.clip_size)
915                 self.clip_load = self.(weapon_load[PS(self).m_switchweapon.m_id]) = self.clip_size;
916
917         if((self.buffs & BUFF_INVISIBLE.m_itemid) && (self.oldbuffs & BUFF_INVISIBLE.m_itemid))
918         if(self.alpha != autocvar_g_buffs_invisible_alpha)
919                 self.alpha = autocvar_g_buffs_invisible_alpha; // powerups reset alpha, so we must enforce this (TODO)
920
921         if(self.buffs & BUFF_MEDIC.m_itemid)
922         if(time >= self.buff_medic_healtime)
923         {
924                 buff_Medic_Heal(self);
925                 self.buff_medic_healtime = time + autocvar_g_buffs_medic_heal_delay;
926         }
927
928 #define BUFF_ONADD(b) if ( (self.buffs & (b).m_itemid) && !(self.oldbuffs & (b).m_itemid))
929 #define BUFF_ONREM(b) if (!(self.buffs & (b).m_itemid) &&  (self.oldbuffs & (b).m_itemid))
930
931         if(self.buffs != self.oldbuffs)
932         {
933                 entity buff = buff_FirstFromFlags(self.buffs);
934                 float bufftime = buff != BUFF_Null ? buff.m_time(buff) : 0;
935                 self.buff_time = (bufftime) ? time + bufftime : 0;
936
937                 BUFF_ONADD(BUFF_AMMO)
938                 {
939                         self.buff_ammo_prev_infitems = (self.items & IT_UNLIMITED_WEAPON_AMMO);
940                         self.items |= IT_UNLIMITED_WEAPON_AMMO;
941
942                         if(self.clip_load)
943                                 self.buff_ammo_prev_clipload = self.clip_load;
944                         self.clip_load = self.(weapon_load[PS(self).m_switchweapon.m_id]) = self.clip_size;
945                 }
946
947                 BUFF_ONREM(BUFF_AMMO)
948                 {
949                         if(self.buff_ammo_prev_infitems)
950                                 self.items |= IT_UNLIMITED_WEAPON_AMMO;
951                         else
952                                 self.items &= ~IT_UNLIMITED_WEAPON_AMMO;
953
954                         if(self.buff_ammo_prev_clipload)
955                                 self.clip_load = self.buff_ammo_prev_clipload;
956                 }
957
958                 BUFF_ONADD(BUFF_INVISIBLE)
959                 {
960                         if(time < self.strength_finished && g_instagib)
961                                 self.alpha = autocvar_g_instagib_invis_alpha;
962                         else
963                                 self.alpha = self.buff_invisible_prev_alpha;
964                         self.alpha = autocvar_g_buffs_invisible_alpha;
965                 }
966
967                 BUFF_ONREM(BUFF_INVISIBLE)
968                         self.alpha = self.buff_invisible_prev_alpha;
969
970                 BUFF_ONADD(BUFF_FLIGHT)
971                 {
972                         self.buff_flight_prev_gravity = self.gravity;
973                         self.gravity = autocvar_g_buffs_flight_gravity;
974                 }
975
976                 BUFF_ONREM(BUFF_FLIGHT)
977                         self.gravity = self.buff_flight_prev_gravity;
978
979                 self.oldbuffs = self.buffs;
980                 if(self.buffs)
981                 {
982                         if(!self.buff_model)
983                                 buffs_BuffModel_Spawn(self);
984
985                         self.buff_model.color = buff.m_color;
986                         self.buff_model.glowmod = buff_GlowColor(self.buff_model);
987                         self.buff_model.skin = buff.m_skin;
988
989                         self.effects |= EF_NOSHADOW;
990                 }
991                 else
992                 {
993                         remove(self.buff_model);
994                         self.buff_model = world;
995
996                         self.effects &= ~(EF_NOSHADOW);
997                 }
998         }
999
1000         if(self.buff_model)
1001         {
1002                 self.buff_model.effects = self.effects;
1003                 self.buff_model.effects |= EF_LOWPRECISION;
1004                 self.buff_model.effects = self.buff_model.effects & EFMASK_CHEAP; // eat performance
1005
1006                 self.buff_model.alpha = self.alpha;
1007         }
1008
1009 #undef BUFF_ONADD
1010 #undef BUFF_ONREM
1011         return false;
1012 }
1013
1014 MUTATOR_HOOKFUNCTION(buffs, SpectateCopy)
1015 {SELFPARAM();
1016         self.buffs = other.buffs;
1017         return false;
1018 }
1019
1020 MUTATOR_HOOKFUNCTION(buffs, VehicleEnter)
1021 {
1022         vh_vehicle.buffs = vh_player.buffs;
1023         vh_player.buffs = 0;
1024         vh_vehicle.buff_time = max(0, vh_player.buff_time - time);
1025         vh_player.buff_time = 0;
1026         return false;
1027 }
1028
1029 MUTATOR_HOOKFUNCTION(buffs, VehicleExit)
1030 {
1031         vh_player.buffs = vh_player.oldbuffs = vh_vehicle.buffs;
1032         vh_vehicle.buffs = 0;
1033         vh_player.buff_time = time + vh_vehicle.buff_time;
1034         vh_vehicle.buff_time = 0;
1035         return false;
1036 }
1037
1038 MUTATOR_HOOKFUNCTION(buffs, PlayerRegen)
1039 {SELFPARAM();
1040         if(self.buffs & BUFF_MEDIC.m_itemid)
1041         {
1042                 regen_mod_rot = autocvar_g_buffs_medic_rot;
1043                 regen_mod_limit = regen_mod_max = autocvar_g_buffs_medic_max;
1044                 regen_mod_regen = autocvar_g_buffs_medic_regen;
1045         }
1046
1047         if(self.buffs & BUFF_SPEED.m_itemid)
1048                 regen_mod_regen = autocvar_g_buffs_speed_regen;
1049
1050         return false;
1051 }
1052
1053 MUTATOR_HOOKFUNCTION(buffs, GetCvars)
1054 {
1055         GetCvars_handleFloat(get_cvars_s, get_cvars_f, cvar_cl_buffs_autoreplace, "cl_buffs_autoreplace");
1056         return false;
1057 }
1058
1059 MUTATOR_HOOKFUNCTION(buffs, BuildMutatorsString)
1060 {
1061         ret_string = strcat(ret_string, ":Buffs");
1062         return false;
1063 }
1064
1065 MUTATOR_HOOKFUNCTION(buffs, BuildMutatorsPrettyString)
1066 {
1067         ret_string = strcat(ret_string, ", Buffs");
1068         return false;
1069 }
1070
1071 void buffs_DelayedInit()
1072 {
1073         if(autocvar_g_buffs_spawn_count > 0)
1074         if(find(world, classname, "item_buff") == world)
1075         {
1076                 float i;
1077                 for(i = 0; i < autocvar_g_buffs_spawn_count; ++i)
1078                 {
1079                         entity e = spawn();
1080                         e.spawnflags |= 64; // always randomize
1081                         e.velocity = randomvec() * 250; // this gets reset anyway if random location works
1082                         buff_Init(e);
1083                 }
1084         }
1085 }
1086 #endif