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