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