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