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