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