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