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