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