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