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