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