]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/g_damage.qc
Merge branch 'master' into terencehill/accuracy_shotgun
[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 revivespeed, 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 = revivespeed;
538         if(targ.bot_attack)
539                 IL_REMOVE(g_bot_targets, targ);
540         targ.bot_attack = false;
541         targ.freeze_time = time;
542
543         entity ice = new(ice);
544         ice.owner = targ;
545         ice.scale = targ.scale;
546         setthink(ice, Ice_Think);
547         ice.nextthink = time;
548         ice.frame = floor(random() * 21); // ice model has 20 different looking frames
549         setmodel(ice, MDL_ICE);
550         ice.alpha = 1;
551         ice.colormod = Team_ColorRGB(targ.team);
552         ice.glowmod = ice.colormod;
553         targ.iceblock = ice;
554         targ.revival_time = 0;
555
556         Ice_Think(ice);
557
558         RemoveGrapplingHooks(targ);
559
560         FOREACH_CLIENT(IS_PLAYER(it),
561         {
562                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
563             {
564                 .entity weaponentity = weaponentities[slot];
565                 if(it.(weaponentity).hook.aiment == targ)
566                         RemoveHook(it.(weaponentity).hook);
567             }
568         });
569
570         // add waypoint
571         if(show_waypoint)
572                 WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
573 }
574
575 void Unfreeze (entity targ)
576 {
577         if(!STAT(FROZEN, targ))
578                 return;
579
580         if(STAT(FROZEN, targ) && STAT(FROZEN, targ) != 3) // only reset health if target was frozen
581         {
582                 SetResourceAmount(targ, RESOURCE_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
583                 targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
584         }
585
586         STAT(FROZEN, targ) = 0;
587         targ.revive_progress = 0;
588         targ.revival_time = time;
589         if(!targ.bot_attack)
590                 IL_PUSH(g_bot_targets, targ);
591         targ.bot_attack = true;
592
593         WaypointSprite_Kill(targ.waypointsprite_attached);
594
595         FOREACH_CLIENT(IS_PLAYER(it),
596         {
597                 for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
598             {
599                 .entity weaponentity = weaponentities[slot];
600                 if(it.(weaponentity).hook.aiment == targ)
601                         RemoveHook(it.(weaponentity).hook);
602             }
603         });
604
605         // remove the ice block
606         if(targ.iceblock)
607                 delete(targ.iceblock);
608         targ.iceblock = NULL;
609 }
610
611 void Damage (entity targ, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
612 {
613         float complainteamdamage = 0;
614         float mirrordamage = 0;
615         float mirrorforce = 0;
616
617         if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
618                 return;
619
620         entity attacker_save = attacker;
621
622         // special rule: gravity bomb does not hit team mates (other than for disconnecting the hook)
623         if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || DEATH_ISWEAPON(deathtype, WEP_TUBA))
624         {
625                 if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
626                 {
627                         return;
628                 }
629         }
630
631         if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
632         {
633                 // exit the vehicle before killing (fixes a crash)
634                 if(IS_PLAYER(targ) && targ.vehicle)
635                         vehicles_exit(targ.vehicle, VHEF_RELEASE);
636
637                 // These are ALWAYS lethal
638                 // No damage modification here
639                 // Instead, prepare the victim for his death...
640                 SetResourceAmount(targ, RESOURCE_ARMOR, 0);
641                 targ.spawnshieldtime = 0;
642                 SetResourceAmount(targ, RESOURCE_HEALTH, 0.9); // this is < 1
643                 targ.flags -= targ.flags & FL_GODMODE;
644                 damage = 100000;
645         }
646         else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
647         {
648                 // no processing
649         }
650         else
651         {
652                 // nullify damage if teamplay is on
653                 if(deathtype != DEATH_TELEFRAG.m_id)
654                 if(IS_PLAYER(attacker))
655                 {
656                         if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
657                         {
658                                 damage = 0;
659                                 force = '0 0 0';
660                         }
661                         else if(SAME_TEAM(attacker, targ))
662                         {
663                                 if(autocvar_teamplay_mode == 1)
664                                         damage = 0;
665                                 else if(attacker != targ)
666                                 {
667                                         if(autocvar_teamplay_mode == 3)
668                                                 damage = 0;
669                                         else if(autocvar_teamplay_mode == 4)
670                                         {
671                                                 if(IS_PLAYER(targ) && !IS_DEAD(targ))
672                                                 {
673                                                         attacker.dmg_team = attacker.dmg_team + damage;
674                                                         complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
675                                                         if(complainteamdamage > 0)
676                                                                 mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
677                                                         mirrorforce = autocvar_g_mirrordamage * vlen(force);
678                                                         damage = autocvar_g_friendlyfire * damage;
679                                                         // mirrordamage will be used LATER
680
681                                                         if(autocvar_g_mirrordamage_virtual)
682                                                         {
683                                                                 vector v  = healtharmor_applydamage(attacker.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
684                                                                 attacker.dmg_take += v.x;
685                                                                 attacker.dmg_save += v.y;
686                                                                 attacker.dmg_inflictor = inflictor;
687                                                                 mirrordamage = v.z;
688                                                                 mirrorforce = 0;
689                                                         }
690
691                                                         if(autocvar_g_friendlyfire_virtual)
692                                                         {
693                                                                 vector v = healtharmor_applydamage(targ.armorvalue, autocvar_g_balance_armor_blockpercent, deathtype, damage);
694                                                                 targ.dmg_take += v.x;
695                                                                 targ.dmg_save += v.y;
696                                                                 targ.dmg_inflictor = inflictor;
697                                                                 damage = 0;
698                                                                 if(!autocvar_g_friendlyfire_virtual_force)
699                                                                         force = '0 0 0';
700                                                         }
701                                                 }
702                                                 else if(!targ.canteamdamage)
703                                                         damage = 0;
704                                         }
705                                 }
706                         }
707                 }
708
709                 if (!DEATH_ISSPECIAL(deathtype))
710                 {
711                         damage *= g_weapondamagefactor;
712                         mirrordamage *= g_weapondamagefactor;
713                         complainteamdamage *= g_weapondamagefactor;
714                         force = force * g_weaponforcefactor;
715                         mirrorforce *= g_weaponforcefactor;
716                 }
717
718                 // should this be changed at all? If so, in what way?
719                 MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force);
720                 damage = M_ARGV(4, float);
721                 mirrordamage = M_ARGV(5, float);
722                 force = M_ARGV(6, vector);
723
724                 if(IS_PLAYER(targ) && damage > 0 && attacker)
725                 {
726                         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
727                     {
728                         .entity weaponentity = weaponentities[slot];
729                         if(targ.(weaponentity).hook && targ.(weaponentity).hook.aiment == attacker)
730                                 RemoveHook(targ.(weaponentity).hook);
731                     }
732                 }
733
734                 if(STAT(FROZEN, targ))
735                 if(deathtype != DEATH_HURTTRIGGER.m_id && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
736                 {
737                         if(autocvar_g_frozen_revive_falldamage > 0)
738                         if(deathtype == DEATH_FALL.m_id)
739                         if(damage >= autocvar_g_frozen_revive_falldamage)
740                         {
741                                 Unfreeze(targ);
742                                 SetResourceAmount(targ, RESOURCE_HEALTH, autocvar_g_frozen_revive_falldamage_health);
743                                 Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
744                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
745                                 Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
746                         }
747
748                         damage = 0;
749                         force *= autocvar_g_frozen_force;
750                 }
751
752                 if(STAT(FROZEN, targ) && deathtype == DEATH_HURTTRIGGER.m_id && !autocvar_g_frozen_damage_trigger)
753                 {
754                         Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
755
756                         entity spot = SelectSpawnPoint (targ, false);
757
758                         if(spot)
759                         {
760                                 damage = 0;
761                                 targ.deadflag = DEAD_NO;
762
763                                 targ.angles = spot.angles;
764
765                                 targ.effects = 0;
766                                 targ.effects |= EF_TELEPORT_BIT;
767
768                                 targ.angles_z = 0; // never spawn tilted even if the spot says to
769                                 targ.fixangle = true; // turn this way immediately
770                                 targ.velocity = '0 0 0';
771                                 targ.avelocity = '0 0 0';
772                                 targ.punchangle = '0 0 0';
773                                 targ.punchvector = '0 0 0';
774                                 targ.oldvelocity = targ.velocity;
775
776                                 targ.spawnorigin = spot.origin;
777                                 setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
778                                 // don't reset back to last position, even if new position is stuck in solid
779                                 targ.oldorigin = targ.origin;
780
781                                 Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
782                         }
783                 }
784
785                 if(!g_instagib)
786                 {
787                         // apply strength multiplier
788                         if (attacker.items & ITEM_Strength.m_itemid)
789                         {
790                                 if(targ == attacker)
791                                 {
792                                         damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
793                                         force = force * autocvar_g_balance_powerup_strength_selfforce;
794                                 }
795                                 else
796                                 {
797                                         damage = damage * autocvar_g_balance_powerup_strength_damage;
798                                         force = force * autocvar_g_balance_powerup_strength_force;
799                                 }
800                         }
801
802                         // apply invincibility multiplier
803                         if (targ.items & ITEM_Shield.m_itemid)
804                                 damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
805                 }
806
807                 if (targ == attacker)
808                         damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
809
810                 // count the damage
811                 if(attacker)
812                 if(!IS_DEAD(targ))
813                 if(deathtype != DEATH_BUFF.m_id)
814                 if(targ.takedamage == DAMAGE_AIM)
815                 if(targ != attacker)
816                 {
817                         entity victim;
818                         if(IS_VEHICLE(targ) && targ.owner)
819                                 victim = targ.owner;
820                         else
821                                 victim = targ;
822
823                         if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
824                         {
825                                 if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
826                                 {
827                                         if(damage > 0)
828                                         {
829                                                 if(deathtype != DEATH_FIRE.m_id)
830                                                 {
831                                                         if(PHYS_INPUT_BUTTON_CHAT(victim))
832                                                                 attacker.typehitsound += 1;
833                                                         else
834                                                                 attacker.damage_dealt += damage;
835                                                 }
836
837                                                 damage_goodhits += 1;
838                                                 damage_gooddamage += damage;
839
840                                                 if (!DEATH_ISSPECIAL(deathtype))
841                                                 {
842                                                         if(IS_PLAYER(targ)) // don't do this for vehicles
843                                                         if(IsFlying(victim))
844                                                                 yoda = 1;
845                                                 }
846                                         }
847                                 }
848                                 else if(IS_PLAYER(attacker))
849                                 {
850                                         if(deathtype != DEATH_FIRE.m_id)
851                                         {
852                                                 attacker.typehitsound += 1;
853                                         }
854                                         if(complainteamdamage > 0)
855                                                 if(time > CS(attacker).teamkill_complain)
856                                                 {
857                                                         CS(attacker).teamkill_complain = time + 5;
858                                                         CS(attacker).teamkill_soundtime = time + 0.4;
859                                                         CS(attacker).teamkill_soundsource = targ;
860                                                 }
861                                 }
862                         }
863                 }
864         }
865
866         // apply push
867         if (targ.damageforcescale)
868         if (force)
869         if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
870         {
871                 vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
872                 if(targ.move_movetype == MOVETYPE_PHYSICS)
873                 {
874                         entity farcent = new(farce);
875                         farcent.enemy = targ;
876                         farcent.movedir = farce * 10;
877                         if(targ.mass)
878                                 farcent.movedir = farcent.movedir * targ.mass;
879                         farcent.origin = hitloc;
880                         farcent.forcetype = FORCETYPE_FORCEATPOS;
881                         farcent.nextthink = time + 0.1;
882                         setthink(farcent, SUB_Remove);
883                 }
884                 else
885                 {
886                         targ.velocity = targ.velocity + farce;
887                 }
888                 UNSET_ONGROUND(targ);
889                 UpdateCSQCProjectile(targ);
890         }
891         // apply damage
892         if (damage != 0 || (targ.damageforcescale && force))
893         if (targ.event_damage)
894                 targ.event_damage (targ, inflictor, attacker, damage, deathtype, hitloc, force);
895
896         // apply mirror damage if any
897         if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
898         if(mirrordamage > 0 || mirrorforce > 0)
899         {
900                 attacker = attacker_save;
901
902                 force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
903                 Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, attacker.origin, force);
904         }
905 }
906
907 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)
908         // Returns total damage applies to creatures
909 {
910         entity  targ;
911         vector  force;
912         float   total_damage_to_creatures;
913         entity  next;
914         float   tfloordmg;
915         float   tfloorforce;
916
917         float stat_damagedone;
918
919         if(RadiusDamage_running)
920         {
921                 backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
922                 return 0;
923         }
924
925         RadiusDamage_running = 1;
926
927         tfloordmg = autocvar_g_throughfloor_damage;
928         tfloorforce = autocvar_g_throughfloor_force;
929
930         total_damage_to_creatures = 0;
931
932         if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
933                 if(DEATH_WEAPONOF(deathtype) != WEP_TUBA) // do not send tuba damage (bandwidth hog)
934                 {
935                         force = inflictorvelocity;
936                         if(force == '0 0 0')
937                                 force = '0 0 -1';
938                         else
939                                 force = normalize(force);
940                         if(forceintensity >= 0)
941                                 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
942                         else
943                                 Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
944                 }
945
946         stat_damagedone = 0;
947
948         targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
949         while (targ)
950         {
951                 next = targ.chain;
952                 if ((targ != inflictor) || inflictorselfdamage)
953                 if (((cantbe != targ) && !mustbe) || (mustbe == targ))
954                 if (targ.takedamage)
955                 {
956                         vector nearest;
957                         vector diff;
958                         float power;
959
960                         // LordHavoc: measure distance to nearest point on target (not origin)
961                         // (this guarentees 100% damage on a touch impact)
962                         nearest = targ.WarpZone_findradius_nearest;
963                         diff = targ.WarpZone_findradius_dist;
964                         // round up a little on the damage to ensure full damage on impacts
965                         // and turn the distance into a fraction of the radius
966                         power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
967                         //bprint(" ");
968                         //bprint(ftos(power));
969                         //if (targ == attacker)
970                         //      print(ftos(power), "\n");
971                         if (power > 0)
972                         {
973                                 float finaldmg;
974                                 if (power > 1)
975                                         power = 1;
976                                 finaldmg = coredamage * power + edgedamage * (1 - power);
977                                 if (finaldmg > 0)
978                                 {
979                                         float a;
980                                         float c;
981                                         vector hitloc;
982                                         vector myblastorigin;
983                                         vector center;
984
985                                         myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
986
987                                         // if it's a player, use the view origin as reference
988                                         center = CENTER_OR_VIEWOFS(targ);
989
990                                         force = normalize(center - myblastorigin);
991                                         force = force * (finaldmg / coredamage) * forceintensity;
992                                         hitloc = nearest;
993
994                                         if(deathtype & WEP_BLASTER.m_id)
995                                                 force *= WEP_CVAR_BOTH(blaster, !(deathtype & HITTYPE_SECONDARY), force_zscale);
996
997                                         if(targ != directhitentity)
998                                         {
999                                                 float hits;
1000                                                 float total;
1001                                                 float hitratio;
1002                                                 float mininv_f, mininv_d;
1003
1004                                                 // test line of sight to multiple positions on box,
1005                                                 // and do damage if any of them hit
1006                                                 hits = 0;
1007
1008                                                 // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
1009                                                 // so for a given max stddev:
1010                                                 // n = (1 / (2 * max stddev of hitratio))^2
1011
1012                                                 mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
1013                                                 mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
1014
1015                                                 if(autocvar_g_throughfloor_debug)
1016                                                         LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
1017
1018
1019                                                 total = 0.25 * (max(mininv_f, mininv_d) ** 2);
1020
1021                                                 if(autocvar_g_throughfloor_debug)
1022                                                         LOG_INFOF(" steps=%f", total);
1023
1024
1025                                                 if (IS_PLAYER(targ))
1026                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
1027                                                 else
1028                                                         total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
1029
1030                                                 if(autocvar_g_throughfloor_debug)
1031                                                         LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
1032
1033                                                 for(c = 0; c < total; ++c)
1034                                                 {
1035                                                         //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
1036                                                         WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
1037                                                         if (trace_fraction == 1 || trace_ent == targ)
1038                                                         {
1039                                                                 ++hits;
1040                                                                 if (hits > 1)
1041                                                                         hitloc = hitloc + nearest;
1042                                                                 else
1043                                                                         hitloc = nearest;
1044                                                         }
1045                                                         nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
1046                                                         nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
1047                                                         nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
1048                                                 }
1049
1050                                                 nearest = hitloc * (1 / max(1, hits));
1051                                                 hitratio = (hits / total);
1052                                                 a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
1053                                                 finaldmg = finaldmg * a;
1054                                                 a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
1055                                                 force = force * a;
1056
1057                                                 if(autocvar_g_throughfloor_debug)
1058                                                         LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
1059                                         }
1060
1061                                         //if (targ == attacker)
1062                                         //{
1063                                         //      print("hits ", ftos(hits), " / ", ftos(total));
1064                                         //      print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
1065                                         //      print(" (", ftos(a), ")\n");
1066                                         //}
1067                                         if(finaldmg || force)
1068                                         {
1069                                                 if(targ.iscreature)
1070                                                 {
1071                                                         total_damage_to_creatures += finaldmg;
1072
1073                                                         if(accuracy_isgooddamage(attacker, targ))
1074                                                                 stat_damagedone += finaldmg;
1075                                                 }
1076
1077                                                 if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
1078                                                         Damage (targ, inflictor, attacker, finaldmg, deathtype, nearest, force);
1079                                                 else
1080                                                         Damage (targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, nearest, force);
1081                                         }
1082                                 }
1083                         }
1084                 }
1085                 targ = next;
1086         }
1087
1088         RadiusDamage_running = 0;
1089
1090         if(!DEATH_ISSPECIAL(deathtype))
1091                 accuracy_add(attacker, DEATH_WEAPONOF(deathtype).m_id, 0, min(coredamage, stat_damagedone));
1092
1093         return total_damage_to_creatures;
1094 }
1095
1096 float RadiusDamage (entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, entity directhitentity)
1097 {
1098         return RadiusDamageForSource (inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, cantbe, mustbe, false, forceintensity, deathtype, directhitentity);
1099 }
1100
1101 float Fire_IsBurning(entity e)
1102 {
1103         return (time < e.fire_endtime);
1104 }
1105
1106 float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
1107 {
1108         float dps;
1109         float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
1110
1111         if(IS_PLAYER(e))
1112         {
1113                 if(IS_DEAD(e))
1114                         return -1;
1115         }
1116         else
1117         {
1118                 if(!e.fire_burner)
1119                 {
1120                         // print("adding a fire burner to ", e.classname, "\n");
1121                         e.fire_burner = new(fireburner);
1122                         setthink(e.fire_burner, fireburner_think);
1123                         e.fire_burner.nextthink = time;
1124                         e.fire_burner.owner = e;
1125                 }
1126         }
1127
1128         t = max(t, 0.1);
1129         dps = d / t;
1130         if(Fire_IsBurning(e))
1131         {
1132                 mintime = e.fire_endtime - time;
1133                 maxtime = max(mintime, t);
1134
1135                 mindps = e.fire_damagepersec;
1136                 maxdps = max(mindps, dps);
1137
1138                 if(maxtime > mintime || maxdps > mindps)
1139                 {
1140                         // Constraints:
1141
1142                         // damage we have right now
1143                         mindamage = mindps * mintime;
1144
1145                         // damage we want to get
1146                         maxdamage = mindamage + d;
1147
1148                         // but we can't exceed maxtime * maxdps!
1149                         totaldamage = min(maxdamage, maxtime * maxdps);
1150
1151                         // LEMMA:
1152                         // Look at:
1153                         // totaldamage = min(mindamage + d, maxtime * maxdps)
1154                         // We see:
1155                         // totaldamage <= maxtime * maxdps
1156                         // ==> totaldamage / maxdps <= maxtime.
1157                         // We also see:
1158                         // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
1159                         //                     >= min(mintime, maxtime)
1160                         // ==> totaldamage / maxdps >= mintime.
1161
1162                         /*
1163                         // how long do we damage then?
1164                         // at least as long as before
1165                         // but, never exceed maxdps
1166                         totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
1167                         */
1168
1169                         // alternate:
1170                         // at most as long as maximum allowed
1171                         // but, never below mindps
1172                         totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
1173
1174                         // assuming t > mintime, dps > mindps:
1175                         // we get d = t * dps = maxtime * maxdps
1176                         // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
1177                         // totaldamage / maxdps = maxtime
1178                         // totaldamage / mindps > totaldamage / maxdps = maxtime
1179                         // FROM THIS:
1180                         // a) totaltime = max(mintime, maxtime) = maxtime
1181                         // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
1182
1183                         // assuming t <= mintime:
1184                         // we get maxtime = mintime
1185                         // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
1186                         // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
1187
1188                         // assuming dps <= mindps:
1189                         // we get mindps = maxdps.
1190                         // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
1191                         // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
1192                         // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
1193
1194                         e.fire_damagepersec = totaldamage / totaltime;
1195                         e.fire_endtime = time + totaltime;
1196                         if(totaldamage > 1.2 * mindamage)
1197                         {
1198                                 e.fire_deathtype = dt;
1199                                 if(e.fire_owner != o)
1200                                 {
1201                                         e.fire_owner = o;
1202                                         e.fire_hitsound = false;
1203                                 }
1204                         }
1205                         if(accuracy_isgooddamage(o, e))
1206                                 accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, max(0, totaldamage - mindamage));
1207                         return max(0, totaldamage - mindamage); // can never be negative, but to make sure
1208                 }
1209                 else
1210                         return 0;
1211         }
1212         else
1213         {
1214                 e.fire_damagepersec = dps;
1215                 e.fire_endtime = time + t;
1216                 e.fire_deathtype = dt;
1217                 e.fire_owner = o;
1218                 e.fire_hitsound = false;
1219                 if(accuracy_isgooddamage(o, e))
1220                         accuracy_add(o, DEATH_WEAPONOF(dt).m_id, 0, d);
1221                 return d;
1222         }
1223 }
1224
1225 void Fire_ApplyDamage(entity e)
1226 {
1227         float t, d, hi, ty;
1228         entity o;
1229
1230         if (!Fire_IsBurning(e))
1231                 return;
1232
1233         for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
1234         if(IS_NOT_A_CLIENT(o))
1235                 o = e.fire_owner;
1236
1237         // water and slime stop fire
1238         if(e.waterlevel)
1239         if(e.watertype != CONTENT_LAVA)
1240                 e.fire_endtime = 0;
1241
1242         // ice stops fire
1243         if(STAT(FROZEN, e))
1244                 e.fire_endtime = 0;
1245
1246         t = min(frametime, e.fire_endtime - time);
1247         d = e.fire_damagepersec * t;
1248
1249         hi = e.fire_owner.damage_dealt;
1250         ty = e.fire_owner.typehitsound;
1251         Damage(e, e, e.fire_owner, d, e.fire_deathtype, e.origin, '0 0 0');
1252         if(e.fire_hitsound && e.fire_owner)
1253         {
1254                 e.fire_owner.damage_dealt = hi;
1255                 e.fire_owner.typehitsound = ty;
1256         }
1257         e.fire_hitsound = true;
1258
1259         if(!IS_INDEPENDENT_PLAYER(e))
1260         if(!STAT(FROZEN, e))
1261                 FOREACH_CLIENT(IS_PLAYER(it) && it != e, {
1262                         if(!IS_DEAD(it))
1263                         if(!IS_INDEPENDENT_PLAYER(it))
1264                         if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
1265                         {
1266                                 t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
1267                                 d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
1268                                 Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
1269                         }
1270                 });
1271 }
1272
1273 void Fire_ApplyEffect(entity e)
1274 {
1275         if(Fire_IsBurning(e))
1276                 e.effects |= EF_FLAME;
1277         else
1278                 e.effects &= ~EF_FLAME;
1279 }
1280
1281 void fireburner_think(entity this)
1282 {
1283         // for players, this is done in the regular loop
1284         if(wasfreed(this.owner))
1285         {
1286                 delete(this);
1287                 return;
1288         }
1289         Fire_ApplyEffect(this.owner);
1290         if(!Fire_IsBurning(this.owner))
1291         {
1292                 this.owner.fire_burner = NULL;
1293                 delete(this);
1294                 return;
1295         }
1296         Fire_ApplyDamage(this.owner);
1297         this.nextthink = time;
1298 }