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