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