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