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