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