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