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