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