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