]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/damage.qc
Resolve "Setting a weapon's primary damage to 1 causes extremely overpowered push"
[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 their 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                         // and avoid dealing damage or force to things owned by other independent players
633                         if((IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ))) ||
634                                 (targ.realowner && IS_INDEPENDENT_PLAYER(targ.realowner) && attacker != targ.realowner))
635                         {
636                                 damage = 0;
637                                 force = '0 0 0';
638                         }
639                         else if(!STAT(FROZEN, targ) && SAME_TEAM(attacker, targ))
640                         {
641                                 if(autocvar_teamplay_mode == 1)
642                                         damage = 0;
643                                 else if(attacker != targ)
644                                 {
645                                         if(autocvar_teamplay_mode == 2)
646                                         {
647                                                 if(IS_PLAYER(targ) && !IS_DEAD(targ))
648                                                 {
649                                                         attacker.dmg_team = attacker.dmg_team + damage;
650                                                         complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
651                                                 }
652                                         }
653                                         else if(autocvar_teamplay_mode == 3)
654                                                 damage = 0;
655                                         else if(autocvar_teamplay_mode == 4)
656                                         {
657                                                 if(IS_PLAYER(targ) && !IS_DEAD(targ))
658                                                 {
659                                                         attacker.dmg_team = attacker.dmg_team + damage;
660                                                         complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
661                                                         if(complainteamdamage > 0)
662                                                                 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
663                                                         mirrorforce = autocvar_g_mirrordamage * vlen(force);
664                                                         damage = autocvar_g_friendlyfire * damage;
665                                                         // mirrordamage will be used LATER
666
667                                                         if(autocvar_g_mirrordamage_virtual)
668                                                         {
669                                                                 vector v  = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
670                                                                 attacker.dmg_take += v.x;
671                                                                 attacker.dmg_save += v.y;
672                                                                 attacker.dmg_inflictor = inflictor;
673                                                                 mirrordamage = v.z;
674                                                                 mirrorforce = 0;
675                                                         }
676
677                                                         if(autocvar_g_friendlyfire_virtual)
678                                                         {
679                                                                 vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
680                                                                 targ.dmg_take += v.x;
681                                                                 targ.dmg_save += v.y;
682                                                                 targ.dmg_inflictor = inflictor;
683                                                                 damage = 0;
684                                                                 if(!autocvar_g_friendlyfire_virtual_force)
685                                                                         force = '0 0 0';
686                                                         }
687                                                 }
688                                                 else if(!targ.canteamdamage)
689                                                         damage = 0;
690                                         }
691                                 }
692                         }
693                 }
694
695                 if (!DEATH_ISSPECIAL(deathtype))
696                 {
697                         damage *= autocvar_g_weapondamagefactor;
698                         mirrordamage *= autocvar_g_weapondamagefactor;
699                         complainteamdamage *= autocvar_g_weapondamagefactor;
700                         force = force * autocvar_g_weaponforcefactor;
701                         mirrorforce *= autocvar_g_weaponforcefactor;
702                 }
703
704                 // should this be changed at all? If so, in what way?
705                 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
706                 damage = M_ARGV(4, float);
707                 mirrordamage = M_ARGV(5, float);
708                 force = M_ARGV(6, vector);
709
710                 if(IS_PLAYER(targ) && damage > 0 && attacker)
711                 {
712                         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
713                         {
714                                 .entity went = weaponentities[slot];
715                                 if(targ.(went).hook && targ.(went).hook.aiment == attacker)
716                                         RemoveHook(targ.(went).hook);
717                         }
718                 }
719
720                 if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
721                         && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
722                 {
723                         if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
724                         {
725                                 Unfreeze(targ, false);
726                                 SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
727                                 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
728                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
729                                 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
730                         }
731
732                         damage = 0;
733                         force *= autocvar_g_frozen_force;
734                 }
735
736                 if(IS_PLAYER(targ) && STAT(FROZEN, targ)
737                         && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
738                 {
739                         Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
740
741                         entity spot = SelectSpawnPoint(targ, false);
742                         if(spot)
743                         {
744                                 damage = 0;
745                                 targ.deadflag = DEAD_NO;
746
747                                 targ.angles = spot.angles;
748
749                                 targ.effects = 0;
750                                 targ.effects |= EF_TELEPORT_BIT;
751
752                                 targ.angles_z = 0; // never spawn tilted even if the spot says to
753                                 targ.fixangle = true; // turn this way immediately
754                                 targ.velocity = '0 0 0';
755                                 targ.avelocity = '0 0 0';
756                                 targ.punchangle = '0 0 0';
757                                 targ.punchvector = '0 0 0';
758                                 targ.oldvelocity = targ.velocity;
759
760                                 targ.spawnorigin = spot.origin;
761                                 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
762                                 // don't reset back to last position, even if new position is stuck in solid
763                                 targ.oldorigin = targ.origin;
764
765                                 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
766                         }
767                 }
768
769                 if (targ == attacker)
770                         damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
771
772                 // count the damage
773                 if(attacker)
774                 if(!IS_DEAD(targ))
775                 if(deathtype != DEATH_BUFF.m_id)
776                 if(targ.takedamage == DAMAGE_AIM)
777                 if(targ != attacker)
778                 {
779                         entity victim;
780                         if(IS_VEHICLE(targ) && targ.owner)
781                                 victim = targ.owner;
782                         else
783                                 victim = targ;
784
785                         if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
786                         {
787                                 if (DIFF_TEAM(victim, attacker))
788                                 {
789                                         if(damage > 0)
790                                         {
791                                                 if(deathtype != DEATH_FIRE.m_id)
792                                                 {
793                                                         if(PHYS_INPUT_BUTTON_CHAT(victim))
794                                                                 attacker.typehitsound += 1;
795                                                         else
796                                                                 attacker.hitsound_damage_dealt += damage;
797                                                 }
798
799                                                 impressive_hits += 1;
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) && !STAT(FROZEN, victim)) // same team
810                                 {
811                                         if (deathtype != DEATH_FIRE.m_id)
812                                         {
813                                                 attacker.typehitsound += 1;
814                                         }
815                                         if(complainteamdamage > 0)
816                                                 if(time > CS(attacker).teamkill_complain)
817                                                 {
818                                                         CS(attacker).teamkill_complain = time + 5;
819                                                         CS(attacker).teamkill_soundtime = time + 0.4;
820                                                         CS(attacker).teamkill_soundsource = targ;
821                                                 }
822                                 }
823                         }
824                 }
825         }
826
827         // apply push
828         if (targ.damageforcescale)
829         if (force)
830         if (!IS_PLAYER(targ) || !StatusEffects_active(STATUSEFFECT_SpawnShield, targ) || targ == attacker)
831         {
832                 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
833                 if(targ.move_movetype == MOVETYPE_PHYSICS)
834                 {
835                         entity farcent = new(farce);
836                         farcent.enemy = targ;
837                         farcent.movedir = farce * 10;
838                         if(targ.mass)
839                                 farcent.movedir = farcent.movedir * targ.mass;
840                         farcent.origin = hitloc;
841                         farcent.forcetype = FORCETYPE_FORCEATPOS;
842                         farcent.nextthink = time + 0.1;
843                         setthink(farcent, SUB_Remove);
844                 }
845                 else if(targ.move_movetype != MOVETYPE_NOCLIP)
846                 {
847                         targ.velocity = targ.velocity + farce;
848                 }
849                 UNSET_ONGROUND(targ);
850                 UpdateCSQCProjectile(targ);
851         }
852         // apply damage
853         if (damage != 0 || (targ.damageforcescale && force))
854         if (targ.event_damage)
855                 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
856
857         // apply mirror damage if any
858         if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
859         if(mirrordamage > 0 || mirrorforce > 0)
860         {
861                 attacker = attacker_save;
862
863                 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
864                 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
865         }
866 }
867
868 // Returns total damage applies to creatures
869 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
870                                                                 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
871 {
872         entity  targ;
873         vector  force;
874         float   total_damage_to_creatures;
875         entity  next;
876         float   tfloordmg;
877         float   tfloorforce;
878
879         float stat_damagedone;
880
881         if(RadiusDamage_running)
882         {
883                 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
884                 return 0;
885         }
886
887         if (rad < 0) rad = 0;
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(!(deathtype & HITTYPE_SOUND)) // do not send radial sound 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                         // measure distance from nearest point on target (not origin)
921                         // to nearest point on inflictor (not origin)
922                         vector nearest = targ.WarpZone_findradius_nearest;
923                         vector inflictornearest = NearestPointOnBoundingBox(
924                                 inflictororigin - (inflictor.maxs - inflictor.mins) * 0.5,
925                                 inflictororigin + (inflictor.maxs - inflictor.mins) * 0.5,
926                                 nearest);
927                         vector diff = inflictornearest - nearest;
928
929                         // round up a little on the damage to ensure full damage on impacts
930                         // and turn the distance into a fraction of the radius
931                         float dist = max(0, vlen(diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS));
932                         if (dist <= rad)
933                         {
934                                 float f = (rad > 0) ? 1 - (dist / rad) : 1;
935                                 // at this point f can't be < 0 or > 1
936                                 float finaldmg = coredamage * f + edgedamage * (1 - f);
937                                 if (finaldmg > 0)
938                                 {
939                                         float a;
940                                         float c;
941                                         vector hitloc;
942                                         vector myblastorigin;
943                                         vector center;
944
945                                         myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
946
947                                         // if it's a player, use the view origin as reference
948                                         center = CENTER_OR_VIEWOFS(targ);
949
950                                         force = normalize(center - myblastorigin);
951                                         force = force * (finaldmg / max(coredamage, edgedamage)) * forceintensity;
952                                         hitloc = nearest;
953
954                                         // apply special scaling along the z axis if set
955                                         // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
956                                         if(forcezscale)
957                                                 force.z *= forcezscale;
958
959                                         if(targ != directhitentity)
960                                         {
961                                                 float hits;
962                                                 float total;
963                                                 float hitratio;
964                                                 float mininv_f, mininv_d;
965
966                                                 // test line of sight to multiple positions on box,
967                                                 // and do damage if any of them hit
968                                                 hits = 0;
969
970                                                 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
971                                                 // so for a given max stddev:
972                                                 // n = (1 / (2 * max stddev of hitratio))^2
973
974                                                 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
975                                                 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
976
977                                                 if(autocvar_g_throughfloor_debug)
978                                                         LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
979
980
981                                                 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
982
983                                                 if(autocvar_g_throughfloor_debug)
984                                                         LOG_INFOF(" steps=%f", total);
985
986
987                                                 if (IS_PLAYER(targ))
988                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
989                                                 else
990                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
991
992                                                 if(autocvar_g_throughfloor_debug)
993                                                         LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
994
995                                                 for(c = 0; c < total; ++c)
996                                                 {
997                                                         //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
998                                                         WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
999                                                         if (trace_fraction == 1 || trace_ent == targ)
1000                                                         {
1001                                                                 ++hits;
1002                                                                 if (hits > 1)
1003                                                                         hitloc = hitloc + nearest;
1004                                                                 else
1005                                                                         hitloc = nearest;
1006                                                         }
1007                                                         nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1008                                                         nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1009                                                         nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1010                                                 }
1011
1012                                                 nearest = hitloc * (1 / max(1, hits));
1013                                                 hitratio = (hits / total);
1014                                                 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1015                                                 finaldmg = finaldmg * a;
1016                                                 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1017                                                 force = force * a;
1018
1019                                                 if(autocvar_g_throughfloor_debug)
1020                                                         LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1021
1022                                                 /*if (targ == attacker)
1023                                                 {
1024                                                         print("hits ", ftos(hits), " / ", ftos(total));
1025                                                         print(" finaldmg ", ftos(finaldmg), " force ", ftos(vlen(force)));
1026                                                         print(" (", vtos(force), ") (", ftos(a), ")\n");
1027                                                 }*/
1028                                         }
1029
1030                                         if(finaldmg || force)
1031                                         {
1032                                                 if(targ.iscreature)
1033                                                 {
1034                                                         total_damage_to_creatures += finaldmg;
1035
1036                                                         if(accuracy_isgooddamage(attacker, targ))
1037                                                                 stat_damagedone += finaldmg;
1038                                                 }
1039
1040                                                 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1041                                                         Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1042                                                 else
1043                                                         Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1044                                         }
1045                                 }
1046                         }
1047                 }
1048                 targ = next;
1049         }
1050
1051         RadiusDamage_running = 0;
1052
1053         if(!DEATH_ISSPECIAL(deathtype))
1054                 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(max(coredamage, edgedamage), stat_damagedone));
1055
1056         return total_damage_to_creatures;
1057 }
1058
1059 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1060 {
1061         return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad,
1062                                                                         cantbe, mustbe, false, forceintensity, 1, 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_AddDamage(entity e, entity o, float d, float t, float dt)
1080 {
1081         float dps;
1082         float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1083
1084         if(IS_PLAYER(e))
1085         {
1086                 if(IS_DEAD(e))
1087                         return -1;
1088         }
1089
1090         t = max(t, 0.1);
1091         dps = d / t;
1092         if(StatusEffects_active(STATUSEFFECT_Burning, e))
1093         {
1094                 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1095
1096                 mintime = fireendtime - time;
1097                 maxtime = max(mintime, t);
1098
1099                 mindps = e.fire_damagepersec;
1100                 maxdps = max(mindps, dps);
1101
1102                 if(maxtime > mintime || maxdps > mindps)
1103                 {
1104                         // Constraints:
1105
1106                         // damage we have right now
1107                         mindamage = mindps * mintime;
1108
1109                         // damage we want to get
1110                         maxdamage = mindamage + d;
1111
1112                         // but we can't exceed maxtime * maxdps!
1113                         totaldamage = min(maxdamage, maxtime * maxdps);
1114
1115                         // LEMMA:
1116                         // Look at:
1117                         // totaldamage = min(mindamage + d, maxtime * maxdps)
1118                         // We see:
1119                         // totaldamage <= maxtime * maxdps
1120                         // ==> totaldamage / maxdps <= maxtime.
1121                         // We also see:
1122                         // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1123                         //                     >= min(mintime, maxtime)
1124                         // ==> totaldamage / maxdps >= mintime.
1125
1126                         /*
1127                         // how long do we damage then?
1128                         // at least as long as before
1129                         // but, never exceed maxdps
1130                         totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1131                         */
1132
1133                         // alternate:
1134                         // at most as long as maximum allowed
1135                         // but, never below mindps
1136                         totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1137
1138                         // assuming t > mintime, dps > mindps:
1139                         // we get d = t * dps = maxtime * maxdps
1140                         // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1141                         // totaldamage / maxdps = maxtime
1142                         // totaldamage / mindps > totaldamage / maxdps = maxtime
1143                         // FROM THIS:
1144                         // a) totaltime = max(mintime, maxtime) = maxtime
1145                         // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1146
1147                         // assuming t <= mintime:
1148                         // we get maxtime = mintime
1149                         // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1150                         // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1151
1152                         // assuming dps <= mindps:
1153                         // we get mindps = maxdps.
1154                         // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1155                         // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1156                         // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1157
1158                         e.fire_damagepersec = totaldamage / totaltime;
1159                         StatusEffects_apply(STATUSEFFECT_Burning, e, time + totaltime, 0);
1160                         if(totaldamage > 1.2 * mindamage)
1161                         {
1162                                 e.fire_deathtype = dt;
1163                                 if(e.fire_owner != o)
1164                                 {
1165                                         e.fire_owner = o;
1166                                         e.fire_hitsound = false;
1167                                 }
1168                         }
1169                         if(accuracy_isgooddamage(o, e))
1170                                 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1171                         return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1172                 }
1173                 else
1174                         return 0;
1175         }
1176         else
1177         {
1178                 e.fire_damagepersec = dps;
1179                 StatusEffects_apply(STATUSEFFECT_Burning, e, time + t, 0);
1180                 e.fire_deathtype = dt;
1181                 e.fire_owner = o;
1182                 e.fire_hitsound = false;
1183                 if(accuracy_isgooddamage(o, e))
1184                         accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1185                 return d;
1186         }
1187 }
1188
1189 void Fire_ApplyDamage(entity e)
1190 {
1191         float t, d, hi, ty;
1192         entity o;
1193
1194         for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1195         if(IS_NOT_A_CLIENT(o))
1196                 o = e.fire_owner;
1197
1198         float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1199         t = min(frametime, fireendtime - time);
1200         d = e.fire_damagepersec * t;
1201
1202         hi = e.fire_owner.hitsound_damage_dealt;
1203         ty = e.fire_owner.typehitsound;
1204         Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1205         if(e.fire_hitsound && e.fire_owner)
1206         {
1207                 e.fire_owner.hitsound_damage_dealt = hi;
1208                 e.fire_owner.typehitsound = ty;
1209         }
1210         e.fire_hitsound = true;
1211
1212         if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1213         {
1214                 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1215                 {
1216                         if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1217                         if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1218                         {
1219                                 t = autocvar_g_balance_firetransfer_time * (fireendtime - time);
1220                                 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1221                                 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1222                         }
1223                 });
1224         }
1225 }