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