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