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