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