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