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