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