]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/g_damage.qc
Merge branch 'martin-t/shield' into 'master'
[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 if(!targ.canteamdamage)
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                         {
804                                 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
805                                 if (targ != attacker)
806                                 {
807                                         force = force * autocvar_g_balance_powerup_invincible_takeforce;
808                                 }
809                         }
810                 }
811
812                 if (targ == attacker)
813                         damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
814
815                 // count the damage
816                 if(attacker)
817                 if(!IS_DEAD(targ))
818                 if(deathtype != DEATH_BUFF.m_id)
819                 if(targ.takedamage == DAMAGE_AIM)
820                 if(targ != attacker)
821                 {
822                         entity victim;
823                         if(IS_VEHICLE(targ) && targ.owner)
824                                 victim = targ.owner;
825                         else
826                                 victim = targ;
827
828                         if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
829                         {
830                                 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
831                                 {
832                                         if(damage > 0)
833                                         {
834                                                 if(deathtype != DEATH_FIRE.m_id)
835                                                 {
836                                                         if(PHYS_INPUT_BUTTON_CHAT(victim))
837                                                                 attacker.typehitsound += 1;
838                                                         else
839                                                                 attacker.damage_dealt += damage;
840                                                 }
841
842                                                 damage_goodhits += 1;
843                                                 damage_gooddamage += damage;
844
845                                                 if (!DEATH_ISSPECIAL(deathtype))
846                                                 {
847                                                         if(IS_PLAYER(targ)) // don't do this for vehicles
848                                                         if(IsFlying(victim))
849                                                                 yoda = 1;
850                                                 }
851                                         }
852                                 }
853                                 else if(IS_PLAYER(attacker))
854                                 {
855                                         if(deathtype != DEATH_FIRE.m_id)
856                                         {
857                                                 attacker.typehitsound += 1;
858                                         }
859                                         if(complainteamdamage > 0)
860                                                 if(time > CS(attacker).teamkill_complain)
861                                                 {
862                                                         CS(attacker).teamkill_complain = time + 5;
863                                                         CS(attacker).teamkill_soundtime = time + 0.4;
864                                                         CS(attacker).teamkill_soundsource = targ;
865                                                 }
866                                 }
867                         }
868                 }
869         }
870
871         // apply push
872         if (targ.damageforcescale)
873         if (force)
874         if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
875         {
876                 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
877                 if(targ.move_movetype == MOVETYPE_PHYSICS)
878                 {
879                         entity farcent = new(farce);
880                         farcent.enemy = targ;
881                         farcent.movedir = farce * 10;
882                         if(targ.mass)
883                                 farcent.movedir = farcent.movedir * targ.mass;
884                         farcent.origin = hitloc;
885                         farcent.forcetype = FORCETYPE_FORCEATPOS;
886                         farcent.nextthink = time + 0.1;
887                         setthink(farcent, SUB_Remove);
888                 }
889                 else
890                 {
891                         targ.velocity = targ.velocity + farce;
892                 }
893                 UNSET_ONGROUND(targ);
894                 UpdateCSQCProjectile(targ);
895         }
896         // apply damage
897         if (damage != 0 || (targ.damageforcescale && force))
898         if (targ.event_damage)
899                 targ.event_damage (targ, inflictor, attacker, damage, deathtype, hitloc, force);
900
901         // apply mirror damage if any
902         if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
903         if(mirrordamage > 0 || mirrorforce > 0)
904         {
905                 attacker = attacker_save;
906
907                 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
908                 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, attacker.origin, force);
909         }
910 }
911
912 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)
913         // Returns total damage applies to creatures
914 {
915         entity  targ;
916         vector  force;
917         float   total_damage_to_creatures;
918         entity  next;
919         float   tfloordmg;
920         float   tfloorforce;
921
922         float stat_damagedone;
923
924         if(RadiusDamage_running)
925         {
926                 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
927                 return 0;
928         }
929
930         RadiusDamage_running = 1;
931
932         tfloordmg = autocvar_g_throughfloor_damage;
933         tfloorforce = autocvar_g_throughfloor_force;
934
935         total_damage_to_creatures = 0;
936
937         if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
938                 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
939                 {
940                         force = inflictorvelocity;
941                         if(force == '0 0 0')
942                                 force = '0 0 -1';
943                         else
944                                 force = normalize(force);
945                         if(forceintensity >= 0)
946                                 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
947                         else
948                                 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
949                 }
950
951         stat_damagedone = 0;
952
953         targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
954         while (targ)
955         {
956                 next = targ.chain;
957                 if ((targ != inflictor) || inflictorselfdamage)
958                 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
959                 if (targ.takedamage)
960                 {
961                         vector nearest;
962                         vector diff;
963                         float power;
964
965                         // LordHavoc: measure distance to nearest point on target (not origin)
966                         // (this guarentees 100% damage on a touch impact)
967                         nearest = targ.WarpZone_findradius_nearest;
968                         diff = targ.WarpZone_findradius_dist;
969                         // round up a little on the damage to ensure full damage on impacts
970                         // and turn the distance into a fraction of the radius
971                         power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
972                         //bprint(" ");
973                         //bprint(ftos(power));
974                         //if (targ == attacker)
975                         //      print(ftos(power), "\n");
976                         if (power > 0)
977                         {
978                                 float finaldmg;
979                                 if (power > 1)
980                                         power = 1;
981                                 finaldmg = coredamage * power + edgedamage * (1 - power);
982                                 if (finaldmg > 0)
983                                 {
984                                         float a;
985                                         float c;
986                                         vector hitloc;
987                                         vector myblastorigin;
988                                         vector center;
989
990                                         myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
991
992                                         // if it's a player, use the view origin as reference
993                                         center = CENTER_OR_VIEWOFS(targ);
994
995                                         force = normalize(center - myblastorigin);
996                                         force = force * (finaldmg / coredamage) * forceintensity;
997                                         hitloc = nearest;
998
999                                         if(deathtype & WEP_BLASTER.m_id)
1000                                                 force *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
1001
1002                                         if(targ != directhitentity)
1003                                         {
1004                                                 float hits;
1005                                                 float total;
1006                                                 float hitratio;
1007                                                 float mininv_f, mininv_d;
1008
1009                                                 // test line of sight to multiple positions on box,
1010                                                 // and do damage if any of them hit
1011                                                 hits = 0;
1012
1013                                                 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
1014                                                 // so for a given max stddev:
1015                                                 // n = (1 / (2 * max stddev of hitratio))^2
1016
1017                                                 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
1018                                                 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
1019
1020                                                 if(autocvar_g_throughfloor_debug)
1021                                                         LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
1022
1023
1024                                                 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
1025
1026                                                 if(autocvar_g_throughfloor_debug)
1027                                                         LOG_INFOF(" steps=%f", total);
1028
1029
1030                                                 if (IS_PLAYER(targ))
1031                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1032                                                 else
1033                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1034
1035                                                 if(autocvar_g_throughfloor_debug)
1036                                                         LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1037
1038                                                 for(c = 0; c < total; ++c)
1039                                                 {
1040                                                         //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1041                                                         WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1042                                                         if (trace_fraction == 1 || trace_ent == targ)
1043                                                         {
1044                                                                 ++hits;
1045                                                                 if (hits > 1)
1046                                                                         hitloc = hitloc + nearest;
1047                                                                 else
1048                                                                         hitloc = nearest;
1049                                                         }
1050                                                         nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1051                                                         nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1052                                                         nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1053                                                 }
1054
1055                                                 nearest = hitloc * (1 / max(1, hits));
1056                                                 hitratio = (hits / total);
1057                                                 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1058                                                 finaldmg = finaldmg * a;
1059                                                 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1060                                                 force = force * a;
1061
1062                                                 if(autocvar_g_throughfloor_debug)
1063                                                         LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1064                                         }
1065
1066                                         //if (targ == attacker)
1067                                         //{
1068                                         //      print("hits ", ftos(hits), " / ", ftos(total));
1069                                         //      print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1070                                         //      print(" (", ftos(a), ")\n");
1071                                         //}
1072                                         if(finaldmg || force)
1073                                         {
1074                                                 if(targ.iscreature)
1075                                                 {
1076                                                         total_damage_to_creatures += finaldmg;
1077
1078                                                         if(accuracy_isgooddamage(attacker, targ))
1079                                                                 stat_damagedone += finaldmg;
1080                                                 }
1081
1082                                                 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1083                                                         Damage (targ, inflictor, attacker, finaldmg, deathtype, nearest, force);
1084                                                 else
1085                                                         Damage (targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, nearest, force);
1086                                         }
1087                                 }
1088                         }
1089                 }
1090                 targ = next;
1091         }
1092
1093         RadiusDamage_running = 0;
1094
1095         if(!DEATH_ISSPECIAL(deathtype))
1096                 accuracy_add(attacker, DEATH_WEAPONOF(deathtype).m_id, 0, min(coredamage, stat_damagedone));
1097
1098         return total_damage_to_creatures;
1099 }
1100
1101 float RadiusDamage (entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, entity directhitentity)
1102 {
1103         return RadiusDamageForSource (inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, deathtype, directhitentity);
1104 }
1105
1106 float Fire_IsBurning(entity e)
1107 {
1108         return (time < e.fire_endtime);
1109 }
1110
1111 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1112 {
1113         float dps;
1114         float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1115
1116         if(IS_PLAYER(e))
1117         {
1118                 if(IS_DEAD(e))
1119                         return -1;
1120         }
1121         else
1122         {
1123                 if(!e.fire_burner)
1124                 {
1125                         // print("adding a fire burner to ", e.classname, "\n");
1126                         e.fire_burner = new(fireburner);
1127                         setthink(e.fire_burner, fireburner_think);
1128                         e.fire_burner.nextthink = time;
1129                         e.fire_burner.owner = e;
1130                 }
1131         }
1132
1133         t = max(t, 0.1);
1134         dps = d / t;
1135         if(Fire_IsBurning(e))
1136         {
1137                 mintime = e.fire_endtime - time;
1138                 maxtime = max(mintime, t);
1139
1140                 mindps = e.fire_damagepersec;
1141                 maxdps = max(mindps, dps);
1142
1143                 if(maxtime > mintime || maxdps > mindps)
1144                 {
1145                         // Constraints:
1146
1147                         // damage we have right now
1148                         mindamage = mindps * mintime;
1149
1150                         // damage we want to get
1151                         maxdamage = mindamage + d;
1152
1153                         // but we can't exceed maxtime * maxdps!
1154                         totaldamage = min(maxdamage, maxtime * maxdps);
1155
1156                         // LEMMA:
1157                         // Look at:
1158                         // totaldamage = min(mindamage + d, maxtime * maxdps)
1159                         // We see:
1160                         // totaldamage <= maxtime * maxdps
1161                         // ==> totaldamage / maxdps <= maxtime.
1162                         // We also see:
1163                         // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1164                         //                     >= min(mintime, maxtime)
1165                         // ==> totaldamage / maxdps >= mintime.
1166
1167                         /*
1168                         // how long do we damage then?
1169                         // at least as long as before
1170                         // but, never exceed maxdps
1171                         totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1172                         */
1173
1174                         // alternate:
1175                         // at most as long as maximum allowed
1176                         // but, never below mindps
1177                         totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1178
1179                         // assuming t > mintime, dps > mindps:
1180                         // we get d = t * dps = maxtime * maxdps
1181                         // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1182                         // totaldamage / maxdps = maxtime
1183                         // totaldamage / mindps > totaldamage / maxdps = maxtime
1184                         // FROM THIS:
1185                         // a) totaltime = max(mintime, maxtime) = maxtime
1186                         // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1187
1188                         // assuming t <= mintime:
1189                         // we get maxtime = mintime
1190                         // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1191                         // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1192
1193                         // assuming dps <= mindps:
1194                         // we get mindps = maxdps.
1195                         // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1196                         // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1197                         // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1198
1199                         e.fire_damagepersec = totaldamage / totaltime;
1200                         e.fire_endtime = time + totaltime;
1201                         if(totaldamage > 1.2 * mindamage)
1202                         {
1203                                 e.fire_deathtype = dt;
1204                                 if(e.fire_owner != o)
1205                                 {
1206                                         e.fire_owner = o;
1207                                         e.fire_hitsound = false;
1208                                 }
1209                         }
1210                         if(accuracy_isgooddamage(o, e))
1211                                 accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, max(0, totaldamage - mindamage));
1212                         return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1213                 }
1214                 else
1215                         return 0;
1216         }
1217         else
1218         {
1219                 e.fire_damagepersec = dps;
1220                 e.fire_endtime = time + t;
1221                 e.fire_deathtype = dt;
1222                 e.fire_owner = o;
1223                 e.fire_hitsound = false;
1224                 if(accuracy_isgooddamage(o, e))
1225                         accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, d);
1226                 return d;
1227         }
1228 }
1229
1230 void Fire_ApplyDamage(entity e)
1231 {
1232         float t, d, hi, ty;
1233         entity o;
1234
1235         if (!Fire_IsBurning(e))
1236                 return;
1237
1238         for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1239         if(IS_NOT_A_CLIENT(o))
1240                 o = e.fire_owner;
1241
1242         // water and slime stop fire
1243         if(e.waterlevel)
1244         if(e.watertype != CONTENT_LAVA)
1245                 e.fire_endtime = 0;
1246
1247         // ice stops fire
1248         if(STAT(FROZEN, e))
1249                 e.fire_endtime = 0;
1250
1251         t = min(frametime, e.fire_endtime - time);
1252         d = e.fire_damagepersec * t;
1253
1254         hi = e.fire_owner.damage_dealt;
1255         ty = e.fire_owner.typehitsound;
1256         Damage(e, e, e.fire_owner, d, e.fire_deathtype, e.origin, '0 0 0');
1257         if(e.fire_hitsound && e.fire_owner)
1258         {
1259                 e.fire_owner.damage_dealt = hi;
1260                 e.fire_owner.typehitsound = ty;
1261         }
1262         e.fire_hitsound = true;
1263
1264         if(!IS_INDEPENDENT_PLAYER(e))
1265         if(!STAT(FROZEN, e))
1266                 FOREACH_CLIENT(IS_PLAYER(it) && it != e, {
1267                         if(!IS_DEAD(it))
1268                         if(!IS_INDEPENDENT_PLAYER(it))
1269                         if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1270                         {
1271                                 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1272                                 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1273                                 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1274                         }
1275                 });
1276 }
1277
1278 void Fire_ApplyEffect(entity e)
1279 {
1280         if(Fire_IsBurning(e))
1281                 e.effects |= EF_FLAME;
1282         else
1283                 e.effects &= ~EF_FLAME;
1284 }
1285
1286 void fireburner_think(entity this)
1287 {
1288         // for players, this is done in the regular loop
1289         if(wasfreed(this.owner))
1290         {
1291                 delete(this);
1292                 return;
1293         }
1294         Fire_ApplyEffect(this.owner);
1295         if(!Fire_IsBurning(this.owner))
1296         {
1297                 this.owner.fire_burner = NULL;
1298                 delete(this);
1299                 return;
1300         }
1301         Fire_ApplyDamage(this.owner);
1302         this.nextthink = time;
1303 }