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