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