]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/g_damage.qc
e5363db7738fd9a79b5206fbcaa0262110350a49
[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         setorigin(this, this.owner.origin - '0 0 16');
472         this.nextthink = time;
473 }
474
475 void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
476 {
477         if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
478                 return;
479
480         if(STAT(FROZEN, targ))
481                 return;
482
483         float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
484
485         STAT(FROZEN, targ) = frozen_type;
486         STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
487         SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
488         targ.revive_speed = revivespeed;
489         if(targ.bot_attack)
490                 IL_REMOVE(g_bot_targets, targ);
491         targ.bot_attack = false;
492         targ.freeze_time = time;
493
494         entity ice = new(ice);
495         ice.owner = targ;
496         ice.scale = targ.scale;
497         setthink(ice, Ice_Think);
498         ice.nextthink = time;
499         ice.frame = floor(random() * 21); // ice model has 20 different looking frames
500         setmodel(ice, MDL_ICE);
501         ice.alpha = 1;
502         ice.colormod = Team_ColorRGB(targ.team);
503         ice.glowmod = ice.colormod;
504         targ.iceblock = ice;
505         targ.revival_time = 0;
506
507         Ice_Think(ice);
508
509         RemoveGrapplingHooks(targ);
510
511         FOREACH_CLIENT(IS_PLAYER(it),
512         {
513                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
514             {
515                 .entity weaponentity = weaponentities[slot];
516                 if(it.(weaponentity).hook.aiment == targ)
517                         RemoveHook(it.(weaponentity).hook);
518             }
519         });
520
521         // add waypoint
522         if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
523                 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
524 }
525
526 void Unfreeze(entity targ, bool reset_health)
527 {
528         if(!STAT(FROZEN, targ))
529                 return;
530
531         if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
532                 SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
533
534         targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
535
536         STAT(FROZEN, targ) = 0;
537         STAT(REVIVE_PROGRESS, targ) = 0;
538         targ.revival_time = time;
539         if(!targ.bot_attack)
540                 IL_PUSH(g_bot_targets, targ);
541         targ.bot_attack = true;
542
543         WaypointSprite_Kill(targ.waypointsprite_attached);
544
545         FOREACH_CLIENT(IS_PLAYER(it),
546         {
547                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
548             {
549                 .entity weaponentity = weaponentities[slot];
550                 if(it.(weaponentity).hook.aiment == targ)
551                         RemoveHook(it.(weaponentity).hook);
552             }
553         });
554
555         // remove the ice block
556         if(targ.iceblock)
557                 delete(targ.iceblock);
558         targ.iceblock = NULL;
559
560         MUTATOR_CALLHOOK(Unfreeze, targ);
561 }
562
563 void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
564 {
565         float complainteamdamage = 0;
566         float mirrordamage = 0;
567         float mirrorforce = 0;
568
569         if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
570                 return;
571
572         entity attacker_save = attacker;
573
574         // special rule: gravity bomb does not hit team mates (other than for disconnecting the hook)
575         if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || DEATH_ISWEAPON(deathtype, WEP_TUBA))
576         {
577                 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
578                 {
579                         return;
580                 }
581         }
582
583         if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
584         {
585                 // exit the vehicle before killing (fixes a crash)
586                 if(IS_PLAYER(targ) && targ.vehicle)
587                         vehicles_exit(targ.vehicle, VHEF_RELEASE);
588
589                 // These are ALWAYS lethal
590                 // No damage modification here
591                 // Instead, prepare the victim for his death...
592                 SetResourceExplicit(targ, RES_ARMOR, 0);
593                 targ.spawnshieldtime = 0;
594                 SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
595                 targ.flags -= targ.flags & FL_GODMODE;
596                 damage = 100000;
597         }
598         else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
599         {
600                 // no processing
601         }
602         else
603         {
604                 // nullify damage if teamplay is on
605                 if(deathtype != DEATH_TELEFRAG.m_id)
606                 if(IS_PLAYER(attacker))
607                 {
608                         if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
609                         {
610                                 damage = 0;
611                                 force = '0 0 0';
612                         }
613                         else if(SAME_TEAM(attacker, targ))
614                         {
615                                 if(autocvar_teamplay_mode == 1)
616                                         damage = 0;
617                                 else if(attacker != targ)
618                                 {
619                                         if(autocvar_teamplay_mode == 3)
620                                                 damage = 0;
621                                         else if(autocvar_teamplay_mode == 4)
622                                         {
623                                                 if(IS_PLAYER(targ) && !IS_DEAD(targ))
624                                                 {
625                                                         attacker.dmg_team = attacker.dmg_team + damage;
626                                                         complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
627                                                         if(complainteamdamage > 0)
628                                                                 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
629                                                         mirrorforce = autocvar_g_mirrordamage * vlen(force);
630                                                         damage = autocvar_g_friendlyfire * damage;
631                                                         // mirrordamage will be used LATER
632
633                                                         if(autocvar_g_mirrordamage_virtual)
634                                                         {
635                                                                 vector v  = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
636                                                                 attacker.dmg_take += v.x;
637                                                                 attacker.dmg_save += v.y;
638                                                                 attacker.dmg_inflictor = inflictor;
639                                                                 mirrordamage = v.z;
640                                                                 mirrorforce = 0;
641                                                         }
642
643                                                         if(autocvar_g_friendlyfire_virtual)
644                                                         {
645                                                                 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
646                                                                 targ.dmg_take += v.x;
647                                                                 targ.dmg_save += v.y;
648                                                                 targ.dmg_inflictor = inflictor;
649                                                                 damage = 0;
650                                                                 if(!autocvar_g_friendlyfire_virtual_force)
651                                                                         force = '0 0 0';
652                                                         }
653                                                 }
654                                                 else if(!targ.canteamdamage)
655                                                         damage = 0;
656                                         }
657                                 }
658                         }
659                 }
660
661                 if (!DEATH_ISSPECIAL(deathtype))
662                 {
663                         damage *= g_weapondamagefactor;
664                         mirrordamage *= g_weapondamagefactor;
665                         complainteamdamage *= g_weapondamagefactor;
666                         force = force * g_weaponforcefactor;
667                         mirrorforce *= g_weaponforcefactor;
668                 }
669
670                 // should this be changed at all? If so, in what way?
671                 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
672                 damage = M_ARGV(4, float);
673                 mirrordamage = M_ARGV(5, float);
674                 force = M_ARGV(6, vector);
675
676                 if(IS_PLAYER(targ) && damage > 0 && attacker)
677                 {
678                         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
679                     {
680                         .entity went = weaponentities[slot];
681                         if(targ.(went).hook && targ.(went).hook.aiment == attacker)
682                                 RemoveHook(targ.(went).hook);
683                     }
684                 }
685
686                 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
687                         && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
688                 {
689                         if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
690                         {
691                                 Unfreeze(targ, false);
692                                 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
693                                 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
694                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
695                                 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
696                         }
697
698                         damage = 0;
699                         force *= autocvar_g_frozen_force;
700                 }
701
702                 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
703                         && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
704                 {
705                         Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
706
707                         entity spot = SelectSpawnPoint(targ, false);
708                         if(spot)
709                         {
710                                 damage = 0;
711                                 targ.deadflag = DEAD_NO;
712
713                                 targ.angles = spot.angles;
714
715                                 targ.effects = 0;
716                                 targ.effects |= EF_TELEPORT_BIT;
717
718                                 targ.angles_z = 0; // never spawn tilted even if the spot says to
719                                 targ.fixangle = true; // turn this way immediately
720                                 targ.velocity = '0 0 0';
721                                 targ.avelocity = '0 0 0';
722                                 targ.punchangle = '0 0 0';
723                                 targ.punchvector = '0 0 0';
724                                 targ.oldvelocity = targ.velocity;
725
726                                 targ.spawnorigin = spot.origin;
727                                 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
728                                 // don't reset back to last position, even if new position is stuck in solid
729                                 targ.oldorigin = targ.origin;
730
731                                 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
732                         }
733                 }
734
735                 if(!MUTATOR_IS_ENABLED(mutator_instagib))
736                 {
737                         // apply strength multiplier
738                         if (attacker.items & ITEM_Strength.m_itemid)
739                         {
740                                 if(targ == attacker)
741                                 {
742                                         damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
743                                         force = force * autocvar_g_balance_powerup_strength_selfforce;
744                                 }
745                                 else
746                                 {
747                                         damage = damage * autocvar_g_balance_powerup_strength_damage;
748                                         force = force * autocvar_g_balance_powerup_strength_force;
749                                 }
750                         }
751
752                         // apply invincibility multiplier
753                         if (targ.items & ITEM_Shield.m_itemid)
754                         {
755                                 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
756                                 if (targ != attacker)
757                                 {
758                                         force = force * autocvar_g_balance_powerup_invincible_takeforce;
759                                 }
760                         }
761                 }
762
763                 if (targ == attacker)
764                         damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
765
766                 // count the damage
767                 if(attacker)
768                 if(!IS_DEAD(targ))
769                 if(deathtype != DEATH_BUFF.m_id)
770                 if(targ.takedamage == DAMAGE_AIM)
771                 if(targ != attacker)
772                 {
773                         entity victim;
774                         if(IS_VEHICLE(targ) && targ.owner)
775                                 victim = targ.owner;
776                         else
777                                 victim = targ;
778
779                         if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
780                         {
781                                 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
782                                 {
783                                         if(damage > 0)
784                                         {
785                                                 if(deathtype != DEATH_FIRE.m_id)
786                                                 {
787                                                         if(PHYS_INPUT_BUTTON_CHAT(victim))
788                                                                 attacker.typehitsound += 1;
789                                                         else
790                                                                 attacker.damage_dealt += damage;
791                                                 }
792
793                                                 damage_goodhits += 1;
794                                                 damage_gooddamage += damage;
795
796                                                 if (!DEATH_ISSPECIAL(deathtype))
797                                                 {
798                                                         if(IS_PLAYER(targ)) // don't do this for vehicles
799                                                         if(IsFlying(victim))
800                                                                 yoda = 1;
801                                                 }
802                                         }
803                                 }
804                                 else if(IS_PLAYER(attacker))
805                                 {
806                                         // if enemy gets frozen in this frame and receives other damage don't
807                                         // play the typehitsound e.g. when hit by multiple bullets of the shotgun
808                                         if (deathtype != DEATH_FIRE.m_id && (!STAT(FROZEN, victim) || time > victim.freeze_time))
809                                         {
810                                                 attacker.typehitsound += 1;
811                                         }
812                                         if(complainteamdamage > 0)
813                                                 if(time > CS(attacker).teamkill_complain)
814                                                 {
815                                                         CS(attacker).teamkill_complain = time + 5;
816                                                         CS(attacker).teamkill_soundtime = time + 0.4;
817                                                         CS(attacker).teamkill_soundsource = targ;
818                                                 }
819                                 }
820                         }
821                 }
822         }
823
824         // apply push
825         if (targ.damageforcescale)
826         if (force)
827         if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
828         {
829                 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
830                 if(targ.move_movetype == MOVETYPE_PHYSICS)
831                 {
832                         entity farcent = new(farce);
833                         farcent.enemy = targ;
834                         farcent.movedir = farce * 10;
835                         if(targ.mass)
836                                 farcent.movedir = farcent.movedir * targ.mass;
837                         farcent.origin = hitloc;
838                         farcent.forcetype = FORCETYPE_FORCEATPOS;
839                         farcent.nextthink = time + 0.1;
840                         setthink(farcent, SUB_Remove);
841                 }
842                 else
843                 {
844                         targ.velocity = targ.velocity + farce;
845                 }
846                 UNSET_ONGROUND(targ);
847                 UpdateCSQCProjectile(targ);
848         }
849         // apply damage
850         if (damage != 0 || (targ.damageforcescale && force))
851         if (targ.event_damage)
852                 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
853
854         // apply mirror damage if any
855         if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
856         if(mirrordamage > 0 || mirrorforce > 0)
857         {
858                 attacker = attacker_save;
859
860                 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
861                 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
862         }
863 }
864
865 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
866                                                                 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
867         // Returns total damage applies to creatures
868 {
869         entity  targ;
870         vector  force;
871         float   total_damage_to_creatures;
872         entity  next;
873         float   tfloordmg;
874         float   tfloorforce;
875
876         float stat_damagedone;
877
878         if(RadiusDamage_running)
879         {
880                 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
881                 return 0;
882         }
883
884         RadiusDamage_running = 1;
885
886         tfloordmg = autocvar_g_throughfloor_damage;
887         tfloorforce = autocvar_g_throughfloor_force;
888
889         total_damage_to_creatures = 0;
890
891         if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
892                 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
893                 {
894                         force = inflictorvelocity;
895                         if(force == '0 0 0')
896                                 force = '0 0 -1';
897                         else
898                                 force = normalize(force);
899                         if(forceintensity >= 0)
900                                 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
901                         else
902                                 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
903                 }
904
905         stat_damagedone = 0;
906
907         targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
908         while (targ)
909         {
910                 next = targ.chain;
911                 if ((targ != inflictor) || inflictorselfdamage)
912                 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
913                 if (targ.takedamage)
914                 {
915                         vector nearest;
916                         vector diff;
917                         float power;
918
919                         // LordHavoc: measure distance to nearest point on target (not origin)
920                         // (this guarentees 100% damage on a touch impact)
921                         nearest = targ.WarpZone_findradius_nearest;
922                         diff = targ.WarpZone_findradius_dist;
923                         // round up a little on the damage to ensure full damage on impacts
924                         // and turn the distance into a fraction of the radius
925                         power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
926                         //bprint(" ");
927                         //bprint(ftos(power));
928                         //if (targ == attacker)
929                         //      print(ftos(power), "\n");
930                         if (power > 0)
931                         {
932                                 float finaldmg;
933                                 if (power > 1)
934                                         power = 1;
935                                 finaldmg = coredamage * power + edgedamage * (1 - power);
936                                 if (finaldmg > 0)
937                                 {
938                                         float a;
939                                         float c;
940                                         vector hitloc;
941                                         vector myblastorigin;
942                                         vector center;
943
944                                         myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
945
946                                         // if it's a player, use the view origin as reference
947                                         center = CENTER_OR_VIEWOFS(targ);
948
949                                         force = normalize(center - myblastorigin);
950                                         force = force * (finaldmg / coredamage) * forceintensity;
951                                         hitloc = nearest;
952
953                                         // apply special scaling along the z axis if set
954                                         // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
955                                         if(forcezscale)
956                                                 force.z *= forcezscale;
957
958                                         if(targ != directhitentity)
959                                         {
960                                                 float hits;
961                                                 float total;
962                                                 float hitratio;
963                                                 float mininv_f, mininv_d;
964
965                                                 // test line of sight to multiple positions on box,
966                                                 // and do damage if any of them hit
967                                                 hits = 0;
968
969                                                 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
970                                                 // so for a given max stddev:
971                                                 // n = (1 / (2 * max stddev of hitratio))^2
972
973                                                 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
974                                                 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
975
976                                                 if(autocvar_g_throughfloor_debug)
977                                                         LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
978
979
980                                                 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
981
982                                                 if(autocvar_g_throughfloor_debug)
983                                                         LOG_INFOF(" steps=%f", total);
984
985
986                                                 if (IS_PLAYER(targ))
987                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
988                                                 else
989                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
990
991                                                 if(autocvar_g_throughfloor_debug)
992                                                         LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
993
994                                                 for(c = 0; c < total; ++c)
995                                                 {
996                                                         //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
997                                                         WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
998                                                         if (trace_fraction == 1 || trace_ent == targ)
999                                                         {
1000                                                                 ++hits;
1001                                                                 if (hits > 1)
1002                                                                         hitloc = hitloc + nearest;
1003                                                                 else
1004                                                                         hitloc = nearest;
1005                                                         }
1006                                                         nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1007                                                         nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1008                                                         nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1009                                                 }
1010
1011                                                 nearest = hitloc * (1 / max(1, hits));
1012                                                 hitratio = (hits / total);
1013                                                 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1014                                                 finaldmg = finaldmg * a;
1015                                                 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1016                                                 force = force * a;
1017
1018                                                 if(autocvar_g_throughfloor_debug)
1019                                                         LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1020                                         }
1021
1022                                         //if (targ == attacker)
1023                                         //{
1024                                         //      print("hits ", ftos(hits), " / ", ftos(total));
1025                                         //      print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1026                                         //      print(" (", ftos(a), ")\n");
1027                                         //}
1028                                         if(finaldmg || force)
1029                                         {
1030                                                 if(targ.iscreature)
1031                                                 {
1032                                                         total_damage_to_creatures += finaldmg;
1033
1034                                                         if(accuracy_isgooddamage(attacker, targ))
1035                                                                 stat_damagedone += finaldmg;
1036                                                 }
1037
1038                                                 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1039                                                         Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1040                                                 else
1041                                                         Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1042                                         }
1043                                 }
1044                         }
1045                 }
1046                 targ = next;
1047         }
1048
1049         RadiusDamage_running = 0;
1050
1051         if(!DEATH_ISSPECIAL(deathtype))
1052                 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1053
1054         return total_damage_to_creatures;
1055 }
1056
1057 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1058 {
1059         return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, 
1060                                                                         cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1061 }
1062
1063 bool Heal(entity targ, entity inflictor, float amount, float limit)
1064 {
1065         if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1066                 return false;
1067
1068         bool healed = false;
1069         if(targ.event_heal)
1070                 healed = targ.event_heal(targ, inflictor, amount, limit);
1071         // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1072         // TODO: healing fx!
1073         // TODO: armor healing?
1074         return healed;
1075 }
1076
1077 float Fire_IsBurning(entity e)
1078 {
1079         return (time < e.fire_endtime);
1080 }
1081
1082 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1083 {
1084         float dps;
1085         float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1086
1087         if(IS_PLAYER(e))
1088         {
1089                 if(IS_DEAD(e))
1090                         return -1;
1091         }
1092         else
1093         {
1094                 if(!e.fire_burner)
1095                 {
1096                         // print("adding a fire burner to ", e.classname, "\n");
1097                         e.fire_burner = new(fireburner);
1098                         setthink(e.fire_burner, fireburner_think);
1099                         e.fire_burner.nextthink = time;
1100                         e.fire_burner.owner = e;
1101                 }
1102         }
1103
1104         t = max(t, 0.1);
1105         dps = d / t;
1106         if(Fire_IsBurning(e))
1107         {
1108                 mintime = e.fire_endtime - time;
1109                 maxtime = max(mintime, t);
1110
1111                 mindps = e.fire_damagepersec;
1112                 maxdps = max(mindps, dps);
1113
1114                 if(maxtime > mintime || maxdps > mindps)
1115                 {
1116                         // Constraints:
1117
1118                         // damage we have right now
1119                         mindamage = mindps * mintime;
1120
1121                         // damage we want to get
1122                         maxdamage = mindamage + d;
1123
1124                         // but we can't exceed maxtime * maxdps!
1125                         totaldamage = min(maxdamage, maxtime * maxdps);
1126
1127                         // LEMMA:
1128                         // Look at:
1129                         // totaldamage = min(mindamage + d, maxtime * maxdps)
1130                         // We see:
1131                         // totaldamage <= maxtime * maxdps
1132                         // ==> totaldamage / maxdps <= maxtime.
1133                         // We also see:
1134                         // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1135                         //                     >= min(mintime, maxtime)
1136                         // ==> totaldamage / maxdps >= mintime.
1137
1138                         /*
1139                         // how long do we damage then?
1140                         // at least as long as before
1141                         // but, never exceed maxdps
1142                         totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1143                         */
1144
1145                         // alternate:
1146                         // at most as long as maximum allowed
1147                         // but, never below mindps
1148                         totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1149
1150                         // assuming t > mintime, dps > mindps:
1151                         // we get d = t * dps = maxtime * maxdps
1152                         // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1153                         // totaldamage / maxdps = maxtime
1154                         // totaldamage / mindps > totaldamage / maxdps = maxtime
1155                         // FROM THIS:
1156                         // a) totaltime = max(mintime, maxtime) = maxtime
1157                         // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1158
1159                         // assuming t <= mintime:
1160                         // we get maxtime = mintime
1161                         // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1162                         // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1163
1164                         // assuming dps <= mindps:
1165                         // we get mindps = maxdps.
1166                         // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1167                         // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1168                         // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1169
1170                         e.fire_damagepersec = totaldamage / totaltime;
1171                         e.fire_endtime = time + totaltime;
1172                         if(totaldamage > 1.2 * mindamage)
1173                         {
1174                                 e.fire_deathtype = dt;
1175                                 if(e.fire_owner != o)
1176                                 {
1177                                         e.fire_owner = o;
1178                                         e.fire_hitsound = false;
1179                                 }
1180                         }
1181                         if(accuracy_isgooddamage(o, e))
1182                                 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1183                         return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1184                 }
1185                 else
1186                         return 0;
1187         }
1188         else
1189         {
1190                 e.fire_damagepersec = dps;
1191                 e.fire_endtime = time + t;
1192                 e.fire_deathtype = dt;
1193                 e.fire_owner = o;
1194                 e.fire_hitsound = false;
1195                 if(accuracy_isgooddamage(o, e))
1196                         accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1197                 return d;
1198         }
1199 }
1200
1201 void Fire_ApplyDamage(entity e)
1202 {
1203         float t, d, hi, ty;
1204         entity o;
1205
1206         if (!Fire_IsBurning(e))
1207                 return;
1208
1209         for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1210         if(IS_NOT_A_CLIENT(o))
1211                 o = e.fire_owner;
1212
1213         // water and slime stop fire
1214         if(e.waterlevel)
1215         if(e.watertype != CONTENT_LAVA)
1216                 e.fire_endtime = 0;
1217
1218         // ice stops fire
1219         if(STAT(FROZEN, e))
1220                 e.fire_endtime = 0;
1221
1222         t = min(frametime, e.fire_endtime - time);
1223         d = e.fire_damagepersec * t;
1224
1225         hi = e.fire_owner.damage_dealt;
1226         ty = e.fire_owner.typehitsound;
1227         Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1228         if(e.fire_hitsound && e.fire_owner)
1229         {
1230                 e.fire_owner.damage_dealt = hi;
1231                 e.fire_owner.typehitsound = ty;
1232         }
1233         e.fire_hitsound = true;
1234
1235         if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1236         {
1237                 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1238                 {
1239                         if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1240                         if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1241                         {
1242                                 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1243                                 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1244                                 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1245                         }
1246                 });
1247         }
1248 }
1249
1250 void Fire_ApplyEffect(entity e)
1251 {
1252         if(Fire_IsBurning(e))
1253                 e.effects |= EF_FLAME;
1254         else
1255                 e.effects &= ~EF_FLAME;
1256 }
1257
1258 void fireburner_think(entity this)
1259 {
1260         // for players, this is done in the regular loop
1261         if(wasfreed(this.owner))
1262         {
1263                 delete(this);
1264                 return;
1265         }
1266         Fire_ApplyEffect(this.owner);
1267         if(!Fire_IsBurning(this.owner))
1268         {
1269                 this.owner.fire_burner = NULL;
1270                 delete(this);
1271                 return;
1272         }
1273         Fire_ApplyDamage(this.owner);
1274         this.nextthink = time;
1275 }