]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/buffs/buffs.qc
Remove flight buff
[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_jump_height;
40 float autocvar_g_buffs_inferno_burntime_factor;
41 float autocvar_g_buffs_inferno_burntime_min_time;
42 float autocvar_g_buffs_inferno_burntime_target_damage;
43 float autocvar_g_buffs_inferno_burntime_target_time;
44 float autocvar_g_buffs_inferno_damagemultiplier;
45 float autocvar_g_buffs_swapper_range;
46 float autocvar_g_buffs_magnet_range_item;
47 float autocvar_g_buffs_magnet_range_buff = 200;
48 float autocvar_g_buffs_luck_chance = 0.15;
49 float autocvar_g_buffs_luck_damagemultiplier = 3;
50
51 // ammo
52 .float buff_ammo_prev_infitems;
53 .int buff_ammo_prev_clipload;
54 // invisible
55 .float buff_invisible_prev_alpha;
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_target.buffs & BUFF_INFERNO.m_itemid)
581         {
582                 if(frag_deathtype == DEATH_FIRE.m_id)
583                         frag_damage = 0;
584                 if(frag_deathtype == DEATH_LAVA.m_id)
585                         frag_damage *= 0.5; // TODO: cvarize?
586         }
587
588         if(frag_attacker.buffs & BUFF_LUCK.m_itemid)
589         if(frag_attacker != frag_target)
590         if(autocvar_g_buffs_luck_damagemultiplier > 0)
591         if(random() <= autocvar_g_buffs_luck_chance)
592                 frag_damage *= autocvar_g_buffs_luck_damagemultiplier;
593
594         if(frag_attacker.buffs & BUFF_INFERNO.m_itemid)
595         if(frag_target != frag_attacker) {
596                 float btime = buff_Inferno_CalculateTime(
597                         frag_damage,
598                         0,
599                         autocvar_g_buffs_inferno_burntime_min_time,
600                         autocvar_g_buffs_inferno_burntime_target_damage,
601                         autocvar_g_buffs_inferno_burntime_target_time,
602                         autocvar_g_buffs_inferno_burntime_factor
603                 );
604                 Fire_AddDamage(frag_target, frag_attacker, (frag_damage * autocvar_g_buffs_inferno_damagemultiplier), btime, DEATH_BUFF.m_id);
605         }
606
607         // this... is ridiculous (TODO: fix!)
608         if(frag_attacker.buffs & BUFF_VAMPIRE.m_itemid)
609         if(!frag_target.vehicle)
610         if(DEATH_WEAPONOF(frag_deathtype) != WEP_ARC)
611         if(!ITEM_DAMAGE_NEEDKILL(frag_deathtype))
612         if(!IS_DEAD(frag_target))
613         if(IS_PLAYER(frag_target) || IS_MONSTER(frag_target))
614         if(frag_attacker != frag_target)
615         if(!STAT(FROZEN, frag_target))
616         if(frag_target.takedamage)
617         if(DIFF_TEAM(frag_attacker, frag_target))
618         {
619                 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);
620                 if(frag_target.armorvalue)
621                         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);
622         }
623
624         return false;
625 }
626
627 MUTATOR_HOOKFUNCTION(buffs,PlayerSpawn)
628 {SELFPARAM();
629         self.buffs = 0;
630         // reset timers here to prevent them continuing after re-spawn
631         self.buff_disability_time = 0;
632         self.buff_disability_effect_time = 0;
633         return false;
634 }
635
636 .float stat_sv_maxspeed;
637 .float stat_sv_airspeedlimit_nonqw;
638 .float stat_sv_jumpvelocity;
639
640 MUTATOR_HOOKFUNCTION(buffs, PlayerPhysics)
641 {SELFPARAM();
642         if(self.buffs & BUFF_SPEED.m_itemid)
643         {
644                 self.stat_sv_maxspeed *= autocvar_g_buffs_speed_speed;
645                 self.stat_sv_airspeedlimit_nonqw *= autocvar_g_buffs_speed_speed;
646         }
647
648         if(time < self.buff_disability_time)
649         {
650                 self.stat_sv_maxspeed *= autocvar_g_buffs_disability_speed;
651                 self.stat_sv_airspeedlimit_nonqw *= autocvar_g_buffs_disability_speed;
652         }
653
654         if(self.buffs & BUFF_JUMP.m_itemid)
655         {
656                 // automatically reset, no need to worry
657                 self.stat_sv_jumpvelocity = autocvar_g_buffs_jump_height;
658         }
659
660         return false;
661 }
662
663 MUTATOR_HOOKFUNCTION(buffs, PlayerJump)
664 {SELFPARAM();
665         if(self.buffs & BUFF_JUMP.m_itemid)
666                 player_jumpheight = autocvar_g_buffs_jump_height;
667
668         return false;
669 }
670
671 MUTATOR_HOOKFUNCTION(buffs, MonsterMove)
672 {SELFPARAM();
673         if(time < self.buff_disability_time)
674         {
675                 monster_speed_walk *= autocvar_g_buffs_disability_speed;
676                 monster_speed_run *= autocvar_g_buffs_disability_speed;
677         }
678
679         return false;
680 }
681
682 MUTATOR_HOOKFUNCTION(buffs, PlayerDies)
683 {
684         if(frag_target.buffs)
685         {
686                 int buffid = buff_FirstFromFlags(frag_target.buffs).m_id;
687                 Send_Notification(NOTIF_ALL_EXCEPT, frag_target, MSG_INFO, INFO_ITEM_BUFF_LOST, frag_target.netname, buffid);
688                 frag_target.buffs = 0;
689
690                 if(frag_target.buff_model)
691                 {
692                         remove(frag_target.buff_model);
693                         frag_target.buff_model = world;
694                 }
695         }
696         return false;
697 }
698
699 MUTATOR_HOOKFUNCTION(buffs, PlayerUseKey, CBC_ORDER_FIRST)
700 {SELFPARAM();
701         if(MUTATOR_RETURNVALUE || gameover) { return false; }
702         if(self.buffs)
703         {
704                 int buffid = buff_FirstFromFlags(self.buffs).m_id;
705                 Send_Notification(NOTIF_ONE, self, MSG_MULTI, ITEM_BUFF_DROP, buffid);
706                 Send_Notification(NOTIF_ALL_EXCEPT, self, MSG_INFO, INFO_ITEM_BUFF_LOST, self.netname, buffid);
707
708                 self.buffs = 0;
709                 sound(self, CH_TRIGGER, SND_BUFF_LOST, VOL_BASE, ATTN_NORM);
710                 return true;
711         }
712         return false;
713 }
714
715 MUTATOR_HOOKFUNCTION(buffs, ForbidThrowCurrentWeapon)
716 {SELFPARAM();
717         if(MUTATOR_RETURNVALUE || gameover) { return false; }
718
719         if(self.buffs & BUFF_SWAPPER.m_itemid)
720         {
721                 float best_distance = autocvar_g_buffs_swapper_range;
722                 entity closest = world;
723                 FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
724                         if(!IS_DEAD(it) && !STAT(FROZEN, it) && !it.vehicle)
725                         if(DIFF_TEAM(it, self))
726                         {
727                                 float test = vlen2(self.origin - it.origin);
728                                 if(test <= best_distance * best_distance)
729                                 {
730                                         best_distance = sqrt(test);
731                                         closest = it;
732                                 }
733                         }
734                 ));
735
736                 if(closest)
737                 {
738                         vector my_org, my_vel, my_ang, their_org, their_vel, their_ang;
739
740                         my_org = self.origin;
741                         my_vel = self.velocity;
742                         my_ang = self.angles;
743                         their_org = closest.origin;
744                         their_vel = closest.velocity;
745                         their_ang = closest.angles;
746
747                         Drop_Special_Items(closest);
748
749                         MUTATOR_CALLHOOK(PortalTeleport, self); // initiate flag dropper
750
751                         setorigin(self, their_org);
752                         setorigin(closest, my_org);
753
754                         closest.velocity = my_vel;
755                         closest.angles = my_ang;
756                         closest.fixangle = true;
757                         closest.oldorigin = my_org;
758                         closest.oldvelocity = my_vel;
759                         self.velocity = their_vel;
760                         self.angles = their_ang;
761                         self.fixangle = true;
762                         self.oldorigin = their_org;
763                         self.oldvelocity = their_vel;
764
765                         // set pusher so self gets the kill if they fall into void
766                         closest.pusher = self;
767                         closest.pushltime = time + autocvar_g_maxpushtime;
768                         closest.istypefrag = closest.BUTTON_CHAT;
769
770                         Send_Effect(EFFECT_ELECTRO_COMBO, their_org, '0 0 0', 1);
771                         Send_Effect(EFFECT_ELECTRO_COMBO, my_org, '0 0 0', 1);
772
773                         sound(self, CH_TRIGGER, SND_KA_RESPAWN, VOL_BASE, ATTEN_NORM);
774                         sound(closest, CH_TRIGGER, SND_KA_RESPAWN, VOL_BASE, ATTEN_NORM);
775
776                         // TODO: add a counter to handle how many times one can teleport, and a delay to prevent spam
777                         self.buffs = 0;
778                         return true;
779                 }
780         }
781         return false;
782 }
783
784 bool buffs_RemovePlayer(entity player)
785 {
786         if(player.buff_model)
787         {
788                 remove(player.buff_model);
789                 player.buff_model = world;
790         }
791
792         // also reset timers here to prevent them continuing after spectating
793         player.buff_disability_time = 0;
794         player.buff_disability_effect_time = 0;
795
796         return false;
797 }
798 MUTATOR_HOOKFUNCTION(buffs, MakePlayerObserver) { return buffs_RemovePlayer(self); }
799 MUTATOR_HOOKFUNCTION(buffs, ClientDisconnect) { return buffs_RemovePlayer(self); }
800
801 MUTATOR_HOOKFUNCTION(buffs, CustomizeWaypoint)
802 {SELFPARAM();
803         entity e = WaypointSprite_getviewentity(other);
804
805         // if you have the invisibility powerup, sprites ALWAYS are restricted to your team
806         // but only apply this to real players, not to spectators
807         if((self.owner.flags & FL_CLIENT) && (self.owner.buffs & BUFF_INVISIBLE.m_itemid) && (e == other))
808         if(DIFF_TEAM(self.owner, e))
809                 return true;
810
811         return false;
812 }
813
814 MUTATOR_HOOKFUNCTION(buffs, OnEntityPreSpawn, CBC_ORDER_LAST)
815 {SELFPARAM();
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                 self.oldbuffs = self.buffs;
971                 if(self.buffs)
972                 {
973                         if(!self.buff_model)
974                                 buffs_BuffModel_Spawn(self);
975
976                         self.buff_model.color = buff.m_color;
977                         self.buff_model.glowmod = buff_GlowColor(self.buff_model);
978                         self.buff_model.skin = buff.m_skin;
979
980                         self.effects |= EF_NOSHADOW;
981                 }
982                 else
983                 {
984                         remove(self.buff_model);
985                         self.buff_model = world;
986
987                         self.effects &= ~(EF_NOSHADOW);
988                 }
989         }
990
991         if(self.buff_model)
992         {
993                 self.buff_model.effects = self.effects;
994                 self.buff_model.effects |= EF_LOWPRECISION;
995                 self.buff_model.effects = self.buff_model.effects & EFMASK_CHEAP; // eat performance
996
997                 self.buff_model.alpha = self.alpha;
998         }
999
1000 #undef BUFF_ONADD
1001 #undef BUFF_ONREM
1002         return false;
1003 }
1004
1005 MUTATOR_HOOKFUNCTION(buffs, SpectateCopy)
1006 {SELFPARAM();
1007         self.buffs = other.buffs;
1008         return false;
1009 }
1010
1011 MUTATOR_HOOKFUNCTION(buffs, VehicleEnter)
1012 {
1013         vh_vehicle.buffs = vh_player.buffs;
1014         vh_player.buffs = 0;
1015         vh_vehicle.buff_time = max(0, vh_player.buff_time - time);
1016         vh_player.buff_time = 0;
1017         return false;
1018 }
1019
1020 MUTATOR_HOOKFUNCTION(buffs, VehicleExit)
1021 {
1022         vh_player.buffs = vh_player.oldbuffs = vh_vehicle.buffs;
1023         vh_vehicle.buffs = 0;
1024         vh_player.buff_time = time + vh_vehicle.buff_time;
1025         vh_vehicle.buff_time = 0;
1026         return false;
1027 }
1028
1029 MUTATOR_HOOKFUNCTION(buffs, PlayerRegen)
1030 {SELFPARAM();
1031         if(self.buffs & BUFF_MEDIC.m_itemid)
1032         {
1033                 regen_mod_rot = autocvar_g_buffs_medic_rot;
1034                 regen_mod_limit = regen_mod_max = autocvar_g_buffs_medic_max;
1035                 regen_mod_regen = autocvar_g_buffs_medic_regen;
1036         }
1037
1038         if(self.buffs & BUFF_SPEED.m_itemid)
1039                 regen_mod_regen = autocvar_g_buffs_speed_regen;
1040
1041         return false;
1042 }
1043
1044 MUTATOR_HOOKFUNCTION(buffs, GetCvars)
1045 {
1046         GetCvars_handleFloat(get_cvars_s, get_cvars_f, cvar_cl_buffs_autoreplace, "cl_buffs_autoreplace");
1047         return false;
1048 }
1049
1050 MUTATOR_HOOKFUNCTION(buffs, BuildMutatorsString)
1051 {
1052         ret_string = strcat(ret_string, ":Buffs");
1053         return false;
1054 }
1055
1056 MUTATOR_HOOKFUNCTION(buffs, BuildMutatorsPrettyString)
1057 {
1058         ret_string = strcat(ret_string, ", Buffs");
1059         return false;
1060 }
1061
1062 void buffs_DelayedInit()
1063 {
1064         if(autocvar_g_buffs_spawn_count > 0)
1065         if(find(world, classname, "item_buff") == world)
1066         {
1067                 float i;
1068                 for(i = 0; i < autocvar_g_buffs_spawn_count; ++i)
1069                 {
1070                         entity e = spawn();
1071                         e.spawnflags |= 64; // always randomize
1072                         e.velocity = randomvec() * 250; // this gets reset anyway if random location works
1073                         buff_Init(e);
1074                 }
1075         }
1076 }
1077 #endif