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