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