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