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