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