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