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