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