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