]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/damage.qc
Merge branch 'master' into Mario/monsters
[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(!STAT(FROZEN, targ) && 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))
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) && !STAT(FROZEN, victim)) // same team
833                                 {
834                                         if (deathtype != DEATH_FIRE.m_id)
835                                         {
836                                                 attacker.typehitsound += 1;
837                                         }
838                                         if(complainteamdamage > 0)
839                                                 if(time > CS(attacker).teamkill_complain)
840                                                 {
841                                                         CS(attacker).teamkill_complain = time + 5;
842                                                         CS(attacker).teamkill_soundtime = time + 0.4;
843                                                         CS(attacker).teamkill_soundsource = targ;
844                                                 }
845                                 }
846                         }
847                 }
848         }
849
850         // apply push
851         if (targ.damageforcescale)
852         if (force)
853         if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
854         {
855                 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
856                 if(targ.move_movetype == MOVETYPE_PHYSICS)
857                 {
858                         entity farcent = new(farce);
859                         farcent.enemy = targ;
860                         farcent.movedir = farce * 10;
861                         if(targ.mass)
862                                 farcent.movedir = farcent.movedir * targ.mass;
863                         farcent.origin = hitloc;
864                         farcent.forcetype = FORCETYPE_FORCEATPOS;
865                         farcent.nextthink = time + 0.1;
866                         setthink(farcent, SUB_Remove);
867                 }
868                 else if(targ.move_movetype != MOVETYPE_NOCLIP)
869                 {
870                         targ.velocity = targ.velocity + farce;
871                 }
872                 UNSET_ONGROUND(targ);
873                 UpdateCSQCProjectile(targ);
874         }
875         // apply damage
876         if (damage != 0 || (targ.damageforcescale && force))
877         if (targ.event_damage)
878                 targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
879
880         // apply mirror damage if any
881         if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
882         if(mirrordamage > 0 || mirrorforce > 0)
883         {
884                 attacker = attacker_save;
885
886                 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
887                 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
888         }
889 }
890
891 float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
892                                                                 float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
893         // Returns total damage applies to creatures
894 {
895         entity  targ;
896         vector  force;
897         float   total_damage_to_creatures;
898         entity  next;
899         float   tfloordmg;
900         float   tfloorforce;
901
902         float stat_damagedone;
903
904         if(RadiusDamage_running)
905         {
906                 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
907                 return 0;
908         }
909
910         RadiusDamage_running = 1;
911
912         tfloordmg = autocvar_g_throughfloor_damage;
913         tfloorforce = autocvar_g_throughfloor_force;
914
915         total_damage_to_creatures = 0;
916
917         if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
918                 if(!(deathtype & HITTYPE_SOUND)) // do not send radial sound damage (bandwidth hog)
919                 {
920                         force = inflictorvelocity;
921                         if(force == '0 0 0')
922                                 force = '0 0 -1';
923                         else
924                                 force = normalize(force);
925                         if(forceintensity >= 0)
926                                 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
927                         else
928                                 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
929                 }
930
931         stat_damagedone = 0;
932
933         targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
934         while (targ)
935         {
936                 next = targ.chain;
937                 if ((targ != inflictor) || inflictorselfdamage)
938                 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
939                 if (targ.takedamage)
940                 {
941                         vector nearest;
942                         vector diff;
943                         float power;
944
945                         // LordHavoc: measure distance to nearest point on target (not origin)
946                         // (this guarentees 100% damage on a touch impact)
947                         nearest = targ.WarpZone_findradius_nearest;
948                         diff = targ.WarpZone_findradius_dist;
949                         // round up a little on the damage to ensure full damage on impacts
950                         // and turn the distance into a fraction of the radius
951                         power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
952                         //bprint(" ");
953                         //bprint(ftos(power));
954                         //if (targ == attacker)
955                         //      print(ftos(power), "\n");
956                         if (power > 0)
957                         {
958                                 float finaldmg;
959                                 if (power > 1)
960                                         power = 1;
961                                 finaldmg = coredamage * power + edgedamage * (1 - power);
962                                 if (finaldmg > 0)
963                                 {
964                                         float a;
965                                         float c;
966                                         vector hitloc;
967                                         vector myblastorigin;
968                                         vector center;
969
970                                         myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
971
972                                         // if it's a player, use the view origin as reference
973                                         center = CENTER_OR_VIEWOFS(targ);
974
975                                         force = normalize(center - myblastorigin);
976                                         force = force * (finaldmg / coredamage) * forceintensity;
977                                         hitloc = nearest;
978
979                                         // apply special scaling along the z axis if set
980                                         // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
981                                         if(forcezscale)
982                                                 force.z *= forcezscale;
983
984                                         if(targ != directhitentity)
985                                         {
986                                                 float hits;
987                                                 float total;
988                                                 float hitratio;
989                                                 float mininv_f, mininv_d;
990
991                                                 // test line of sight to multiple positions on box,
992                                                 // and do damage if any of them hit
993                                                 hits = 0;
994
995                                                 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
996                                                 // so for a given max stddev:
997                                                 // n = (1 / (2 * max stddev of hitratio))^2
998
999                                                 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
1000                                                 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
1001
1002                                                 if(autocvar_g_throughfloor_debug)
1003                                                         LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
1004
1005
1006                                                 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
1007
1008                                                 if(autocvar_g_throughfloor_debug)
1009                                                         LOG_INFOF(" steps=%f", total);
1010
1011
1012                                                 if (IS_PLAYER(targ))
1013                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1014                                                 else
1015                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1016
1017                                                 if(autocvar_g_throughfloor_debug)
1018                                                         LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1019
1020                                                 for(c = 0; c < total; ++c)
1021                                                 {
1022                                                         //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1023                                                         WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1024                                                         if (trace_fraction == 1 || trace_ent == targ)
1025                                                         {
1026                                                                 ++hits;
1027                                                                 if (hits > 1)
1028                                                                         hitloc = hitloc + nearest;
1029                                                                 else
1030                                                                         hitloc = nearest;
1031                                                         }
1032                                                         nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1033                                                         nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1034                                                         nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1035                                                 }
1036
1037                                                 nearest = hitloc * (1 / max(1, hits));
1038                                                 hitratio = (hits / total);
1039                                                 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1040                                                 finaldmg = finaldmg * a;
1041                                                 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1042                                                 force = force * a;
1043
1044                                                 if(autocvar_g_throughfloor_debug)
1045                                                         LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1046                                         }
1047
1048                                         //if (targ == attacker)
1049                                         //{
1050                                         //      print("hits ", ftos(hits), " / ", ftos(total));
1051                                         //      print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1052                                         //      print(" (", ftos(a), ")\n");
1053                                         //}
1054                                         if(finaldmg || force)
1055                                         {
1056                                                 if(targ.iscreature)
1057                                                 {
1058                                                         total_damage_to_creatures += finaldmg;
1059
1060                                                         if(accuracy_isgooddamage(attacker, targ))
1061                                                                 stat_damagedone += finaldmg;
1062                                                 }
1063
1064                                                 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1065                                                         Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
1066                                                 else
1067                                                         Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
1068                                         }
1069                                 }
1070                         }
1071                 }
1072                 targ = next;
1073         }
1074
1075         RadiusDamage_running = 0;
1076
1077         if(!DEATH_ISSPECIAL(deathtype))
1078                 accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
1079
1080         return total_damage_to_creatures;
1081 }
1082
1083 float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
1084 {
1085         return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, 
1086                                                                         cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
1087 }
1088
1089 bool Heal(entity targ, entity inflictor, float amount, float limit)
1090 {
1091         if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
1092                 return false;
1093
1094         bool healed = false;
1095         if(targ.event_heal)
1096                 healed = targ.event_heal(targ, inflictor, amount, limit);
1097         // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
1098         // TODO: healing fx!
1099         // TODO: armor healing?
1100         return healed;
1101 }
1102
1103 float Fire_IsBurning(entity e)
1104 {
1105         return (time < e.fire_endtime);
1106 }
1107
1108 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1109 {
1110         float dps;
1111         float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1112
1113         if(IS_PLAYER(e))
1114         {
1115                 if(IS_DEAD(e))
1116                         return -1;
1117         }
1118         else
1119         {
1120                 if(!e.fire_burner)
1121                 {
1122                         // print("adding a fire burner to ", e.classname, "\n");
1123                         e.fire_burner = new(fireburner);
1124                         setthink(e.fire_burner, fireburner_think);
1125                         e.fire_burner.nextthink = time;
1126                         e.fire_burner.owner = e;
1127                 }
1128         }
1129
1130         t = max(t, 0.1);
1131         dps = d / t;
1132         if(Fire_IsBurning(e))
1133         {
1134                 mintime = e.fire_endtime - time;
1135                 maxtime = max(mintime, t);
1136
1137                 mindps = e.fire_damagepersec;
1138                 maxdps = max(mindps, dps);
1139
1140                 if(maxtime > mintime || maxdps > mindps)
1141                 {
1142                         // Constraints:
1143
1144                         // damage we have right now
1145                         mindamage = mindps * mintime;
1146
1147                         // damage we want to get
1148                         maxdamage = mindamage + d;
1149
1150                         // but we can't exceed maxtime * maxdps!
1151                         totaldamage = min(maxdamage, maxtime * maxdps);
1152
1153                         // LEMMA:
1154                         // Look at:
1155                         // totaldamage = min(mindamage + d, maxtime * maxdps)
1156                         // We see:
1157                         // totaldamage <= maxtime * maxdps
1158                         // ==> totaldamage / maxdps <= maxtime.
1159                         // We also see:
1160                         // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1161                         //                     >= min(mintime, maxtime)
1162                         // ==> totaldamage / maxdps >= mintime.
1163
1164                         /*
1165                         // how long do we damage then?
1166                         // at least as long as before
1167                         // but, never exceed maxdps
1168                         totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1169                         */
1170
1171                         // alternate:
1172                         // at most as long as maximum allowed
1173                         // but, never below mindps
1174                         totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1175
1176                         // assuming t > mintime, dps > mindps:
1177                         // we get d = t * dps = maxtime * maxdps
1178                         // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1179                         // totaldamage / maxdps = maxtime
1180                         // totaldamage / mindps > totaldamage / maxdps = maxtime
1181                         // FROM THIS:
1182                         // a) totaltime = max(mintime, maxtime) = maxtime
1183                         // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1184
1185                         // assuming t <= mintime:
1186                         // we get maxtime = mintime
1187                         // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1188                         // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1189
1190                         // assuming dps <= mindps:
1191                         // we get mindps = maxdps.
1192                         // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1193                         // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1194                         // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1195
1196                         e.fire_damagepersec = totaldamage / totaltime;
1197                         e.fire_endtime = time + totaltime;
1198                         if(totaldamage > 1.2 * mindamage)
1199                         {
1200                                 e.fire_deathtype = dt;
1201                                 if(e.fire_owner != o)
1202                                 {
1203                                         e.fire_owner = o;
1204                                         e.fire_hitsound = false;
1205                                 }
1206                         }
1207                         if(accuracy_isgooddamage(o, e))
1208                                 accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
1209                         return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1210                 }
1211                 else
1212                         return 0;
1213         }
1214         else
1215         {
1216                 e.fire_damagepersec = dps;
1217                 e.fire_endtime = time + t;
1218                 e.fire_deathtype = dt;
1219                 e.fire_owner = o;
1220                 e.fire_hitsound = false;
1221                 if(accuracy_isgooddamage(o, e))
1222                         accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
1223                 return d;
1224         }
1225 }
1226
1227 void Fire_ApplyDamage(entity e)
1228 {
1229         float t, d, hi, ty;
1230         entity o;
1231
1232         if (!Fire_IsBurning(e))
1233                 return;
1234
1235         for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1236         if(IS_NOT_A_CLIENT(o))
1237                 o = e.fire_owner;
1238
1239         // water and slime stop fire
1240         if(e.waterlevel)
1241         if(e.watertype != CONTENT_LAVA)
1242                 e.fire_endtime = 0;
1243
1244         // ice stops fire
1245         if(STAT(FROZEN, e))
1246                 e.fire_endtime = 0;
1247
1248         t = min(frametime, e.fire_endtime - time);
1249         d = e.fire_damagepersec * t;
1250
1251         hi = e.fire_owner.damage_dealt;
1252         ty = e.fire_owner.typehitsound;
1253         Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
1254         if(e.fire_hitsound && e.fire_owner)
1255         {
1256                 e.fire_owner.damage_dealt = hi;
1257                 e.fire_owner.typehitsound = ty;
1258         }
1259         e.fire_hitsound = true;
1260
1261         if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
1262         {
1263                 IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
1264                 {
1265                         if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
1266                         if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1267                         {
1268                                 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1269                                 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1270                                 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1271                         }
1272                 });
1273         }
1274 }
1275
1276 void Fire_ApplyEffect(entity e)
1277 {
1278         if(Fire_IsBurning(e))
1279                 e.effects |= EF_FLAME;
1280         else
1281                 e.effects &= ~EF_FLAME;
1282 }
1283
1284 void fireburner_think(entity this)
1285 {
1286         // for players, this is done in the regular loop
1287         if(wasfreed(this.owner))
1288         {
1289                 delete(this);
1290                 return;
1291         }
1292         Fire_ApplyEffect(this.owner);
1293         if(!Fire_IsBurning(this.owner))
1294         {
1295                 this.owner.fire_burner = NULL;
1296                 delete(this);
1297                 return;
1298         }
1299         Fire_ApplyDamage(this.owner);
1300         this.nextthink = time;
1301 }