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