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