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