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