Merge branch 'martin-t/spec' into 'master'
[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                         #define SPREE_ITEM(counta,countb,center,normal,gentle) \
379                                 case counta: \
380                                 { \
381                                         Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
382                                         PS_GR_P_ADDVAL(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
383                                         break; \
384                                 }
385                         switch(attacker.killcount)
386                         {
387                                 KILL_SPREE_LIST
388                                 default: break;
389                         }
390                         #undef SPREE_ITEM
391
392                         if(!checkrules_firstblood)
393                         {
394                                 checkrules_firstblood = true;
395                                 notif_firstblood = true; // modify the current messages so that they too show firstblood information
396                                 PS_GR_P_ADDVAL(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
397                                 PS_GR_P_ADDVAL(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
398
399                                 // tell spree_inf and spree_cen that this is a first-blood and first-victim event
400                                 kill_count_to_attacker = -1;
401                                 kill_count_to_target = -2;
402                         }
403                         else
404                         {
405                                 kill_count_to_attacker = attacker.killcount;
406                                 kill_count_to_target = 0;
407                         }
408
409                         if(targ.istypefrag)
410                         {
411                                 Send_Notification(
412                                         NOTIF_ONE,
413                                         attacker,
414                                         MSG_CHOICE,
415                                         CHOICE_TYPEFRAG,
416                                         targ.netname,
417                                         kill_count_to_attacker,
418                                         (IS_BOT_CLIENT(targ) ? -1 : targ.ping)
419                                 );
420                                 Send_Notification(
421                                         NOTIF_ONE,
422                                         targ,
423                                         MSG_CHOICE,
424                                         CHOICE_TYPEFRAGGED,
425                                         attacker.netname,
426                                         kill_count_to_target,
427                                         attacker.health,
428                                         attacker.armorvalue,
429                                         (IS_BOT_CLIENT(attacker) ? -1 : attacker.ping)
430                                 );
431                         }
432                         else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target))
433                         {
434                                 Send_Notification(
435                                         NOTIF_ONE,
436                                         attacker,
437                                         MSG_CHOICE,
438                                         CHOICE_FRAG,
439                                         targ.netname,
440                                         kill_count_to_attacker,
441                                         (IS_BOT_CLIENT(targ) ? -1 : targ.ping)
442                                 );
443                                 Send_Notification(
444                                         NOTIF_ONE,
445                                         targ,
446                                         MSG_CHOICE,
447                                         CHOICE_FRAGGED,
448                                         attacker.netname,
449                                         kill_count_to_target,
450                                         attacker.health,
451                                         attacker.armorvalue,
452                                         (IS_BOT_CLIENT(attacker) ? -1 : attacker.ping)
453                                 );
454                         }
455
456                         int f3 = 0;
457                         if(deathtype == DEATH_BUFF.m_id)
458                                 f3 = buff_FirstFromFlags(attacker.buffs).m_id;
459
460                         if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, targ.killcount, kill_count_to_attacker))
461                                 Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker.netname, deathlocation, targ.killcount, kill_count_to_attacker, f3);
462                 }
463         }
464
465         // =============
466         // ACCIDENT/TRAP
467         // =============
468         else
469         {
470                 switch(DEATH_ENT(deathtype))
471                 {
472                         // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
473                         // Later on you will only be able to make custom messages using DEATH_CUSTOM,
474                         // and there will be a REAL DEATH_VOID implementation which mappers will use.
475                         case DEATH_HURTTRIGGER:
476                         {
477                                 Obituary_SpecialDeath(targ, false, deathtype,
478                                         targ.netname,
479                                         inflictor.message,
480                                         deathlocation,
481                                         targ.killcount,
482                                         0,
483                                         0);
484                                 break;
485                         }
486
487                         case DEATH_CUSTOM:
488                         {
489                                 Obituary_SpecialDeath(targ, false, deathtype,
490                                         targ.netname,
491                                         ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
492                                         deathlocation,
493                                         targ.killcount,
494                                         0,
495                                         0);
496                                 break;
497                         }
498
499                         default:
500                         {
501                                 Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.killcount, 0, 0);
502                                 break;
503                         }
504                 }
505
506                 LogDeath("accident", deathtype, targ, targ);
507                 GiveFrags(targ, targ, -1, deathtype);
508
509                 if(PlayerScore_Add(targ, SP_SCORE, 0) == -5)
510                 {
511                         Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
512                         PS_GR_P_ADDVAL(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
513                 }
514         }
515
516         // reset target kill count
517         if(targ.killcount) { targ.killcount = 0; }
518 }
519
520 void Ice_Think(entity this)
521 {
522         if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
523         {
524                 delete(this);
525                 return;
526         }
527         setorigin(this, this.owner.origin - '0 0 16');
528         this.nextthink = time;
529 }
530
531 void Freeze (entity targ, float freeze_time, float frozen_type, float show_waypoint)
532 {
533         if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // only specified entities can be freezed
534                 return;
535
536         if(STAT(FROZEN, targ))
537                 return;
538
539         float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
540
541         STAT(FROZEN, targ) = frozen_type;
542         targ.revive_progress = ((frozen_type == 3) ? 1 : 0);
543         targ.health = ((frozen_type == 3) ? targ_maxhealth : 1);
544         targ.revive_speed = freeze_time;
545         if(targ.bot_attack)
546                 IL_REMOVE(g_bot_targets, targ);
547         targ.bot_attack = false;
548
549         entity ice = new(ice);
550         ice.owner = targ;
551         ice.scale = targ.scale;
552         setthink(ice, Ice_Think);
553         ice.nextthink = time;
554         ice.frame = floor(random() * 21); // ice model has 20 different looking frames
555         setmodel(ice, MDL_ICE);
556         ice.alpha = 1;
557         ice.colormod = Team_ColorRGB(targ.team);
558         ice.glowmod = ice.colormod;
559         targ.iceblock = ice;
560         targ.revival_time = 0;
561
562         Ice_Think(ice);
563
564         RemoveGrapplingHooks(targ);
565
566         FOREACH_CLIENT(IS_PLAYER(it),
567         {
568                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
569             {
570                 .entity weaponentity = weaponentities[slot];
571                 if(it.(weaponentity).hook.aiment == targ)
572                         RemoveHook(it.(weaponentity).hook);
573             }
574         });
575
576         // add waypoint
577         if(show_waypoint)
578                 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
579 }
580
581 void Unfreeze (entity targ)
582 {
583         if(!STAT(FROZEN, targ))
584                 return;
585
586         if(STAT(FROZEN, targ) && STAT(FROZEN, targ) != 3) // only reset health if target was frozen
587         {
588                 targ.health = ((IS_PLAYER(targ)) ? start_health : targ.max_health);
589                 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
590         }
591
592         STAT(FROZEN, targ) = 0;
593         targ.revive_progress = 0;
594         targ.revival_time = time;
595         if(!targ.bot_attack)
596                 IL_PUSH(g_bot_targets, targ);
597         targ.bot_attack = true;
598
599         WaypointSprite_Kill(targ.waypointsprite_attached);
600
601         FOREACH_CLIENT(IS_PLAYER(it),
602         {
603                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
604             {
605                 .entity weaponentity = weaponentities[slot];
606                 if(it.(weaponentity).hook.aiment == targ)
607                         RemoveHook(it.(weaponentity).hook);
608             }
609         });
610
611         // remove the ice block
612         if(targ.iceblock)
613                 delete(targ.iceblock);
614         targ.iceblock = NULL;
615 }
616
617 void Damage (entity targ, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
618 {
619         float mirrordamage;
620         float mirrorforce;
621         float complainteamdamage = 0;
622         entity attacker_save;
623         mirrordamage = 0;
624         mirrorforce = 0;
625
626         if (game_stopped || targ.killcount == FRAGS_SPECTATOR)
627                 return;
628
629     damage_targ = targ;
630     damage_inflictor = inflictor;
631     damage_attacker = attacker;
632         attacker_save = attacker;
633
634         if(IS_PLAYER(targ))
635         {
636                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
637             {
638                 .entity weaponentity = weaponentities[slot];
639                 if(targ.(weaponentity).hook && targ.(weaponentity).hook.aiment == attacker)
640                         RemoveHook(targ.(weaponentity).hook);
641             }
642         }
643
644         // special rule: gravity bomb does not hit team mates (other than for disconnecting the hook)
645         if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || DEATH_ISWEAPON(deathtype, WEP_TUBA))
646         {
647                 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
648                 {
649                         return;
650                 }
651         }
652
653         if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
654         {
655                 // exit the vehicle before killing (fixes a crash)
656                 if(IS_PLAYER(targ) && targ.vehicle)
657                         vehicles_exit(targ.vehicle, VHEF_RELEASE);
658
659                 // These are ALWAYS lethal
660                 // No damage modification here
661                 // Instead, prepare the victim for his death...
662                 targ.armorvalue = 0;
663                 targ.spawnshieldtime = 0;
664                 targ.health = 0.9; // this is < 1
665                 targ.flags -= targ.flags & FL_GODMODE;
666                 damage = 100000;
667         }
668         else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
669         {
670                 // no processing
671         }
672         else
673         {
674                 // nullify damage if teamplay is on
675                 if(deathtype != DEATH_TELEFRAG.m_id)
676                 if(IS_PLAYER(attacker))
677                 {
678                         if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
679                         {
680                                 damage = 0;
681                                 force = '0 0 0';
682                         }
683                         else if(SAME_TEAM(attacker, targ))
684                         {
685                                 if(autocvar_teamplay_mode == 1)
686                                         damage = 0;
687                                 else if(attacker != targ)
688                                 {
689                                         if(autocvar_teamplay_mode == 3)
690                                                 damage = 0;
691                                         else if(autocvar_teamplay_mode == 4)
692                                         {
693                                                 if(IS_PLAYER(targ) && !IS_DEAD(targ))
694                                                 {
695                                                         attacker.dmg_team = attacker.dmg_team + damage;
696                                                         complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
697                                                         if(complainteamdamage > 0)
698                                                                 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
699                                                         mirrorforce = autocvar_g_mirrordamage * vlen(force);
700                                                         damage = autocvar_g_friendlyfire * damage;
701                                                         // mirrordamage will be used LATER
702
703                                                         if(autocvar_g_mirrordamage_virtual)
704                                                         {
705                                                                 vector v  = healtharmor_applydamage(attacker.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
706                                                                 attacker.dmg_take += v.x;
707                                                                 attacker.dmg_save += v.y;
708                                                                 attacker.dmg_inflictor = inflictor;
709                                                                 mirrordamage = v.z;
710                                                                 mirrorforce = 0;
711                                                         }
712
713                                                         if(autocvar_g_friendlyfire_virtual)
714                                                         {
715                                                                 vector v = healtharmor_applydamage(targ.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, damage);
716                                                                 targ.dmg_take += v.x;
717                                                                 targ.dmg_save += v.y;
718                                                                 targ.dmg_inflictor = inflictor;
719                                                                 damage = 0;
720                                                                 if(!autocvar_g_friendlyfire_virtual_force)
721                                                                         force = '0 0 0';
722                                                         }
723                                                 }
724                                                 else
725                                                         damage = 0;
726                                         }
727                                 }
728                         }
729                 }
730
731                 if (!DEATH_ISSPECIAL(deathtype))
732                 {
733                         damage *= g_weapondamagefactor;
734                         mirrordamage *= g_weapondamagefactor;
735                         complainteamdamage *= g_weapondamagefactor;
736                         force = force * g_weaponforcefactor;
737                         mirrorforce *= g_weaponforcefactor;
738                 }
739
740                 // should this be changed at all? If so, in what way?
741                 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force);
742                 damage = M_ARGV(4, float);
743                 mirrordamage = M_ARGV(5, float);
744                 force = M_ARGV(6, vector);
745
746                 if(STAT(FROZEN, targ))
747                 if(deathtype != DEATH_HURTTRIGGER.m_id && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
748                 {
749                         if(autocvar_g_frozen_revive_falldamage > 0)
750                         if(deathtype == DEATH_FALL.m_id)
751                         if(damage >= autocvar_g_frozen_revive_falldamage)
752                         {
753                                 Unfreeze(targ);
754                                 targ.health = autocvar_g_frozen_revive_falldamage_health;
755                                 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
756                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
757                                 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
758                         }
759
760                         damage = 0;
761                         force *= autocvar_g_frozen_force;
762                 }
763
764                 if(STAT(FROZEN, targ) && deathtype == DEATH_HURTTRIGGER.m_id && !autocvar_g_frozen_damage_trigger)
765                 {
766                         Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
767
768                         entity spot = SelectSpawnPoint (targ, false);
769
770                         if(spot)
771                         {
772                                 damage = 0;
773                                 targ.deadflag = DEAD_NO;
774
775                                 targ.angles = spot.angles;
776
777                                 targ.effects = 0;
778                                 targ.effects |= EF_TELEPORT_BIT;
779
780                                 targ.angles_z = 0; // never spawn tilted even if the spot says to
781                                 targ.fixangle = true; // turn this way immediately
782                                 targ.velocity = '0 0 0';
783                                 targ.avelocity = '0 0 0';
784                                 targ.punchangle = '0 0 0';
785                                 targ.punchvector = '0 0 0';
786                                 targ.oldvelocity = targ.velocity;
787
788                                 targ.spawnorigin = spot.origin;
789                                 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
790                                 // don't reset back to last position, even if new position is stuck in solid
791                                 targ.oldorigin = targ.origin;
792                                 targ.prevorigin = targ.origin;
793
794                                 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
795                         }
796                 }
797
798                 if(!g_instagib)
799                 {
800                         // apply strength multiplier
801                         if (attacker.items & ITEM_Strength.m_itemid)
802                         {
803                                 if(targ == attacker)
804                                 {
805                                         damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
806                                         force = force * autocvar_g_balance_powerup_strength_selfforce;
807                                 }
808                                 else
809                                 {
810                                         damage = damage * autocvar_g_balance_powerup_strength_damage;
811                                         force = force * autocvar_g_balance_powerup_strength_force;
812                                 }
813                         }
814
815                         // apply invincibility multiplier
816                         if (targ.items & ITEM_Shield.m_itemid)
817                                 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
818                 }
819
820                 if (targ == attacker)
821                         damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
822
823                 // count the damage
824                 if(attacker)
825                 if(!IS_DEAD(targ))
826                 if(deathtype != DEATH_BUFF.m_id)
827                 if(targ.takedamage == DAMAGE_AIM)
828                 if(targ != attacker)
829                 {
830                         entity victim;
831                         if(IS_VEHICLE(targ) && targ.owner)
832                                 victim = targ.owner;
833                         else
834                                 victim = targ;
835
836                         if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
837                         {
838                                 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
839                                 {
840                                         if(damage > 0)
841                                         {
842                                                 if(deathtype != DEATH_FIRE.m_id)
843                                                 {
844                                                         if(PHYS_INPUT_BUTTON_CHAT(victim))
845                                                                 attacker.typehitsound += 1;
846                                                         else
847                                                                 attacker.damage_dealt += damage;
848                                                 }
849
850                                                 damage_goodhits += 1;
851                                                 damage_gooddamage += damage;
852
853                                                 if (!DEATH_ISSPECIAL(deathtype))
854                                                 {
855                                                         if(IS_PLAYER(targ)) // don't do this for vehicles
856                                                         if(IsFlying(victim))
857                                                                 yoda = 1;
858                                                 }
859                                         }
860                                 }
861                                 else
862                                 {
863                                         if(deathtype != DEATH_FIRE.m_id)
864                                         {
865                                                 attacker.typehitsound += 1;
866                                         }
867                                         if(complainteamdamage > 0)
868                                                 if(time > attacker.teamkill_complain)
869                                                 {
870                                                         attacker.teamkill_complain = time + 5;
871                                                         attacker.teamkill_soundtime = time + 0.4;
872                                                         attacker.teamkill_soundsource = targ;
873                                                 }
874                                 }
875                         }
876                 }
877         }
878
879         // apply push
880         if (targ.damageforcescale)
881         if (force)
882         if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
883         {
884                 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
885                 if(targ.move_movetype == MOVETYPE_PHYSICS)
886                 {
887                         entity farcent = new(farce);
888                         farcent.enemy = targ;
889                         farcent.movedir = farce * 10;
890                         if(targ.mass)
891                                 farcent.movedir = farcent.movedir * targ.mass;
892                         farcent.origin = hitloc;
893                         farcent.forcetype = FORCETYPE_FORCEATPOS;
894                         farcent.nextthink = time + 0.1;
895                         setthink(farcent, SUB_Remove);
896                 }
897                 else
898                 {
899                         targ.velocity = targ.velocity + farce;
900                 }
901                 UNSET_ONGROUND(targ);
902                 UpdateCSQCProjectile(targ);
903         }
904         // apply damage
905         if (damage != 0 || (targ.damageforcescale && force))
906         if (targ.event_damage)
907                 targ.event_damage (targ, inflictor, attacker, damage, deathtype, hitloc, force);
908
909         // apply mirror damage if any
910         if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
911         if(mirrordamage > 0 || mirrorforce > 0)
912         {
913                 attacker = attacker_save;
914
915                 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
916                 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, attacker.origin, force);
917         }
918 }
919
920 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)
921         // Returns total damage applies to creatures
922 {
923         entity  targ;
924         vector  force;
925         float   total_damage_to_creatures;
926         entity  next;
927         float   tfloordmg;
928         float   tfloorforce;
929
930         float stat_damagedone;
931
932         if(RadiusDamage_running)
933         {
934                 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
935                 return 0;
936         }
937
938         RadiusDamage_running = 1;
939
940         tfloordmg = autocvar_g_throughfloor_damage;
941         tfloorforce = autocvar_g_throughfloor_force;
942
943         total_damage_to_creatures = 0;
944
945         if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
946                 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
947                 {
948                         force = inflictorvelocity;
949                         if(force == '0 0 0')
950                                 force = '0 0 -1';
951                         else
952                                 force = normalize(force);
953                         if(forceintensity >= 0)
954                                 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
955                         else
956                                 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
957                 }
958
959         stat_damagedone = 0;
960
961         targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
962         while (targ)
963         {
964                 next = targ.chain;
965                 if ((targ != inflictor) || inflictorselfdamage)
966                 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
967                 if (targ.takedamage)
968                 {
969                         vector nearest;
970                         vector diff;
971                         float power;
972
973                         // LordHavoc: measure distance to nearest point on target (not origin)
974                         // (this guarentees 100% damage on a touch impact)
975                         nearest = targ.WarpZone_findradius_nearest;
976                         diff = targ.WarpZone_findradius_dist;
977                         // round up a little on the damage to ensure full damage on impacts
978                         // and turn the distance into a fraction of the radius
979                         power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
980                         //bprint(" ");
981                         //bprint(ftos(power));
982                         //if (targ == attacker)
983                         //      print(ftos(power), "\n");
984                         if (power > 0)
985                         {
986                                 float finaldmg;
987                                 if (power > 1)
988                                         power = 1;
989                                 finaldmg = coredamage * power + edgedamage * (1 - power);
990                                 if (finaldmg > 0)
991                                 {
992                                         float a;
993                                         float c;
994                                         vector hitloc;
995                                         vector myblastorigin;
996                                         vector center;
997
998                                         myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
999
1000                                         // if it's a player, use the view origin as reference
1001                                         center = CENTER_OR_VIEWOFS(targ);
1002
1003                                         force = normalize(center - myblastorigin);
1004                                         force = force * (finaldmg / coredamage) * forceintensity;
1005                                         hitloc = nearest;
1006
1007                                         if(deathtype & WEP_BLASTER.m_id)
1008                                                 force *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
1009
1010                                         if(targ != directhitentity)
1011                                         {
1012                                                 float hits;
1013                                                 float total;
1014                                                 float hitratio;
1015                                                 float mininv_f, mininv_d;
1016
1017                                                 // test line of sight to multiple positions on box,
1018                                                 // and do damage if any of them hit
1019                                                 hits = 0;
1020
1021                                                 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
1022                                                 // so for a given max stddev:
1023                                                 // n = (1 / (2 * max stddev of hitratio))^2
1024
1025                                                 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
1026                                                 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
1027
1028                                                 if(autocvar_g_throughfloor_debug)
1029                                                         LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
1030
1031
1032                                                 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
1033
1034                                                 if(autocvar_g_throughfloor_debug)
1035                                                         LOG_INFOF(" steps=%f", total);
1036
1037
1038                                                 if (IS_PLAYER(targ))
1039                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1040                                                 else
1041                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1042
1043                                                 if(autocvar_g_throughfloor_debug)
1044                                                         LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1045
1046                                                 for(c = 0; c < total; ++c)
1047                                                 {
1048                                                         //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1049                                                         WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1050                                                         if (trace_fraction == 1 || trace_ent == targ)
1051                                                         {
1052                                                                 ++hits;
1053                                                                 if (hits > 1)
1054                                                                         hitloc = hitloc + nearest;
1055                                                                 else
1056                                                                         hitloc = nearest;
1057                                                         }
1058                                                         nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1059                                                         nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1060                                                         nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1061                                                 }
1062
1063                                                 nearest = hitloc * (1 / max(1, hits));
1064                                                 hitratio = (hits / total);
1065                                                 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1066                                                 finaldmg = finaldmg * a;
1067                                                 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1068                                                 force = force * a;
1069
1070                                                 if(autocvar_g_throughfloor_debug)
1071                                                         LOG_INFOF(" D=%f F=%f\n", finaldmg, vlen(force));
1072                                         }
1073
1074                                         //if (targ == attacker)
1075                                         //{
1076                                         //      print("hits ", ftos(hits), " / ", ftos(total));
1077                                         //      print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1078                                         //      print(" (", ftos(a), ")\n");
1079                                         //}
1080                                         if(finaldmg || force)
1081                                         {
1082                                                 if(targ.iscreature)
1083                                                 {
1084                                                         total_damage_to_creatures += finaldmg;
1085
1086                                                         if(accuracy_isgooddamage(attacker, targ))
1087                                                                 stat_damagedone += finaldmg;
1088                                                 }
1089
1090                                                 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1091                                                         Damage (targ, inflictor, attacker, finaldmg, deathtype, nearest, force);
1092                                                 else
1093                                                         Damage (targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, nearest, force);
1094                                         }
1095                                 }
1096                         }
1097                 }
1098                 targ = next;
1099         }
1100
1101         RadiusDamage_running = 0;
1102
1103         if(!DEATH_ISSPECIAL(deathtype))
1104                 accuracy_add(attacker, DEATH_WEAPONOF(deathtype).m_id, 0, min(coredamage, stat_damagedone));
1105
1106         return total_damage_to_creatures;
1107 }
1108
1109 float RadiusDamage (entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, entity directhitentity)
1110 {
1111         return RadiusDamageForSource (inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, deathtype, directhitentity);
1112 }
1113
1114 float Fire_IsBurning(entity e)
1115 {
1116         return (time < e.fire_endtime);
1117 }
1118
1119 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1120 {
1121         float dps;
1122         float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1123
1124         if(IS_PLAYER(e))
1125         {
1126                 if(IS_DEAD(e))
1127                         return -1;
1128         }
1129         else
1130         {
1131                 if(!e.fire_burner)
1132                 {
1133                         // print("adding a fire burner to ", e.classname, "\n");
1134                         e.fire_burner = new(fireburner);
1135                         setthink(e.fire_burner, fireburner_think);
1136                         e.fire_burner.nextthink = time;
1137                         e.fire_burner.owner = e;
1138                 }
1139         }
1140
1141         t = max(t, 0.1);
1142         dps = d / t;
1143         if(Fire_IsBurning(e))
1144         {
1145                 mintime = e.fire_endtime - time;
1146                 maxtime = max(mintime, t);
1147
1148                 mindps = e.fire_damagepersec;
1149                 maxdps = max(mindps, dps);
1150
1151                 if(maxtime > mintime || maxdps > mindps)
1152                 {
1153                         // Constraints:
1154
1155                         // damage we have right now
1156                         mindamage = mindps * mintime;
1157
1158                         // damage we want to get
1159                         maxdamage = mindamage + d;
1160
1161                         // but we can't exceed maxtime * maxdps!
1162                         totaldamage = min(maxdamage, maxtime * maxdps);
1163
1164                         // LEMMA:
1165                         // Look at:
1166                         // totaldamage = min(mindamage + d, maxtime * maxdps)
1167                         // We see:
1168                         // totaldamage <= maxtime * maxdps
1169                         // ==> totaldamage / maxdps <= maxtime.
1170                         // We also see:
1171                         // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1172                         //                     >= min(mintime, maxtime)
1173                         // ==> totaldamage / maxdps >= mintime.
1174
1175                         /*
1176                         // how long do we damage then?
1177                         // at least as long as before
1178                         // but, never exceed maxdps
1179                         totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1180                         */
1181
1182                         // alternate:
1183                         // at most as long as maximum allowed
1184                         // but, never below mindps
1185                         totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1186
1187                         // assuming t > mintime, dps > mindps:
1188                         // we get d = t * dps = maxtime * maxdps
1189                         // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1190                         // totaldamage / maxdps = maxtime
1191                         // totaldamage / mindps > totaldamage / maxdps = maxtime
1192                         // FROM THIS:
1193                         // a) totaltime = max(mintime, maxtime) = maxtime
1194                         // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1195
1196                         // assuming t <= mintime:
1197                         // we get maxtime = mintime
1198                         // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1199                         // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1200
1201                         // assuming dps <= mindps:
1202                         // we get mindps = maxdps.
1203                         // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1204                         // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1205                         // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1206
1207                         e.fire_damagepersec = totaldamage / totaltime;
1208                         e.fire_endtime = time + totaltime;
1209                         if(totaldamage > 1.2 * mindamage)
1210                         {
1211                                 e.fire_deathtype = dt;
1212                                 if(e.fire_owner != o)
1213                                 {
1214                                         e.fire_owner = o;
1215                                         e.fire_hitsound = false;
1216                                 }
1217                         }
1218                         if(accuracy_isgooddamage(o, e))
1219                                 accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, max(0, totaldamage - mindamage));
1220                         return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1221                 }
1222                 else
1223                         return 0;
1224         }
1225         else
1226         {
1227                 e.fire_damagepersec = dps;
1228                 e.fire_endtime = time + t;
1229                 e.fire_deathtype = dt;
1230                 e.fire_owner = o;
1231                 e.fire_hitsound = false;
1232                 if(accuracy_isgooddamage(o, e))
1233                         accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, d);
1234                 return d;
1235         }
1236 }
1237
1238 void Fire_ApplyDamage(entity e)
1239 {
1240         float t, d, hi, ty;
1241         entity o;
1242
1243         if (!Fire_IsBurning(e))
1244                 return;
1245
1246         for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1247         if(IS_NOT_A_CLIENT(o))
1248                 o = e.fire_owner;
1249
1250         // water and slime stop fire
1251         if(e.waterlevel)
1252         if(e.watertype != CONTENT_LAVA)
1253                 e.fire_endtime = 0;
1254
1255         // ice stops fire
1256         if(STAT(FROZEN, e))
1257                 e.fire_endtime = 0;
1258
1259         t = min(frametime, e.fire_endtime - time);
1260         d = e.fire_damagepersec * t;
1261
1262         hi = e.fire_owner.damage_dealt;
1263         ty = e.fire_owner.typehitsound;
1264         Damage(e, e, e.fire_owner, d, e.fire_deathtype, e.origin, '0 0 0');
1265         if(e.fire_hitsound && e.fire_owner)
1266         {
1267                 e.fire_owner.damage_dealt = hi;
1268                 e.fire_owner.typehitsound = ty;
1269         }
1270         e.fire_hitsound = true;
1271
1272         if(!IS_INDEPENDENT_PLAYER(e))
1273         if(!STAT(FROZEN, e))
1274                 FOREACH_CLIENT(IS_PLAYER(it) && it != e, LAMBDA(
1275                         if(!IS_DEAD(it))
1276                         if(!IS_INDEPENDENT_PLAYER(it))
1277                         if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1278                         {
1279                                 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1280                                 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1281                                 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1282                         }
1283                 ));
1284 }
1285
1286 void Fire_ApplyEffect(entity e)
1287 {
1288         if(Fire_IsBurning(e))
1289                 e.effects |= EF_FLAME;
1290         else
1291                 e.effects &= ~EF_FLAME;
1292 }
1293
1294 void fireburner_think(entity this)
1295 {
1296         // for players, this is done in the regular loop
1297         if(wasfreed(this.owner))
1298         {
1299                 delete(this);
1300                 return;
1301         }
1302         Fire_ApplyEffect(this.owner);
1303         if(!Fire_IsBurning(this.owner))
1304         {
1305                 this.owner.fire_burner = NULL;
1306                 delete(this);
1307                 return;
1308         }
1309         Fire_ApplyDamage(this.owner);
1310         this.nextthink = time;
1311 }