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