]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/g_damage.qc
Merge branch 'master' into TimePath/notifications
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / g_damage.qc
1 #include "g_damage.qh"
2
3 #include "bot/bot.qh"
4 #include "g_hook.qh"
5 #include "mutators/all.qh"
6 #include "scores.qh"
7 #include "spawnpoints.qh"
8 #include "../common/t_items.qh"
9 #include "../common/vehicles/all.qh"
10 #include "../common/items/all.qc"
11 #include "../common/mutators/mutator/waypoints/waypointsprites.qh"
12 #include "weapons/accuracy.qh"
13 #include "weapons/csqcprojectile.qh"
14 #include "weapons/selection.qh"
15 #include "../common/constants.qh"
16 #include "../common/deathtypes/all.qh"
17 #include "../common/notifications/all.qh"
18 #include "../common/physics/movetypes/movetypes.qh"
19 #include "../common/playerstats.qh"
20 #include "../common/teams.qh"
21 #include "../common/util.qh"
22 #include "../common/weapons/all.qh"
23 #include "../lib/csqcmodel/sv_model.qh"
24 #include "../lib/warpzone/common.qh"
25
26 void UpdateFrags(entity player, float f)
27 {
28         PlayerTeamScore_AddScore(player, f);
29 }
30
31 void GiveFrags (entity attacker, entity targ, float f, int deathtype)
32 {SELFPARAM();
33         // TODO route through PlayerScores instead
34         if(gameover) return;
35
36         if(f < 0)
37         {
38                 if(targ == attacker)
39                 {
40                         // suicide
41                         PlayerScore_Add(attacker, SP_SUICIDES, 1);
42                 }
43                 else
44                 {
45                         // teamkill
46                         PlayerScore_Add(attacker, SP_KILLS, -1); // or maybe add a teamkills field?
47                 }
48         }
49         else
50         {
51                 // regular frag
52                 PlayerScore_Add(attacker, SP_KILLS, 1);
53                 if(targ.playerid)
54                         PS_GR_P_ADDVAL(attacker, sprintf("kills-%d", targ.playerid), 1);
55         }
56
57         PlayerScore_Add(targ, SP_DEATHS, 1);
58
59         if(targ != attacker) // not for suicides
60         if(g_weaponarena_random)
61         {
62                 // after a frag, exchange the current weapon (or the culprit, if detectable) by a new random weapon
63                 Weapon culprit = DEATH_WEAPONOF(deathtype);
64                 if(!culprit) culprit = PS(attacker).m_weapon;
65                 else if(!(attacker.weapons & (culprit.m_wepset))) culprit = PS(attacker).m_weapon;
66
67                 if(g_weaponarena_random_with_blaster && culprit == WEP_BLASTER) // WEAPONTODO: Shouldn't this be in a mutator?
68                 {
69                         // no exchange
70                 }
71                 else
72                 {
73                         if(!GiveFrags_randomweapons)
74                         {
75                                 GiveFrags_randomweapons = new(GiveFrags_randomweapons);
76                         }
77
78                         if(warmup_stage)
79                                 GiveFrags_randomweapons.weapons = WARMUP_START_WEAPONS;
80                         else
81                                 GiveFrags_randomweapons.weapons = start_weapons;
82
83                         // all others (including the culprit): remove
84                         GiveFrags_randomweapons.weapons &= ~attacker.weapons;
85                         GiveFrags_randomweapons.weapons &= ~(culprit.m_wepset);
86
87                         // among the remaining ones, choose one by random
88                         W_RandomWeapons(GiveFrags_randomweapons, 1);
89
90                         if(GiveFrags_randomweapons.weapons)
91                         {
92                                 attacker.weapons |= GiveFrags_randomweapons.weapons;
93                                 attacker.weapons &= ~(culprit.m_wepset);
94                         }
95                 }
96
97                 // after a frag, choose another random weapon set
98                 if (!(attacker.weapons & WepSet_FromWeapon(PS(attacker).m_weapon)))
99                         W_SwitchWeapon_Force(attacker, w_getbestweapon(attacker));
100         }
101
102         // FIXME fix the mess this is (we have REAL points now!)
103         if(MUTATOR_CALLHOOK(GiveFragsForKill, self, attacker, targ, f))
104         {
105                 f = frag_score;
106         }
107
108         attacker.totalfrags += f;
109
110         if(f)
111                 UpdateFrags(attacker, f);
112 }
113
114 string AppendItemcodes(string s, entity player)
115 {
116         int w = PS(player).m_weapon.m_id;
117         //if(w == 0)
118         //      w = player.switchweapon;
119         if(w == 0)
120                 w = player.cnt; // previous weapon!
121         s = strcat(s, ftos(w));
122         if(time < player.strength_finished)
123                 s = strcat(s, "S");
124         if(time < player.invincible_finished)
125                 s = strcat(s, "I");
126         if(player.flagcarried != world)
127                 s = strcat(s, "F");
128         if(PHYS_INPUT_BUTTON_CHAT(player))
129                 s = strcat(s, "T");
130         if(player.kh_next)
131                 s = strcat(s, "K");
132         return s;
133 }
134
135 void LogDeath(string mode, int deathtype, entity killer, entity killed)
136 {
137         string s;
138         if(!autocvar_sv_eventlog)
139                 return;
140         s = strcat(":kill:", mode);
141         s = strcat(s, ":", ftos(killer.playerid));
142         s = strcat(s, ":", ftos(killed.playerid));
143         s = strcat(s, ":type=", Deathtype_Name(deathtype));
144         s = strcat(s, ":items=");
145         s = AppendItemcodes(s, killer);
146         if(killed != killer)
147         {
148                 s = strcat(s, ":victimitems=");
149                 s = AppendItemcodes(s, killed);
150         }
151         GameLogEcho(s);
152 }
153
154 void Obituary_SpecialDeath(
155         entity notif_target,
156         float murder,
157         int deathtype,
158         string s1, string s2, string s3,
159         float f1, float f2, float f3)
160 {
161         if(DEATH_ISSPECIAL(deathtype))
162         {
163                 entity deathent = Deathtypes_from(deathtype - DT_FIRST);
164                 if (!deathent) { backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n"); return; }
165
166                 if(g_cts && deathtype == DEATH_KILL.m_id)
167                         return; // TODO: somehow put this in CTS gamemode file!
168
169                 if(murder)
170                 {
171                         if(deathent.death_msgmurder)
172                         {
173                                 Send_Notification_WOCOVA(
174                                         NOTIF_ONE,
175                                         notif_target,
176                                         MSG_MULTI,
177                                         deathent.death_msgmurder,
178                                         s1, s2, s3, "",
179                                         f1, f2, f3, 0
180                                 );
181                                 Send_Notification_WOCOVA(
182                                         NOTIF_ALL_EXCEPT,
183                                         notif_target,
184                                         MSG_INFO,
185                                         deathent.death_msgmurder.nent_msginfo,
186                                         s1, s2, s3, "",
187                                         f1, f2, f3, 0
188                                 );
189                         }
190                 }
191                 else
192                 {
193                         if(deathent.death_msgself)
194                         {
195                                 Send_Notification_WOCOVA(
196                                         NOTIF_ONE,
197                                         notif_target,
198                                         MSG_MULTI,
199                                         deathent.death_msgself,
200                                         s1, s2, s3, "",
201                                         f1, f2, f3, 0
202                                 );
203                                 Send_Notification_WOCOVA(
204                                         NOTIF_ALL_EXCEPT,
205                                         notif_target,
206                                         MSG_INFO,
207                                         deathent.death_msgself.nent_msginfo,
208                                         s1, s2, s3, "",
209                                         f1, f2, f3, 0
210                                 );
211                         }
212                 }
213         }
214         else { backtrace("Obituary_SpecialDeath called without a special deathtype?\n"); return; }
215 }
216
217 float Obituary_WeaponDeath(
218         entity notif_target,
219         float murder,
220         int deathtype,
221         string s1, string s2, string s3,
222         float f1, float f2)
223 {
224         Weapon death_weapon = DEATH_WEAPONOF(deathtype);
225         if (death_weapon != WEP_Null)
226         {
227                 w_deathtype = deathtype;
228                 Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
229                 w_deathtype = false;
230
231                 if (death_message)
232                 {
233                         Send_Notification_WOCOVA(
234                                 NOTIF_ONE,
235                                 notif_target,
236                                 MSG_MULTI,
237                                 death_message,
238                                 s1, s2, s3, "",
239                                 f1, f2, 0, 0
240                         );
241                         // send the info part to everyone
242                         Send_Notification_WOCOVA(
243                                 NOTIF_ALL_EXCEPT,
244                                 notif_target,
245                                 MSG_INFO,
246                                 death_message.nent_msginfo,
247                                 s1, s2, s3, "",
248                                 f1, f2, 0, 0
249                         );
250                 }
251                 else
252                 {
253                         LOG_TRACEF(
254                                 "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %d!\n",
255                                 deathtype,
256                                 death_weapon
257                         );
258                 }
259
260                 return true;
261         }
262         return false;
263 }
264
265 .int buffs = _STAT(BUFFS); // TODO: remove
266 entity buff_FirstFromFlags(int _buffs);
267 void Obituary(entity attacker, entity inflictor, entity targ, int deathtype)
268 {
269         // Sanity check
270         if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
271
272         // Declarations
273         float notif_firstblood = false;
274         float kill_count_to_attacker, kill_count_to_target;
275
276         // Set final information for the death
277         targ.death_origin = targ.origin;
278         if(targ != attacker) { targ.killer_origin = attacker.origin; }
279         string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
280
281         #ifdef NOTIFICATIONS_DEBUG
282         Debug_Notification(
283                 sprintf(
284                         "Obituary(%s, %s, %s, %s = %d);\n",
285                         attacker.netname,
286                         inflictor.netname,
287                         targ.netname,
288                         Deathtype_Name(deathtype),
289                         deathtype
290                 )
291         );
292         #endif
293
294         // =======
295         // SUICIDE
296         // =======
297         if(targ == attacker)
298         {
299                 if(DEATH_ISSPECIAL(deathtype))
300                 {
301                         if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
302                         {
303                                 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
304                         }
305                         else
306                         {
307                                 switch(DEATH_ENT(deathtype))
308                                 {
309                                         case DEATH_MIRRORDAMAGE:
310                                         {
311                                                 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.killcount, 0, 0);
312                                                 break;
313                                         }
314
315                                         default:
316                                         {
317                                                 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.killcount, 0, 0);
318                                                 break;
319                                         }
320                                 }
321                         }
322                 }
323                 else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.killcount, 0))
324                 {
325                         backtrace("SUICIDE: what the hell happened here?\n");
326                         return;
327                 }
328                 LogDeath("suicide", deathtype, targ, targ);
329                 GiveFrags(attacker, targ, -1, deathtype);
330         }
331
332         // ======
333         // MURDER
334         // ======
335         else if(IS_PLAYER(attacker))
336         {
337                 if(SAME_TEAM(attacker, targ))
338                 {
339                         LogDeath("tk", deathtype, attacker, targ);
340                         GiveFrags(attacker, targ, -1, deathtype);
341
342                         attacker.killcount = 0;
343
344                         Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
345                         Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker.netname);
346                         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker.netname, deathlocation, targ.killcount);
347
348                         // In this case, the death message will ALWAYS be "foo was betrayed by bar"
349                         // No need for specific death/weapon messages...
350                 }
351                 else
352                 {
353                         LogDeath("frag", deathtype, attacker, targ);
354                         GiveFrags(attacker, targ, 1, deathtype);
355
356                         attacker.taunt_soundtime = time + 1;
357                         attacker.killcount = attacker.killcount + 1;
358
359                         #define SPREE_ITEM(counta,countb,center,normal,gentle) \
360                                 case counta: \
361                                 { \
362                                         Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
363                                         PS_GR_P_ADDVAL(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
364                                         break; \
365                                 }
366                         switch(attacker.killcount)
367                         {
368                                 KILL_SPREE_LIST
369                                 default: break;
370                         }
371                         #undef SPREE_ITEM
372
373                         if(!checkrules_firstblood)
374                         {
375                                 checkrules_firstblood = true;
376                                 notif_firstblood = true; // modify the current messages so that they too show firstblood information
377                                 PS_GR_P_ADDVAL(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
378                                 PS_GR_P_ADDVAL(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
379
380                                 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
381                                 kill_count_to_attacker = -1;
382                                 kill_count_to_target = -2;
383                         }
384                         else
385                         {
386                                 kill_count_to_attacker = attacker.killcount;
387                                 kill_count_to_target = 0;
388                         }
389
390                         if(targ.istypefrag)
391                         {
392                                 Send_Notification(
393                                         NOTIF_ONE,
394                                         attacker,
395                                         MSG_CHOICE,
396                                         CHOICE_TYPEFRAG,
397                                         targ.netname,
398                                         kill_count_to_attacker,
399                                         (IS_BOT_CLIENT(targ) ? -1 : targ.ping)
400                                 );
401                                 Send_Notification(
402                                         NOTIF_ONE,
403                                         targ,
404                                         MSG_CHOICE,
405                                         CHOICE_TYPEFRAGGED,
406                                         attacker.netname,
407                                         kill_count_to_target,
408                                         attacker.health,
409                                         attacker.armorvalue,
410                                         (IS_BOT_CLIENT(attacker) ? -1 : attacker.ping)
411                                 );
412                         }
413                         else
414                         {
415                                 Send_Notification(
416                                         NOTIF_ONE,
417                                         attacker,
418                                         MSG_CHOICE,
419                                         CHOICE_FRAG,
420                                         targ.netname,
421                                         kill_count_to_attacker,
422                                         (IS_BOT_CLIENT(targ) ? -1 : targ.ping)
423                                 );
424                                 Send_Notification(
425                                         NOTIF_ONE,
426                                         targ,
427                                         MSG_CHOICE,
428                                         CHOICE_FRAGGED,
429                                         attacker.netname,
430                                         kill_count_to_target,
431                                         attacker.health,
432                                         attacker.armorvalue,
433                                         (IS_BOT_CLIENT(attacker) ? -1 : attacker.ping)
434                                 );
435                         }
436
437                         int f3 = 0;
438                         if(deathtype == DEATH_BUFF.m_id)
439                                 f3 = buff_FirstFromFlags(attacker.buffs).m_id;
440
441                         if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, targ.killcount, kill_count_to_attacker))
442                                 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, targ.killcount, kill_count_to_attacker, f3);
443                 }
444         }
445
446         // =============
447         // ACCIDENT/TRAP
448         // =============
449         else
450         {
451                 switch(DEATH_ENT(deathtype))
452                 {
453                         // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
454                         // Later on you will only be able to make custom messages using DEATH_CUSTOM,
455                         // and there will be a REAL DEATH_VOID implementation which mappers will use.
456                         case DEATH_HURTTRIGGER:
457                         {
458                                 Obituary_SpecialDeath(targ, false, deathtype,
459                                         targ.netname,
460                                         inflictor.message,
461                                         deathlocation,
462                                         targ.killcount,
463                                         0,
464                                         0);
465                                 break;
466                         }
467
468                         case DEATH_CUSTOM:
469                         {
470                                 Obituary_SpecialDeath(targ, false, deathtype,
471                                         targ.netname,
472                                         ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
473                                         deathlocation,
474                                         targ.killcount,
475                                         0,
476                                         0);
477                                 break;
478                         }
479
480                         default:
481                         {
482                                 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.killcount, 0, 0);
483                                 break;
484                         }
485                 }
486
487                 LogDeath("accident", deathtype, targ, targ);
488                 GiveFrags(targ, targ, -1, deathtype);
489
490                 if(PlayerScore_Add(targ, SP_SCORE, 0) == -5)
491                 {
492                         Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
493                         PS_GR_P_ADDVAL(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
494                 }
495         }
496
497         // reset target kill count
498         if(targ.killcount) { targ.killcount = 0; }
499 }
500
501 void Ice_Think()
502 {SELFPARAM();
503         if(!STAT(FROZEN, self.owner) || self.owner.iceblock != self)
504         {
505                 remove(self);
506                 return;
507         }
508         setorigin(self, self.owner.origin - '0 0 16');
509         self.nextthink = time;
510 }
511
512 void Freeze (entity targ, float freeze_time, float frozen_type, float show_waypoint)
513 {SELFPARAM();
514         if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // only specified entities can be freezed
515                 return;
516
517         if(STAT(FROZEN, targ))
518                 return;
519
520         float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
521
522         STAT(FROZEN, targ) = frozen_type;
523         targ.revive_progress = ((frozen_type == 3) ? 1 : 0);
524         targ.health = ((frozen_type == 3) ? targ_maxhealth : 1);
525         targ.revive_speed = freeze_time;
526         self.bot_attack = false;
527
528         entity ice = new(ice);
529         ice.owner = targ;
530         ice.scale = targ.scale;
531         ice.think = Ice_Think;
532         ice.nextthink = time;
533         ice.frame = floor(random() * 21); // ice model has 20 different looking frames
534         setmodel(ice, MDL_ICE);
535         ice.alpha = 1;
536         ice.colormod = Team_ColorRGB(targ.team);
537         ice.glowmod = ice.colormod;
538         targ.iceblock = ice;
539         targ.revival_time = 0;
540
541         WITH(entity, self, ice, Ice_Think());
542
543         RemoveGrapplingHook(targ);
544
545         FOREACH_CLIENT(IS_PLAYER(it) && it.hook.aiment == targ, LAMBDA(RemoveGrapplingHook(it)));
546
547         // add waypoint
548         if(show_waypoint)
549                 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', world, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
550 }
551
552 void Unfreeze (entity targ)
553 {
554         if(!STAT(FROZEN, targ))
555                 return;
556
557         if(STAT(FROZEN, targ) && STAT(FROZEN, targ) != 3) // only reset health if target was frozen
558                 targ.health = ((IS_PLAYER(targ)) ? start_health : targ.max_health);
559
560         STAT(FROZEN, targ) = 0;
561         targ.revive_progress = 0;
562         targ.revival_time = time;
563         self.bot_attack = true;
564
565         WaypointSprite_Kill(targ.waypointsprite_attached);
566
567         FOREACH_CLIENT(IS_PLAYER(it) && it.hook.aiment == targ, LAMBDA(RemoveGrapplingHook(it)));
568
569         // remove the ice block
570         if(targ.iceblock)
571                 remove(targ.iceblock);
572         targ.iceblock = world;
573 }
574
575 void Damage (entity targ, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
576 {SELFPARAM();
577         float mirrordamage;
578         float mirrorforce;
579         float complainteamdamage = 0;
580         entity attacker_save;
581         mirrordamage = 0;
582         mirrorforce = 0;
583
584         if (gameover || targ.killcount == FRAGS_SPECTATOR)
585                 return;
586
587         setself(targ);
588         damage_targ = targ;
589         damage_inflictor = inflictor;
590         damage_attacker = attacker;
591                 attacker_save = attacker;
592
593         if(IS_PLAYER(targ))
594                 if(targ.hook)
595                         if(targ.hook.aiment)
596                                 if(targ.hook.aiment == attacker)
597                                         RemoveGrapplingHook(targ); // STOP THAT, you parasite!
598
599         // special rule: gravity bomb does not hit team mates (other than for disconnecting the hook)
600         if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || DEATH_ISWEAPON(deathtype, WEP_TUBA))
601         {
602                 if(IS_PLAYER(targ))
603                         if(SAME_TEAM(targ, attacker))
604                         {
605                                 setself(this);
606                                 return;
607                         }
608         }
609
610         if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
611         {
612                 // exit the vehicle before killing (fixes a crash)
613                 if(IS_PLAYER(targ) && targ.vehicle)
614                         vehicles_exit(VHEF_RELEASE);
615
616                 // These are ALWAYS lethal
617                 // No damage modification here
618                 // Instead, prepare the victim for his death...
619                 targ.armorvalue = 0;
620                 targ.spawnshieldtime = 0;
621                 targ.health = 0.9; // this is < 1
622                 targ.flags -= targ.flags & FL_GODMODE;
623                 damage = 100000;
624         }
625         else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
626         {
627                 // no processing
628         }
629         else
630         {
631                 // nullify damage if teamplay is on
632                 if(deathtype != DEATH_TELEFRAG.m_id)
633                 if(IS_PLAYER(attacker))
634                 {
635                         if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
636                         {
637                                 damage = 0;
638                                 force = '0 0 0';
639                         }
640                         else if(SAME_TEAM(attacker, targ))
641                         {
642                                 if(autocvar_teamplay_mode == 1)
643                                         damage = 0;
644                                 else if(attacker != targ)
645                                 {
646                                         if(autocvar_teamplay_mode == 3)
647                                                 damage = 0;
648                                         else if(autocvar_teamplay_mode == 4)
649                                         {
650                                                 if(IS_PLAYER(targ) && !IS_DEAD(targ))
651                                                 {
652                                                         attacker.dmg_team = attacker.dmg_team + damage;
653                                                         complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
654                                                         if(complainteamdamage > 0)
655                                                                 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
656                                                         mirrorforce = autocvar_g_mirrordamage * vlen(force);
657                                                         damage = autocvar_g_friendlyfire * damage;
658                                                         // mirrordamage will be used LATER
659
660                                                         if(autocvar_g_mirrordamage_virtual)
661                                                         {
662                                                                 vector v  = healtharmor_applydamage(attacker.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
663                                                                 attacker.dmg_take += v.x;
664                                                                 attacker.dmg_save += v.y;
665                                                                 attacker.dmg_inflictor = inflictor;
666                                                                 mirrordamage = v.z;
667                                                                 mirrorforce = 0;
668                                                         }
669
670                                                         if(autocvar_g_friendlyfire_virtual)
671                                                         {
672                                                                 vector v = healtharmor_applydamage(targ.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, damage);
673                                                                 targ.dmg_take += v.x;
674                                                                 targ.dmg_save += v.y;
675                                                                 targ.dmg_inflictor = inflictor;
676                                                                 damage = 0;
677                                                                 if(!autocvar_g_friendlyfire_virtual_force)
678                                                                         force = '0 0 0';
679                                                         }
680                                                 }
681                                                 else
682                                                         damage = 0;
683                                         }
684                                 }
685                         }
686                 }
687
688                 if (!DEATH_ISSPECIAL(deathtype))
689                 {
690                         damage *= g_weapondamagefactor;
691                         mirrordamage *= g_weapondamagefactor;
692                         complainteamdamage *= g_weapondamagefactor;
693                         force = force * g_weaponforcefactor;
694                         mirrorforce *= g_weaponforcefactor;
695                 }
696
697                 // should this be changed at all? If so, in what way?
698                 MUTATOR_CALLHOOK(PlayerDamage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force);
699                 damage = frag_damage;
700                 mirrordamage = frag_mirrordamage;
701                 force = frag_force;
702
703                 if(STAT(FROZEN, targ))
704                 if(deathtype != DEATH_HURTTRIGGER.m_id && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
705                 {
706                         if(autocvar_g_frozen_revive_falldamage > 0)
707                         if(deathtype == DEATH_FALL.m_id)
708                         if(damage >= autocvar_g_frozen_revive_falldamage)
709                         {
710                                 Unfreeze(targ);
711                                 targ.health = autocvar_g_frozen_revive_falldamage_health;
712                                 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
713                                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
714                                 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
715                         }
716
717                         damage = 0;
718                         force *= autocvar_g_frozen_force;
719                 }
720
721                 if(STAT(FROZEN, targ) && deathtype == DEATH_HURTTRIGGER.m_id && !autocvar_g_frozen_damage_trigger)
722                 {
723                         Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
724
725                         setself(targ);
726                         entity spot = SelectSpawnPoint (false);
727
728                         if(spot)
729                         {
730                                 damage = 0;
731                                 self.deadflag = DEAD_NO;
732
733                                 self.angles = spot.angles;
734
735                                 self.effects = 0;
736                                 self.effects |= EF_TELEPORT_BIT;
737
738                                 self.angles_z = 0; // never spawn tilted even if the spot says to
739                                 self.fixangle = true; // turn this way immediately
740                                 self.velocity = '0 0 0';
741                                 self.avelocity = '0 0 0';
742                                 self.punchangle = '0 0 0';
743                                 self.punchvector = '0 0 0';
744                                 self.oldvelocity = self.velocity;
745
746                                 self.spawnorigin = spot.origin;
747                                 setorigin (self, spot.origin + '0 0 1' * (1 - self.mins.z - 24));
748                                 // don't reset back to last position, even if new position is stuck in solid
749                                 self.oldorigin = self.origin;
750                                 self.prevorigin = self.origin;
751
752                                 Send_Effect(EFFECT_TELEPORT, self.origin, '0 0 0', 1);
753                         }
754
755                         setself(this);
756                 }
757
758                 if(!g_instagib)
759                 {
760                         // apply strength multiplier
761                         if (attacker.items & ITEM_Strength.m_itemid)
762                         {
763                                 if(targ == attacker)
764                                 {
765                                         damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
766                                         force = force * autocvar_g_balance_powerup_strength_selfforce;
767                                 }
768                                 else
769                                 {
770                                         damage = damage * autocvar_g_balance_powerup_strength_damage;
771                                         force = force * autocvar_g_balance_powerup_strength_force;
772                                 }
773                         }
774
775                         // apply invincibility multiplier
776                         if (targ.items & ITEM_Shield.m_itemid)
777                                 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
778                 }
779
780                 if (targ == attacker)
781                         damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
782
783                 // count the damage
784                 if(attacker)
785                 if(!IS_DEAD(targ))
786                 if(deathtype != DEATH_BUFF.m_id)
787                 if(targ.takedamage == DAMAGE_AIM)
788                 if(targ != attacker)
789                 {
790                         entity victim;
791                         if(IS_VEHICLE(targ) && targ.owner)
792                                 victim = targ.owner;
793                         else
794                                 victim = targ;
795
796                         if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim))
797                         {
798                                 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
799                                 {
800                                         if(damage > 0)
801                                         {
802                                                 if(deathtype != DEATH_FIRE.m_id)
803                                                 {
804                                                         if(PHYS_INPUT_BUTTON_CHAT(victim))
805                                                                 attacker.typehitsound += 1;
806                                                         else
807                                                                 attacker.damage_dealt += damage;
808                                                 }
809
810                                                 damage_goodhits += 1;
811                                                 damage_gooddamage += damage;
812
813                                                 if (!DEATH_ISSPECIAL(deathtype))
814                                                 {
815                                                         if(IS_PLAYER(targ)) // don't do this for vehicles
816                                                         if(IsFlying(victim))
817                                                                 yoda = 1;
818                                                 }
819                                         }
820                                 }
821                                 else
822                                 {
823                                         if(deathtype != DEATH_FIRE.m_id)
824                                         {
825                                                 attacker.typehitsound += 1;
826                                         }
827                                         if(complainteamdamage > 0)
828                                                 if(time > attacker.teamkill_complain)
829                                                 {
830                                                         attacker.teamkill_complain = time + 5;
831                                                         attacker.teamkill_soundtime = time + 0.4;
832                                                         attacker.teamkill_soundsource = targ;
833                                                 }
834                                 }
835                         }
836                 }
837         }
838
839         // apply push
840         if (self.damageforcescale)
841         if (vlen(force))
842         if (!IS_PLAYER(self) || time >= self.spawnshieldtime || self == attacker)
843         {
844                 vector farce = damage_explosion_calcpush(self.damageforcescale * force, self.velocity, autocvar_g_balance_damagepush_speedfactor);
845                 if(self.movetype == MOVETYPE_PHYSICS)
846                 {
847                         entity farcent = new(farce);
848                         farcent.enemy = self;
849                         farcent.movedir = farce * 10;
850                         if(self.mass)
851                                 farcent.movedir = farcent.movedir * self.mass;
852                         farcent.origin = hitloc;
853                         farcent.forcetype = FORCETYPE_FORCEATPOS;
854                         farcent.nextthink = time + 0.1;
855                         farcent.think = SUB_Remove_self;
856                 }
857                 else
858                 {
859                         self.velocity = self.velocity + farce;
860                         self.move_velocity = self.velocity;
861                 }
862                 UNSET_ONGROUND(self);
863                 self.move_flags &= ~FL_ONGROUND;
864                 UpdateCSQCProjectile(self);
865         }
866         // apply damage
867         if (damage != 0 || (self.damageforcescale && vlen(force)))
868         if (self.event_damage)
869                 self.event_damage (self, inflictor, attacker, damage, deathtype, hitloc, force);
870         setself(this);
871
872         // apply mirror damage if any
873         if(mirrordamage > 0 || mirrorforce > 0)
874         {
875                 attacker = attacker_save;
876
877                 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
878                 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, attacker.origin, force);
879         }
880 }
881
882 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float inflictorselfdamage, float forceintensity, int deathtype, entity directhitentity)
883         // Returns total damage applies to creatures
884 {
885         entity  targ;
886         vector  force;
887         float   total_damage_to_creatures;
888         entity  next;
889         float   tfloordmg;
890         float   tfloorforce;
891
892         float stat_damagedone;
893
894         if(RadiusDamage_running)
895         {
896                 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
897                 return 0;
898         }
899
900         RadiusDamage_running = 1;
901
902         tfloordmg = autocvar_g_throughfloor_damage;
903         tfloorforce = autocvar_g_throughfloor_force;
904
905         total_damage_to_creatures = 0;
906
907         if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
908                 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
909                 {
910                         force = inflictorvelocity;
911                         if(vlen(force) == 0)
912                                 force = '0 0 -1';
913                         else
914                                 force = normalize(force);
915                         if(forceintensity >= 0)
916                                 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
917                         else
918                                 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
919                 }
920
921         stat_damagedone = 0;
922
923         targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
924         while (targ)
925         {
926                 next = targ.chain;
927                 if ((targ != inflictor) || inflictorselfdamage)
928                 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
929                 if (targ.takedamage)
930                 {
931                         vector nearest;
932                         vector diff;
933                         float power;
934
935                         // LordHavoc: measure distance to nearest point on target (not origin)
936                         // (this guarentees 100% damage on a touch impact)
937                         nearest = targ.WarpZone_findradius_nearest;
938                         diff = targ.WarpZone_findradius_dist;
939                         // round up a little on the damage to ensure full damage on impacts
940                         // and turn the distance into a fraction of the radius
941                         power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
942                         //bprint(" ");
943                         //bprint(ftos(power));
944                         //if (targ == attacker)
945                         //      print(ftos(power), "\n");
946                         if (power > 0)
947                         {
948                                 float finaldmg;
949                                 if (power > 1)
950                                         power = 1;
951                                 finaldmg = coredamage * power + edgedamage * (1 - power);
952                                 if (finaldmg > 0)
953                                 {
954                                         float a;
955                                         float c;
956                                         vector hitloc;
957                                         vector myblastorigin;
958                                         vector center;
959
960                                         myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
961
962                                         // if it's a player, use the view origin as reference
963                                         center = CENTER_OR_VIEWOFS(targ);
964
965                                         force = normalize(center - myblastorigin);
966                                         force = force * (finaldmg / coredamage) * forceintensity;
967                                         hitloc = nearest;
968
969                                         if(deathtype & WEP_BLASTER.m_id)
970                                                 force *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
971
972                                         if(targ != directhitentity)
973                                         {
974                                                 float hits;
975                                                 float total;
976                                                 float hitratio;
977                                                 float mininv_f, mininv_d;
978
979                                                 // test line of sight to multiple positions on box,
980                                                 // and do damage if any of them hit
981                                                 hits = 0;
982
983                                                 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
984                                                 // so for a given max stddev:
985                                                 // n = (1 / (2 * max stddev of hitratio))^2
986
987                                                 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
988                                                 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
989
990                                                 if(autocvar_g_throughfloor_debug)
991                                                         LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
992
993
994                                                 total = 0.25 * pow(max(mininv_f, mininv_d), 2);
995
996                                                 if(autocvar_g_throughfloor_debug)
997                                                         LOG_INFOF(" steps=%f", total);
998
999
1000                                                 if (IS_PLAYER(targ))
1001                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1002                                                 else
1003                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1004
1005                                                 if(autocvar_g_throughfloor_debug)
1006                                                         LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1007
1008                                                 for(c = 0; c < total; ++c)
1009                                                 {
1010                                                         //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1011                                                         WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1012                                                         if (trace_fraction == 1 || trace_ent == targ)
1013                                                         {
1014                                                                 ++hits;
1015                                                                 if (hits > 1)
1016                                                                         hitloc = hitloc + nearest;
1017                                                                 else
1018                                                                         hitloc = nearest;
1019                                                         }
1020                                                         nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1021                                                         nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1022                                                         nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1023                                                 }
1024
1025                                                 nearest = hitloc * (1 / max(1, hits));
1026                                                 hitratio = (hits / total);
1027                                                 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1028                                                 finaldmg = finaldmg * a;
1029                                                 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1030                                                 force = force * a;
1031
1032                                                 if(autocvar_g_throughfloor_debug)
1033                                                         LOG_INFOF(" D=%f F=%f\n", finaldmg, vlen(force));
1034                                         }
1035
1036                                         //if (targ == attacker)
1037                                         //{
1038                                         //      print("hits ", ftos(hits), " / ", ftos(total));
1039                                         //      print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1040                                         //      print(" (", ftos(a), ")\n");
1041                                         //}
1042                                         if(finaldmg || vlen(force))
1043                                         {
1044                                                 if(targ.iscreature)
1045                                                 {
1046                                                         total_damage_to_creatures += finaldmg;
1047
1048                                                         if(accuracy_isgooddamage(attacker, targ))
1049                                                                 stat_damagedone += finaldmg;
1050                                                 }
1051
1052                                                 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1053                                                         Damage (targ, inflictor, attacker, finaldmg, deathtype, nearest, force);
1054                                                 else
1055                                                         Damage (targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, nearest, force);
1056                                         }
1057                                 }
1058                         }
1059                 }
1060                 targ = next;
1061         }
1062
1063         RadiusDamage_running = 0;
1064
1065         if(!DEATH_ISSPECIAL(deathtype))
1066                 accuracy_add(attacker, DEATH_WEAPONOF(deathtype).m_id, 0, min(coredamage, stat_damagedone));
1067
1068         return total_damage_to_creatures;
1069 }
1070
1071 float RadiusDamage (entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, entity directhitentity)
1072 {
1073         return RadiusDamageForSource (inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, deathtype, directhitentity);
1074 }
1075
1076 float Fire_IsBurning(entity e)
1077 {
1078         return (time < e.fire_endtime);
1079 }
1080
1081 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1082 {
1083         float dps;
1084         float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1085
1086         if(IS_PLAYER(e))
1087         {
1088                 if(IS_DEAD(e))
1089                         return -1;
1090         }
1091         else
1092         {
1093                 if(!e.fire_burner)
1094                 {
1095                         // print("adding a fire burner to ", e.classname, "\n");
1096                         e.fire_burner = new(fireburner);
1097                         e.fire_burner.think = fireburner_think;
1098                         e.fire_burner.nextthink = time;
1099                         e.fire_burner.owner = e;
1100                 }
1101         }
1102
1103         t = max(t, 0.1);
1104         dps = d / t;
1105         if(Fire_IsBurning(e))
1106         {
1107                 mintime = e.fire_endtime - time;
1108                 maxtime = max(mintime, t);
1109
1110                 mindps = e.fire_damagepersec;
1111                 maxdps = max(mindps, dps);
1112
1113                 if(maxtime > mintime || maxdps > mindps)
1114                 {
1115                         // Constraints:
1116
1117                         // damage we have right now
1118                         mindamage = mindps * mintime;
1119
1120                         // damage we want to get
1121                         maxdamage = mindamage + d;
1122
1123                         // but we can't exceed maxtime * maxdps!
1124                         totaldamage = min(maxdamage, maxtime * maxdps);
1125
1126                         // LEMMA:
1127                         // Look at:
1128                         // totaldamage = min(mindamage + d, maxtime * maxdps)
1129                         // We see:
1130                         // totaldamage <= maxtime * maxdps
1131                         // ==> totaldamage / maxdps <= maxtime.
1132                         // We also see:
1133                         // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1134                         //                     >= min(mintime, maxtime)
1135                         // ==> totaldamage / maxdps >= mintime.
1136
1137                         /*
1138                         // how long do we damage then?
1139                         // at least as long as before
1140                         // but, never exceed maxdps
1141                         totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1142                         */
1143
1144                         // alternate:
1145                         // at most as long as maximum allowed
1146                         // but, never below mindps
1147                         totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1148
1149                         // assuming t > mintime, dps > mindps:
1150                         // we get d = t * dps = maxtime * maxdps
1151                         // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1152                         // totaldamage / maxdps = maxtime
1153                         // totaldamage / mindps > totaldamage / maxdps = maxtime
1154                         // FROM THIS:
1155                         // a) totaltime = max(mintime, maxtime) = maxtime
1156                         // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1157
1158                         // assuming t <= mintime:
1159                         // we get maxtime = mintime
1160                         // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1161                         // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1162
1163                         // assuming dps <= mindps:
1164                         // we get mindps = maxdps.
1165                         // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1166                         // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1167                         // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1168
1169                         e.fire_damagepersec = totaldamage / totaltime;
1170                         e.fire_endtime = time + totaltime;
1171                         if(totaldamage > 1.2 * mindamage)
1172                         {
1173                                 e.fire_deathtype = dt;
1174                                 if(e.fire_owner != o)
1175                                 {
1176                                         e.fire_owner = o;
1177                                         e.fire_hitsound = false;
1178                                 }
1179                         }
1180                         if(accuracy_isgooddamage(o, e))
1181                                 accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, max(0, totaldamage - mindamage));
1182                         return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1183                 }
1184                 else
1185                         return 0;
1186         }
1187         else
1188         {
1189                 e.fire_damagepersec = dps;
1190                 e.fire_endtime = time + t;
1191                 e.fire_deathtype = dt;
1192                 e.fire_owner = o;
1193                 e.fire_hitsound = false;
1194                 if(accuracy_isgooddamage(o, e))
1195                         accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, d);
1196                 return d;
1197         }
1198 }
1199
1200 void Fire_ApplyDamage(entity e)
1201 {
1202         float t, d, hi, ty;
1203         entity o;
1204
1205         if (!Fire_IsBurning(e))
1206                 return;
1207
1208         for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1209         if(IS_NOT_A_CLIENT(o))
1210                 o = e.fire_owner;
1211
1212         // water and slime stop fire
1213         if(e.waterlevel)
1214         if(e.watertype != CONTENT_LAVA)
1215                 e.fire_endtime = 0;
1216
1217         // ice stops fire
1218         if(STAT(FROZEN, e))
1219                 e.fire_endtime = 0;
1220
1221         t = min(frametime, e.fire_endtime - time);
1222         d = e.fire_damagepersec * t;
1223
1224         hi = e.fire_owner.damage_dealt;
1225         ty = e.fire_owner.typehitsound;
1226         Damage(e, e, e.fire_owner, d, e.fire_deathtype, e.origin, '0 0 0');
1227         if(e.fire_hitsound && e.fire_owner)
1228         {
1229                 e.fire_owner.damage_dealt = hi;
1230                 e.fire_owner.typehitsound = ty;
1231         }
1232         e.fire_hitsound = true;
1233
1234         if(!IS_INDEPENDENT_PLAYER(e))
1235         if(!STAT(FROZEN, e))
1236                 FOREACH_CLIENT(IS_PLAYER(it) && it != e, LAMBDA(
1237                         if(!IS_DEAD(it))
1238                         if(!IS_INDEPENDENT_PLAYER(it))
1239                         if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1240                         {
1241                                 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1242                                 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1243                                 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1244                         }
1245                 ));
1246 }
1247
1248 void Fire_ApplyEffect(entity e)
1249 {
1250         if(Fire_IsBurning(e))
1251                 e.effects |= EF_FLAME;
1252         else
1253                 e.effects &= ~EF_FLAME;
1254 }
1255
1256 void fireburner_think()
1257 {SELFPARAM();
1258         // for players, this is done in the regular loop
1259         if(wasfreed(self.owner))
1260         {
1261                 remove(self);
1262                 return;
1263         }
1264         Fire_ApplyEffect(self.owner);
1265         if(!Fire_IsBurning(self.owner))
1266         {
1267                 self.owner.fire_burner = world;
1268                 remove(self);
1269                 return;
1270         }
1271         Fire_ApplyDamage(self.owner);
1272         self.nextthink = time;
1273 }