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