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