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