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