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