]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/damage.qc
Fix FL_WEAPON flag overlapping FL_JUMPRELEASED. This unintentional change was introdu...
[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                         // 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 power = 1;
935                                 if (rad > 0)
936                                         power -= (dist / rad);
937                                 // at this point power can't be < 0 or > 1
938                                 float finaldmg = coredamage * power + edgedamage * (1 - power);
939                                 if (finaldmg > 0)
940                                 {
941                                         float a;
942                                         float c;
943                                         vector hitloc;
944                                         vector myblastorigin;
945                                         vector center;
946
947                                         myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
948
949                                         // if it's a player, use the view origin as reference
950                                         center = CENTER_OR_VIEWOFS(targ);
951
952                                         force = normalize(center - myblastorigin);
953                                         force = force * (finaldmg / coredamage) * forceintensity;
954                                         hitloc = nearest;
955
956                                         // apply special scaling along the z axis if set
957                                         // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
958                                         if(forcezscale)
959                                                 force.z *= forcezscale;
960
961                                         if(targ != directhitentity)
962                                         {
963                                                 float hits;
964                                                 float total;
965                                                 float hitratio;
966                                                 float mininv_f, mininv_d;
967
968                                                 // test line of sight to multiple positions on box,
969                                                 // and do damage if any of them hit
970                                                 hits = 0;
971
972                                                 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
973                                                 // so for a given max stddev:
974                                                 // n = (1 / (2 * max stddev of hitratio))^2
975
976                                                 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
977                                                 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
978
979                                                 if(autocvar_g_throughfloor_debug)
980                                                         LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
981
982
983                                                 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
984
985                                                 if(autocvar_g_throughfloor_debug)
986                                                         LOG_INFOF(" steps=%f", total);
987
988
989                                                 if (IS_PLAYER(targ))
990                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
991                                                 else
992                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
993
994                                                 if(autocvar_g_throughfloor_debug)
995                                                         LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
996
997                                                 for(c = 0; c < total; ++c)
998                                                 {
999                                                         //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1000                                                         WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1001                                                         if (trace_fraction == 1 || trace_ent == targ)
1002                                                         {
1003                                                                 ++hits;
1004                                                                 if (hits > 1)
1005                                                                         hitloc = hitloc + nearest;
1006                                                                 else
1007                                                                         hitloc = nearest;
1008                                                         }
1009                                                         nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1010                                                         nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1011                                                         nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1012                                                 }
1013
1014                                                 nearest = hitloc * (1 / max(1, hits));
1015                                                 hitratio = (hits / total);
1016                                                 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1017                                                 finaldmg = finaldmg * a;
1018                                                 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1019                                                 force = force * a;
1020
1021                                                 if(autocvar_g_throughfloor_debug)
1022                                                         LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1023                                         }
1024
1025                                         //if (targ == attacker)
1026                                         //{
1027                                         //      print("hits ", ftos(hits), " / ", ftos(total));
1028                                         //      print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1029                                         //      print(" (", ftos(a), ")\n");
1030                                         //}
1031                                         if(finaldmg || force)
1032                                         {
1033                                                 if(targ.iscreature)
1034                                                 {
1035                                                         total_damage_to_creatures += finaldmg;
1036
1037                                                         if(accuracy_isgooddamage(attacker, targ))
1038                                                                 stat_damagedone += finaldmg;
1039                                                 }
1040
1041                                                 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1042                                                         Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1043                                                 else
1044                                                         Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1045                                         }
1046                                 }
1047                         }
1048                 }
1049                 targ = next;
1050         }
1051
1052         RadiusDamage_running = 0;
1053
1054         if(!DEATH_ISSPECIAL(deathtype))
1055                 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1056
1057         return total_damage_to_creatures;
1058 }
1059
1060 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1061 {
1062         return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, 
1063                                                                         cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1064 }
1065
1066 bool Heal(entity targ, entity inflictor, float amount, float limit)
1067 {
1068         if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1069                 return false;
1070
1071         bool healed = false;
1072         if(targ.event_heal)
1073                 healed = targ.event_heal(targ, inflictor, amount, limit);
1074         // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1075         // TODO: healing fx!
1076         // TODO: armor healing?
1077         return healed;
1078 }
1079
1080 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1081 {
1082         float dps;
1083         float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1084
1085         if(IS_PLAYER(e))
1086         {
1087                 if(IS_DEAD(e))
1088                         return -1;
1089         }
1090
1091         t = max(t, 0.1);
1092         dps = d / t;
1093         if(StatusEffects_active(STATUSEFFECT_Burning, e))
1094         {
1095                 float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1096
1097                 mintime = fireendtime - time;
1098                 maxtime = max(mintime, t);
1099
1100                 mindps = e.fire_damagepersec;
1101                 maxdps = max(mindps, dps);
1102
1103                 if(maxtime > mintime || maxdps > mindps)
1104                 {
1105                         // Constraints:
1106
1107                         // damage we have right now
1108                         mindamage = mindps * mintime;
1109
1110                         // damage we want to get
1111                         maxdamage = mindamage + d;
1112
1113                         // but we can't exceed maxtime * maxdps!
1114                         totaldamage = min(maxdamage, maxtime * maxdps);
1115
1116                         // LEMMA:
1117                         // Look at:
1118                         // totaldamage = min(mindamage + d, maxtime * maxdps)
1119                         // We see:
1120                         // totaldamage <= maxtime * maxdps
1121                         // ==> totaldamage / maxdps <= maxtime.
1122                         // We also see:
1123                         // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1124                         //                     >= min(mintime, maxtime)
1125                         // ==> totaldamage / maxdps >= mintime.
1126
1127                         /*
1128                         // how long do we damage then?
1129                         // at least as long as before
1130                         // but, never exceed maxdps
1131                         totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1132                         */
1133
1134                         // alternate:
1135                         // at most as long as maximum allowed
1136                         // but, never below mindps
1137                         totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1138
1139                         // assuming t > mintime, dps > mindps:
1140                         // we get d = t * dps = maxtime * maxdps
1141                         // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1142                         // totaldamage / maxdps = maxtime
1143                         // totaldamage / mindps > totaldamage / maxdps = maxtime
1144                         // FROM THIS:
1145                         // a) totaltime = max(mintime, maxtime) = maxtime
1146                         // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1147
1148                         // assuming t <= mintime:
1149                         // we get maxtime = mintime
1150                         // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1151                         // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1152
1153                         // assuming dps <= mindps:
1154                         // we get mindps = maxdps.
1155                         // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1156                         // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1157                         // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1158
1159                         e.fire_damagepersec = totaldamage / totaltime;
1160                         StatusEffects_apply(STATUSEFFECT_Burning, e, time + totaltime, 0);
1161                         if(totaldamage > 1.2 * mindamage)
1162                         {
1163                                 e.fire_deathtype = dt;
1164                                 if(e.fire_owner != o)
1165                                 {
1166                                         e.fire_owner = o;
1167                                         e.fire_hitsound = false;
1168                                 }
1169                         }
1170                         if(accuracy_isgooddamage(o, e))
1171                                 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1172                         return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1173                 }
1174                 else
1175                         return 0;
1176         }
1177         else
1178         {
1179                 e.fire_damagepersec = dps;
1180                 StatusEffects_apply(STATUSEFFECT_Burning, e, time + t, 0);
1181                 e.fire_deathtype = dt;
1182                 e.fire_owner = o;
1183                 e.fire_hitsound = false;
1184                 if(accuracy_isgooddamage(o, e))
1185                         accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1186                 return d;
1187         }
1188 }
1189
1190 void Fire_ApplyDamage(entity e)
1191 {
1192         float t, d, hi, ty;
1193         entity o;
1194
1195         for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1196         if(IS_NOT_A_CLIENT(o))
1197                 o = e.fire_owner;
1198
1199         float fireendtime = StatusEffects_gettime(STATUSEFFECT_Burning, e);
1200         t = min(frametime, fireendtime - time);
1201         d = e.fire_damagepersec * t;
1202
1203         hi = e.fire_owner.hitsound_damage_dealt;
1204         ty = e.fire_owner.typehitsound;
1205         Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1206         if(e.fire_hitsound && e.fire_owner)
1207         {
1208                 e.fire_owner.hitsound_damage_dealt = hi;
1209                 e.fire_owner.typehitsound = ty;
1210         }
1211         e.fire_hitsound = true;
1212
1213         if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1214         {
1215                 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1216                 {
1217                         if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1218                         if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1219                         {
1220                                 t = autocvar_g_balance_firetransfer_time * (fireendtime - time);
1221                                 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1222                                 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1223                         }
1224                 });
1225         }
1226 }