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