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