]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'master' into terencehill/ft_autorevive_progress
authorterencehill <piuntn@gmail.com>
Wed, 7 Oct 2020 13:15:05 +0000 (15:15 +0200)
committerterencehill <piuntn@gmail.com>
Wed, 7 Oct 2020 13:15:05 +0000 (15:15 +0200)
1  2 
gamemodes-server.cfg
qcsrc/client/view.qc
qcsrc/common/gamemodes/gamemode/freezetag/sv_freezetag.qc
qcsrc/server/damage.qc
qcsrc/server/player.qc

diff --combined gamemodes-server.cfg
index 9a47eea2ed5d3069290a5a752247b4702f33334c,98b6b1a9051c2c13917e46a6748b10263f130b2a..8dd0d5d4897223a09ae2daa1ce6f45231a88cb5f
@@@ -2,8 -2,8 +2,8 @@@
  //  Master config for core game modes
  // ===================================
  
- // global gametype setting (0 = deathmatch)
- set gamecfg 0
+ // global gametype setting (uses gametype shortname, default gametype is deathmatch)
+ set gamecfg "dm"
  
  
  // =================
@@@ -372,10 -372,6 +372,10 @@@ set g_freezetag_revive_extra_size 100 "
  set g_freezetag_revive_nade 1 "Enable reviving from own nade explosion"
  set g_freezetag_revive_nade_health 40 "Amount of health player has if they revived from their own nade explosion"
  set g_freezetag_round_timelimit 360 "round time limit in seconds"
 +set g_freezetag_revive_auto 1 "automatically revive frozen players after some time (g_freezetag_frozen_maxtime)"
 +set g_freezetag_revive_auto_progress 1 "start the automatic reviving progress as soon as the player gets frozen"
 +set g_freezetag_revive_auto_reducible 1 "reduce auto-revival time when frozen players are hit by enemies; set to -1 to reduce it even when they are hit by teammates"
 +set g_freezetag_revive_auto_reducible_forcefactor 0.025 "hit force to time reduction conversion factor"
  set g_freezetag_frozen_maxtime 60 "frozen players will be automatically unfrozen after this time in seconds"
  set g_freezetag_teams_override 0
  set g_freezetag_team_spawns 0 "when 1, players spawn from the team spawnpoints of the map, if any"
diff --combined qcsrc/client/view.qc
index b8f33d2a10c710ea35a7eba6410e067bc9d85fa7,2b4081aba8a477e379105df27480434754d77082..81d6a634e7f7e7295dbc5ea1eb07af142a98e750
@@@ -1,9 -1,10 +1,10 @@@
  #include "view.qh"
  
  #include "autocvars.qh"
- #include "miscfunctions.qh"
+ #include <client/draw.qh>
  #include "announcer.qh"
  #include "hud/_mod.qh"
+ #include "main.qh"
  #include "mapvoting.qh"
  #include "shownames.qh"
  #include "hud/panel/scoreboard.qh"
@@@ -44,8 -45,6 +45,6 @@@
  #include <lib/warpzone/client.qh>
  #include <lib/warpzone/common.qh>
  
- #define EFMASK_CHEAP (EF_ADDITIVE | EF_DOUBLESIDED | EF_FULLBRIGHT | EF_NODEPTHTEST | EF_NODRAW | EF_NOSHADOW | EF_SELECTABLE | EF_TELEPORT_BIT)
  float autocvar_cl_viewmodel_scale;
  float autocvar_cl_viewmodel_alpha = 1;
  
@@@ -364,6 -363,33 +363,33 @@@ STATIC_INIT(viewmodel) 
        viewmodels[slot] = new(viewmodel);
  }
  
+ vector project_3d_to_2d(vector vec)
+ {
+       vec = cs_project(vec);
+       if(cs_project_is_b0rked > 0)
+       {
+               vec.x *= vid_conwidth / vid_width;
+               vec.y *= vid_conheight / vid_height;
+       }
+       return vec;
+ }
+ bool projected_on_screen(vector screen_pos)
+ {
+       return screen_pos.z >= 0
+               && screen_pos.x >= 0
+               && screen_pos.y >= 0
+               && screen_pos.x < vid_conwidth
+               && screen_pos.y < vid_conheight;
+ }
+ void update_mousepos()
+ {
+       mousepos += getmousepos() * autocvar_menu_mouse_speed;
+       mousepos.x = bound(0, mousepos.x, vid_conwidth);
+       mousepos.y = bound(0, mousepos.y, vid_conheight);
+ }
  float showfps_prevfps;
  float showfps_prevfps_time;
  int showfps_framecounter;
@@@ -373,7 -399,8 +399,8 @@@ void fpscounter_update(
        if(!STAT(SHOWFPS))
                return;
  
-       float currentTime = gettime(GETTIME_REALTIME);
+       float currentTime = gettime(GETTIME_FRAMESTART);
        showfps_framecounter += 1;
        if(currentTime - showfps_prevfps_time > STAT(SHOWFPS))
        {
  
  STATIC_INIT(fpscounter_init)
  {
-       float currentTime = gettime(GETTIME_REALTIME);
+       float currentTime = gettime(GETTIME_FRAMESTART);
        showfps_prevfps_time = currentTime; // we must initialize it to avoid an instant low frame sending
  }
  
@@@ -925,11 -952,9 +952,11 @@@ void HUD_Draw(entity this
        else if(STAT(FROZEN))
        {
                vector col = '0.25 0.90 1';
 -              if(STAT(REVIVE_PROGRESS))
 -                      col += vec3(STAT(REVIVE_PROGRESS), -STAT(REVIVE_PROGRESS), -STAT(REVIVE_PROGRESS));
 -              drawfill('0 0 0', vec2(vid_conwidth, vid_conheight), col, autocvar_hud_colorflash_alpha, DRAWFLAG_ADDITIVE);
 +              float col_fade = max(0, STAT(REVIVE_PROGRESS) * 2 - 1);
 +              float alpha_fade = 0.3 + 0.7 * (1 - max(0, STAT(REVIVE_PROGRESS) * 4 - 3));
 +              if(col_fade)
 +                      col += vec3(col_fade, -col_fade, -col_fade);
 +              drawfill('0 0 0', vec2(vid_conwidth, vid_conheight), col, autocvar_hud_colorflash_alpha * alpha_fade, DRAWFLAG_ADDITIVE);
        }
  
        HUD_Scale_Enable();
        if(autocvar_r_letterbox == 0)
                if(autocvar_viewsize < 120)
                {
-                       if(!(ISGAMETYPE(RACE) || ISGAMETYPE(CTS)))
+                       if(!MUTATOR_CALLHOOK(DrawScoreboardAccuracy))
                                Accuracy_LoadLevels();
  
                        HUD_Main();
@@@ -1310,9 -1335,17 +1337,17 @@@ void View_PostProcessing(
  
  void View_Lock()
  {
-       int lock_type = (!autocvar_hud_cursormode && ((autocvar__hud_configure && spectatee_status <= 0) || intermission > 1 || QuickMenu_IsOpened()));
-       if (lock_type == 0)
-               lock_type = autocvar_cl_lockview;
+       int lock_type = autocvar_cl_lockview;
+       if (!autocvar_hud_cursormode
+               && ((autocvar__hud_configure && spectatee_status <= 0)
+                       || intermission > 1
+                       || HUD_Radar_Clickable()
+                       || HUD_MinigameMenu_IsOpened()
+                       || QuickMenu_IsOpened()
+               )
+       )
+               lock_type = 1;
  
        // lock_type 1: lock origin and angles
        // lock_type 2: lock only origin
index fbd8262af648aacd1bbf5db9ac46a29ca73ae93e,5378fb346abf9793aa38e76cbc16dc100cf09d8e..ba25fa71617792de6e542092543f13ac5de86383
@@@ -1,5 -1,6 +1,6 @@@
  #include "sv_freezetag.qh"
  
+ #include <server/elimination.qh>
  #include <server/resources.qh>
  
  float autocvar_g_freezetag_frozen_maxtime;
@@@ -198,8 -199,7 +199,8 @@@ void freezetag_Freeze(entity targ, enti
        if(STAT(FROZEN, targ))
                return;
  
 -      if(autocvar_g_freezetag_frozen_maxtime > 0)
 +      targ.freezetag_frozen_time = time;
 +      if (autocvar_g_freezetag_revive_auto && autocvar_g_freezetag_frozen_maxtime > 0)
                targ.freezetag_frozen_timeout = time + autocvar_g_freezetag_frozen_maxtime;
  
        Freeze(targ, 0, FROZEN_NORMAL, true);
@@@ -458,30 -458,6 +459,30 @@@ MUTATOR_HOOKFUNCTION(ft, Unfreeze
        targ.freezetag_frozen_timeout = 0;
  }
  
 +MUTATOR_HOOKFUNCTION(ft, Damage_Calculate)
 +{
 +      entity frag_attacker = M_ARGV(1, entity);
 +      entity frag_target = M_ARGV(2, entity);
 +      //float frag_deathtype = M_ARGV(3, float);
 +      //float frag_damage = M_ARGV(4, float);
 +      vector frag_force = M_ARGV(6, vector);
 +
 +      if (STAT(FROZEN, frag_target) == FROZEN_NORMAL && autocvar_g_freezetag_revive_auto_reducible
 +              && autocvar_g_freezetag_frozen_maxtime > 0 && autocvar_g_freezetag_revive_auto)
 +      {
 +              float t = 0;
 +              if ((autocvar_g_freezetag_revive_auto_reducible < 0 || DIFF_TEAM(frag_attacker, frag_target))
 +                      && frag_target.freezetag_frozen_timeout > time)
 +              {
 +                      if (fabs(autocvar_g_freezetag_revive_auto_reducible) == 1)
 +                              t = vlen(frag_force) * autocvar_g_freezetag_revive_auto_reducible_forcefactor;
 +                      frag_target.freezetag_frozen_timeout -= t;
 +                      if (frag_target.freezetag_frozen_timeout < time)
 +                              frag_target.freezetag_frozen_timeout = time;
 +              }
 +      }
 +}
 +
  #ifdef IS_REVIVING
        #undef IS_REVIVING
  #endif
@@@ -529,36 -505,30 +530,36 @@@ MUTATOR_HOOKFUNCTION(ft, PlayerPreThink
        if (!n && player.freezetag_frozen_timeout > 0 && time >= player.freezetag_frozen_timeout)
                n = -1;
  
 +      float base_progress = 0;
 +      if  (STAT(FROZEN, player) == FROZEN_NORMAL && autocvar_g_freezetag_revive_auto
 +              && autocvar_g_freezetag_frozen_maxtime > 0 && autocvar_g_freezetag_revive_auto_progress)
 +      {
 +              // NOTE if auto-revival is in progress, manual revive speed is reduced so that it always takes the same amount of time
 +              base_progress = bound(0, (1 - (player.freezetag_frozen_timeout - time) / autocvar_g_freezetag_frozen_maxtime), 1);
 +      }
 +
        if (!n) // no teammate nearby
        {
                if (STAT(FROZEN, player) == FROZEN_NORMAL)
 -              {
 -                      STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) - frametime * autocvar_g_freezetag_revive_clearspeed, 1);
 -                      SetResourceExplicit(player, RES_HEALTH, max(1, STAT(REVIVE_PROGRESS, player) * ((warmup_stage) ? warmup_start_health : start_health)));
 -              }
 +                      STAT(REVIVE_PROGRESS, player) = bound(base_progress, STAT(REVIVE_PROGRESS, player) - frametime * autocvar_g_freezetag_revive_clearspeed * (1 - base_progress), 1);
                else if (!STAT(FROZEN, player))
 -                      STAT(REVIVE_PROGRESS, player) = 0; // thawing nobody
 +                      STAT(REVIVE_PROGRESS, player) = base_progress; // thawing nobody
        }
        else if (STAT(FROZEN, player) == FROZEN_NORMAL) // OK, there is at least one teammate reviving us
        {
 -              STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) + frametime * max(1/60, autocvar_g_freezetag_revive_speed), 1);
 -              SetResourceExplicit(player, RES_HEALTH, max(1, STAT(REVIVE_PROGRESS, player) * ((warmup_stage) ? warmup_start_health : start_health)));
 +              STAT(REVIVE_PROGRESS, player) = bound(base_progress, STAT(REVIVE_PROGRESS, player) + frametime * max(1/60, autocvar_g_freezetag_revive_speed * (1 - base_progress)), 1);
  
                if(STAT(REVIVE_PROGRESS, player) >= 1)
                {
 +                      float frozen_time = time - player.freezetag_frozen_time;
                        Unfreeze(player, false);
 +                      SetResourceExplicit(player, RES_HEALTH, ((warmup_stage) ? warmup_start_health : start_health));
                        freezetag_count_alive_players();
  
                        if(n == -1)
                        {
 -                              Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_AUTO_REVIVED, autocvar_g_freezetag_frozen_maxtime);
 -                              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_AUTO_REVIVED, player.netname, autocvar_g_freezetag_frozen_maxtime);
 +                              Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_FREEZETAG_AUTO_REVIVED, frozen_time);
 +                              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_AUTO_REVIVED, player.netname, frozen_time);
                                return true;
                        }
  
                        STAT(REVIVE_PROGRESS, it) = STAT(REVIVE_PROGRESS, player);
        }
  
 +      if (STAT(FROZEN, player) == FROZEN_NORMAL)
 +      {
 +              entity player_wp = player.waypointsprite_attached;
 +              if (n > 0 || (n == 0 && STAT(REVIVE_PROGRESS, player) > 0.95))
 +              {
 +                      WaypointSprite_UpdateSprites(player_wp, WP_Reviving, WP_Null, WP_Null);
 +                      WaypointSprite_UpdateTeamRadar(player_wp, RADARICON_WAYPOINT, WP_REVIVING_COLOR);
 +              }
 +              else
 +              {
 +                      WaypointSprite_UpdateSprites(player_wp, WP_Frozen, WP_Null, WP_Null);
 +                      WaypointSprite_UpdateTeamRadar(player_wp, RADARICON_WAYPOINT, WP_FROZEN_COLOR);
 +              }
 +
 +              WaypointSprite_UpdateMaxHealth(player_wp, 1);
 +              WaypointSprite_UpdateHealth(player_wp, STAT(REVIVE_PROGRESS, player));
 +      }
 +
        return true;
  }
  
diff --combined qcsrc/server/damage.qc
index 0000000000000000000000000000000000000000,f0655a9f895d2b746281632a6cfeb1b6a03a8e43..b983cdb8aa2e11c26e002a4438a487983c3ef99c
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,1302 +1,1300 @@@
 -                      else if(SAME_TEAM(attacker, targ))
+ #include "damage.qh"
+ #include <common/effects/all.qh>
+ #include "bot/api.qh"
+ #include "hook.qh"
+ #include <server/client.qh>
+ #include <server/gamelog.qh>
+ #include <server/items/items.qh>
+ #include <server/mutators/_mod.qh>
+ #include <server/main.qh>
+ #include <server/world.qh>
+ #include "teamplay.qh"
+ #include "scores.qh"
+ #include "spawnpoints.qh"
+ #include "../common/state.qh"
+ #include "../common/physics/player.qh"
+ #include "resources.qh"
+ #include "../common/vehicles/all.qh"
+ #include "../common/items/_mod.qh"
+ #include "../common/mutators/mutator/waypoints/waypointsprites.qh"
+ #include "../common/mutators/mutator/instagib/sv_instagib.qh"
+ #include "../common/mutators/mutator/buffs/buffs.qh"
+ #include "weapons/accuracy.qh"
+ #include "weapons/csqcprojectile.qh"
+ #include "weapons/selection.qh"
+ #include "../common/constants.qh"
+ #include "../common/deathtypes/all.qh"
+ #include <common/mapobjects/defs.qh>
+ #include <common/mapobjects/triggers.qh>
+ #include "../common/notifications/all.qh"
+ #include "../common/physics/movetypes/movetypes.qh"
+ #include "../common/playerstats.qh"
+ #include "../common/teams.qh"
+ #include "../common/util.qh"
+ #include <common/gamemodes/_mod.qh>
+ #include <common/gamemodes/rules.qh>
+ #include <common/weapons/_all.qh>
+ #include "../lib/csqcmodel/sv_model.qh"
+ #include "../lib/warpzone/common.qh"
+ void UpdateFrags(entity player, int f)
+ {
+       GameRules_scoring_add_team(player, SCORE, f);
+ }
+ void GiveFrags(entity attacker, entity targ, float f, int deathtype, .entity weaponentity)
+ {
+       // TODO route through PlayerScores instead
+       if(game_stopped) return;
+       if(f < 0)
+       {
+               if(targ == attacker)
+               {
+                       // suicide
+                       GameRules_scoring_add(attacker, SUICIDES, 1);
+               }
+               else
+               {
+                       // teamkill
+                       GameRules_scoring_add(attacker, TEAMKILLS, 1);
+               }
+       }
+       else
+       {
+               // regular frag
+               GameRules_scoring_add(attacker, KILLS, 1);
+               if(!warmup_stage && targ.playerid)
+                       PlayerStats_GameReport_Event_Player(attacker, sprintf("kills-%d", targ.playerid), 1);
+       }
+       GameRules_scoring_add(targ, DEATHS, 1);
+       // FIXME fix the mess this is (we have REAL points now!)
+       if(MUTATOR_CALLHOOK(GiveFragsForKill, attacker, targ, f, deathtype, attacker.(weaponentity)))
+               f = M_ARGV(2, float);
+       attacker.totalfrags += f;
+       if(f)
+               UpdateFrags(attacker, f);
+ }
+ string AppendItemcodes(string s, entity player)
+ {
+       for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
+       {
+               .entity weaponentity = weaponentities[slot];
+               int w = player.(weaponentity).m_weapon.m_id;
+               if(w == 0)
+                       w = player.(weaponentity).cnt; // previous weapon
+               if(w != 0 || slot == 0)
+                       s = strcat(s, ftos(w));
+       }
+       if(time < STAT(STRENGTH_FINISHED, player))
+               s = strcat(s, "S");
+       if(time < STAT(INVINCIBLE_FINISHED, player))
+               s = strcat(s, "I");
+       if(PHYS_INPUT_BUTTON_CHAT(player))
+               s = strcat(s, "T");
+       // TODO: include these codes as a flag on the item itself
+       MUTATOR_CALLHOOK(LogDeath_AppendItemCodes, player, s);
+       s = M_ARGV(1, string);
+       return s;
+ }
+ void LogDeath(string mode, int deathtype, entity killer, entity killed)
+ {
+       string s;
+       if(!autocvar_sv_eventlog)
+               return;
+       s = strcat(":kill:", mode);
+       s = strcat(s, ":", ftos(killer.playerid));
+       s = strcat(s, ":", ftos(killed.playerid));
+       s = strcat(s, ":type=", Deathtype_Name(deathtype));
+       s = strcat(s, ":items=");
+       s = AppendItemcodes(s, killer);
+       if(killed != killer)
+       {
+               s = strcat(s, ":victimitems=");
+               s = AppendItemcodes(s, killed);
+       }
+       GameLogEcho(s);
+ }
+ void Obituary_SpecialDeath(
+       entity notif_target,
+       float murder,
+       int deathtype,
+       string s1, string s2, string s3,
+       float f1, float f2, float f3)
+ {
+       if(!DEATH_ISSPECIAL(deathtype))
+       {
+               backtrace("Obituary_SpecialDeath called without a special deathtype?\n");
+               return;
+       }
+       entity deathent = REGISTRY_GET(Deathtypes, deathtype - DT_FIRST);
+       if (!deathent)
+       {
+               backtrace("Obituary_SpecialDeath: Could not find deathtype entity!\n");
+               return;
+       }
+       if(g_cts && deathtype == DEATH_KILL.m_id)
+               return; // TODO: somehow put this in CTS gamemode file!
+       Notification death_message = (murder) ? deathent.death_msgmurder : deathent.death_msgself;
+       if(death_message)
+       {
+               Send_Notification_WOCOVA(
+                       NOTIF_ONE,
+                       notif_target,
+                       MSG_MULTI,
+                       death_message,
+                       s1, s2, s3, "",
+                       f1, f2, f3, 0
+               );
+               Send_Notification_WOCOVA(
+                       NOTIF_ALL_EXCEPT,
+                       notif_target,
+                       MSG_INFO,
+                       death_message.nent_msginfo,
+                       s1, s2, s3, "",
+                       f1, f2, f3, 0
+               );
+       }
+ }
+ float Obituary_WeaponDeath(
+       entity notif_target,
+       float murder,
+       int deathtype,
+       string s1, string s2, string s3,
+       float f1, float f2)
+ {
+       Weapon death_weapon = DEATH_WEAPONOF(deathtype);
+       if (death_weapon == WEP_Null)
+               return false;
+       w_deathtype = deathtype;
+       Notification death_message = ((murder) ? death_weapon.wr_killmessage(death_weapon) : death_weapon.wr_suicidemessage(death_weapon));
+       w_deathtype = false;
+       if (death_message)
+       {
+               Send_Notification_WOCOVA(
+                       NOTIF_ONE,
+                       notif_target,
+                       MSG_MULTI,
+                       death_message,
+                       s1, s2, s3, "",
+                       f1, f2, 0, 0
+               );
+               // send the info part to everyone
+               Send_Notification_WOCOVA(
+                       NOTIF_ALL_EXCEPT,
+                       notif_target,
+                       MSG_INFO,
+                       death_message.nent_msginfo,
+                       s1, s2, s3, "",
+                       f1, f2, 0, 0
+               );
+       }
+       else
+       {
+               LOG_TRACEF(
+                       "Obituary_WeaponDeath(): ^1Deathtype ^7(%d)^1 has no notification for weapon %s!\n",
+                       deathtype,
+                       death_weapon.netname
+               );
+       }
+       return true;
+ }
+ bool frag_centermessage_override(entity attacker, entity targ, int deathtype, int kill_count_to_attacker, int kill_count_to_target, string attacker_name)
+ {
+       if(deathtype == DEATH_FIRE.m_id)
+       {
+               Send_Notification(NOTIF_ONE, attacker, MSG_CHOICE, CHOICE_FRAG_FIRE, targ.netname, kill_count_to_attacker, (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping));
+               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));
+               return true;
+       }
+       return MUTATOR_CALLHOOK(FragCenterMessage, attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target);
+ }
+ void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .entity weaponentity)
+ {
+       // Sanity check
+       if (!IS_PLAYER(targ)) { backtrace("Obituary called on non-player?!\n"); return; }
+       // Declarations
+       float notif_firstblood = false;
+       float kill_count_to_attacker, kill_count_to_target;
+       bool notif_anonymous = false;
+       string attacker_name = attacker.netname;
+       // Set final information for the death
+       targ.death_origin = targ.origin;
+       string deathlocation = (autocvar_notification_server_allows_location ? NearestLocation(targ.death_origin) : "");
+       // Abort now if a mutator requests it
+       if (MUTATOR_CALLHOOK(ClientObituary, inflictor, attacker, targ, deathtype, attacker.(weaponentity))) { CS(targ).killcount = 0; return; }
+       notif_anonymous = M_ARGV(5, bool);
+       if(notif_anonymous)
+               attacker_name = "Anonymous player";
+       #ifdef NOTIFICATIONS_DEBUG
+       Debug_Notification(
+               sprintf(
+                       "Obituary(%s, %s, %s, %s = %d);\n",
+                       attacker_name,
+                       inflictor.netname,
+                       targ.netname,
+                       Deathtype_Name(deathtype),
+                       deathtype
+               )
+       );
+       #endif
+       // =======
+       // SUICIDE
+       // =======
+       if(targ == attacker)
+       {
+               if(DEATH_ISSPECIAL(deathtype))
+               {
+                       if(deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
+                       {
+                               Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", targ.team, 0, 0);
+                       }
+                       else
+                       {
+                               switch(DEATH_ENT(deathtype))
+                               {
+                                       case DEATH_MIRRORDAMAGE:
+                                       {
+                                               Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
+                                               break;
+                                       }
+                                       default:
+                                       {
+                                               Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
+                                               break;
+                                       }
+                               }
+                       }
+               }
+               else if (!Obituary_WeaponDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0))
+               {
+                       backtrace("SUICIDE: what the hell happened here?\n");
+                       return;
+               }
+               LogDeath("suicide", deathtype, targ, targ);
+               if(deathtype != DEATH_AUTOTEAMCHANGE.m_id) // special case: don't negate frags if auto switched
+                       GiveFrags(attacker, targ, -1, deathtype, weaponentity);
+       }
+       // ======
+       // MURDER
+       // ======
+       else if(IS_PLAYER(attacker))
+       {
+               if(SAME_TEAM(attacker, targ))
+               {
+                       LogDeath("tk", deathtype, attacker, targ);
+                       GiveFrags(attacker, targ, -1, deathtype, weaponentity);
+                       CS(attacker).killcount = 0;
+                       Send_Notification(NOTIF_ONE, attacker, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAG, targ.netname);
+                       Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_DEATH_TEAMKILL_FRAGGED, attacker_name);
+                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(targ.team, INFO_DEATH_TEAMKILL), targ.netname, attacker_name, deathlocation, CS(targ).killcount);
+                       // In this case, the death message will ALWAYS be "foo was betrayed by bar"
+                       // No need for specific death/weapon messages...
+               }
+               else
+               {
+                       LogDeath("frag", deathtype, attacker, targ);
+                       GiveFrags(attacker, targ, 1, deathtype, weaponentity);
+                       CS(attacker).taunt_soundtime = time + 1;
+                       CS(attacker).killcount = CS(attacker).killcount + 1;
+                       attacker.killsound += 1;
+                       // TODO: improve SPREE_ITEM and KILL_SPREE_LIST
+                       // these 2 macros are spread over multiple files
+                       #define SPREE_ITEM(counta,countb,center,normal,gentle) \
+                               case counta: \
+                                       Send_Notification(NOTIF_ONE, attacker, MSG_ANNCE, ANNCE_KILLSTREAK_##countb); \
+                                       if (!warmup_stage) \
+                                               PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_KILL_SPREE_##counta, 1); \
+                                       break;
+                       switch(CS(attacker).killcount)
+                       {
+                               KILL_SPREE_LIST
+                               default: break;
+                       }
+                       #undef SPREE_ITEM
+                       if(!warmup_stage && !checkrules_firstblood)
+                       {
+                               checkrules_firstblood = true;
+                               notif_firstblood = true; // modify the current messages so that they too show firstblood information
+                               PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_FIRSTBLOOD, 1);
+                               PlayerStats_GameReport_Event_Player(targ, PLAYERSTATS_ACHIEVEMENT_FIRSTVICTIM, 1);
+                               // tell spree_inf and spree_cen that this is a first-blood and first-victim event
+                               kill_count_to_attacker = -1;
+                               kill_count_to_target = -2;
+                       }
+                       else
+                       {
+                               kill_count_to_attacker = CS(attacker).killcount;
+                               kill_count_to_target = 0;
+                       }
+                       if(targ.istypefrag)
+                       {
+                               Send_Notification(
+                                       NOTIF_ONE,
+                                       attacker,
+                                       MSG_CHOICE,
+                                       CHOICE_TYPEFRAG,
+                                       targ.netname,
+                                       kill_count_to_attacker,
+                                       (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
+                               );
+                               Send_Notification(
+                                       NOTIF_ONE,
+                                       targ,
+                                       MSG_CHOICE,
+                                       CHOICE_TYPEFRAGGED,
+                                       attacker_name,
+                                       kill_count_to_target,
+                                       GetResource(attacker, RES_HEALTH),
+                                       GetResource(attacker, RES_ARMOR),
+                                       (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
+                               );
+                       }
+                       else if(!frag_centermessage_override(attacker, targ, deathtype, kill_count_to_attacker, kill_count_to_target, attacker_name))
+                       {
+                               Send_Notification(
+                                       NOTIF_ONE,
+                                       attacker,
+                                       MSG_CHOICE,
+                                       CHOICE_FRAG,
+                                       targ.netname,
+                                       kill_count_to_attacker,
+                                       (IS_BOT_CLIENT(targ) ? -1 : CS(targ).ping)
+                               );
+                               Send_Notification(
+                                       NOTIF_ONE,
+                                       targ,
+                                       MSG_CHOICE,
+                                       CHOICE_FRAGGED,
+                                       attacker_name,
+                                       kill_count_to_target,
+                                       GetResource(attacker, RES_HEALTH),
+                                       GetResource(attacker, RES_ARMOR),
+                                       (IS_BOT_CLIENT(attacker) ? -1 : CS(attacker).ping)
+                               );
+                       }
+                       int f3 = 0;
+                       if(deathtype == DEATH_BUFF.m_id)
+                               f3 = buff_FirstFromFlags(STAT(BUFFS, attacker)).m_id;
+                       if (!Obituary_WeaponDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker))
+                               Obituary_SpecialDeath(targ, true, deathtype, targ.netname, attacker_name, deathlocation, CS(targ).killcount, kill_count_to_attacker, f3);
+               }
+       }
+       // =============
+       // ACCIDENT/TRAP
+       // =============
+       else
+       {
+               switch(DEATH_ENT(deathtype))
+               {
+                       // For now, we're just forcing HURTTRIGGER to behave as "DEATH_VOID" and giving it no special options...
+                       // Later on you will only be able to make custom messages using DEATH_CUSTOM,
+                       // and there will be a REAL DEATH_VOID implementation which mappers will use.
+                       case DEATH_HURTTRIGGER:
+                       {
+                               Obituary_SpecialDeath(targ, false, deathtype,
+                                       targ.netname,
+                                       inflictor.message,
+                                       deathlocation,
+                                       CS(targ).killcount,
+                                       0,
+                                       0);
+                               break;
+                       }
+                       case DEATH_CUSTOM:
+                       {
+                               Obituary_SpecialDeath(targ, false, deathtype,
+                                       targ.netname,
+                                       ((strstrofs(deathmessage, "%", 0) < 0) ? strcat("%s ", deathmessage) : deathmessage),
+                                       deathlocation,
+                                       CS(targ).killcount,
+                                       0,
+                                       0);
+                               break;
+                       }
+                       default:
+                       {
+                               Obituary_SpecialDeath(targ, false, deathtype, targ.netname, deathlocation, "", CS(targ).killcount, 0, 0);
+                               break;
+                       }
+               }
+               LogDeath("accident", deathtype, targ, targ);
+               GiveFrags(targ, targ, -1, deathtype, weaponentity);
+               if(GameRules_scoring_add(targ, SCORE, 0) == -5)
+               {
+                       Send_Notification(NOTIF_ONE, targ, MSG_ANNCE, ANNCE_ACHIEVEMENT_BOTLIKE);
+                       if (!warmup_stage)
+                       {
+                               PlayerStats_GameReport_Event_Player(attacker, PLAYERSTATS_ACHIEVEMENT_BOTLIKE, 1);
+                       }
+               }
+       }
+       // reset target kill count
+       CS(targ).killcount = 0;
+ }
+ void Ice_Think(entity this)
+ {
+       if(!STAT(FROZEN, this.owner) || this.owner.iceblock != this)
+       {
+               delete(this);
+               return;
+       }
+       vector ice_org = this.owner.origin - '0 0 16';
+       if (this.origin != ice_org)
+               setorigin(this, ice_org);
+       this.nextthink = time;
+ }
+ void Freeze(entity targ, float revivespeed, int frozen_type, bool show_waypoint)
+ {
+       if(!IS_PLAYER(targ) && !IS_MONSTER(targ)) // TODO: only specified entities can be freezed
+               return;
+       if(STAT(FROZEN, targ))
+               return;
+       float targ_maxhealth = ((IS_MONSTER(targ)) ? targ.max_health : start_health);
+       STAT(FROZEN, targ) = frozen_type;
+       STAT(REVIVE_PROGRESS, targ) = ((frozen_type == FROZEN_TEMP_DYING) ? 1 : 0);
+       SetResource(targ, RES_HEALTH, ((frozen_type == FROZEN_TEMP_DYING) ? targ_maxhealth : 1));
+       targ.revive_speed = revivespeed;
+       if(targ.bot_attack)
+               IL_REMOVE(g_bot_targets, targ);
+       targ.bot_attack = false;
+       targ.freeze_time = time;
+       entity ice = new(ice);
+       ice.owner = targ;
+       ice.scale = targ.scale;
+       // set_movetype(ice, MOVETYPE_FOLLOW) would rotate the ice model with the player
+       setthink(ice, Ice_Think);
+       ice.nextthink = time;
+       ice.frame = floor(random() * 21); // ice model has 20 different looking frames
+       setmodel(ice, MDL_ICE);
+       ice.alpha = 1;
+       ice.colormod = Team_ColorRGB(targ.team);
+       ice.glowmod = ice.colormod;
+       targ.iceblock = ice;
+       targ.revival_time = 0;
+       Ice_Think(ice);
+       RemoveGrapplingHooks(targ);
+       FOREACH_CLIENT(IS_PLAYER(it),
+       {
+               for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
+           {
+               .entity weaponentity = weaponentities[slot];
+               if(it.(weaponentity).hook.aiment == targ)
+                       RemoveHook(it.(weaponentity).hook);
+           }
+       });
+       // add waypoint
+       if(MUTATOR_CALLHOOK(Freeze, targ, revivespeed, frozen_type) || show_waypoint)
+               WaypointSprite_Spawn(WP_Frozen, 0, 0, targ, '0 0 64', NULL, targ.team, targ, waypointsprite_attached, true, RADARICON_WAYPOINT);
+ }
+ void Unfreeze(entity targ, bool reset_health)
+ {
+       if(!STAT(FROZEN, targ))
+               return;
+       if (reset_health && STAT(FROZEN, targ) != FROZEN_TEMP_DYING)
+               SetResource(targ, RES_HEALTH, ((IS_PLAYER(targ)) ? start_health : targ.max_health));
+       targ.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
+       STAT(FROZEN, targ) = 0;
+       STAT(REVIVE_PROGRESS, targ) = 0;
+       targ.revival_time = time;
+       if(!targ.bot_attack)
+               IL_PUSH(g_bot_targets, targ);
+       targ.bot_attack = true;
+       WaypointSprite_Kill(targ.waypointsprite_attached);
+       FOREACH_CLIENT(IS_PLAYER(it),
+       {
+               for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
+           {
+               .entity weaponentity = weaponentities[slot];
+               if(it.(weaponentity).hook.aiment == targ)
+                       RemoveHook(it.(weaponentity).hook);
+           }
+       });
+       // remove the ice block
+       if(targ.iceblock)
+               delete(targ.iceblock);
+       targ.iceblock = NULL;
+       MUTATOR_CALLHOOK(Unfreeze, targ);
+ }
+ void Damage(entity targ, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
+ {
+       float complainteamdamage = 0;
+       float mirrordamage = 0;
+       float mirrorforce = 0;
+       if (game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR))
+               return;
+       entity attacker_save = attacker;
+       // special rule: gravity bombs and sound-based attacks do not affect team mates (other than for disconnecting the hook)
+       if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || (deathtype & HITTYPE_SOUND))
+       {
+               if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker))
+               {
+                       return;
+               }
+       }
+       if(deathtype == DEATH_KILL.m_id || deathtype == DEATH_TEAMCHANGE.m_id || deathtype == DEATH_AUTOTEAMCHANGE.m_id)
+       {
+               // exit the vehicle before killing (fixes a crash)
+               if(IS_PLAYER(targ) && targ.vehicle)
+                       vehicles_exit(targ.vehicle, VHEF_RELEASE);
+               // These are ALWAYS lethal
+               // No damage modification here
+               // Instead, prepare the victim for his death...
+               SetResourceExplicit(targ, RES_ARMOR, 0);
+               targ.spawnshieldtime = 0;
+               SetResourceExplicit(targ, RES_HEALTH, 0.9); // this is < 1
+               targ.flags -= targ.flags & FL_GODMODE;
+               damage = 100000;
+       }
+       else if(deathtype == DEATH_MIRRORDAMAGE.m_id || deathtype == DEATH_NOAMMO.m_id)
+       {
+               // no processing
+       }
+       else
+       {
+               // nullify damage if teamplay is on
+               if(deathtype != DEATH_TELEFRAG.m_id)
+               if(IS_PLAYER(attacker))
+               {
+                       if(IS_PLAYER(targ) && targ != attacker && (IS_INDEPENDENT_PLAYER(attacker) || IS_INDEPENDENT_PLAYER(targ)))
+                       {
+                               damage = 0;
+                               force = '0 0 0';
+                       }
 -                              if(DIFF_TEAM(victim, attacker) && !STAT(FROZEN, victim))
++                      else if(!STAT(FROZEN, targ) && SAME_TEAM(attacker, targ))
+                       {
+                               if(autocvar_teamplay_mode == 1)
+                                       damage = 0;
+                               else if(attacker != targ)
+                               {
+                                       if(autocvar_teamplay_mode == 2)
+                                       {
+                                               if(IS_PLAYER(targ) && !IS_DEAD(targ))
+                                               {
+                                                       attacker.dmg_team = attacker.dmg_team + damage;
+                                                       complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
+                                               }
+                                       }
+                                       else if(autocvar_teamplay_mode == 3)
+                                               damage = 0;
+                                       else if(autocvar_teamplay_mode == 4)
+                                       {
+                                               if(IS_PLAYER(targ) && !IS_DEAD(targ))
+                                               {
+                                                       attacker.dmg_team = attacker.dmg_team + damage;
+                                                       complainteamdamage = attacker.dmg_team - autocvar_g_teamdamage_threshold;
+                                                       if(complainteamdamage > 0)
+                                                               mirrordamage = autocvar_g_mirrordamage * complainteamdamage;
+                                                       mirrorforce = autocvar_g_mirrordamage * vlen(force);
+                                                       damage = autocvar_g_friendlyfire * damage;
+                                                       // mirrordamage will be used LATER
+                                                       if(autocvar_g_mirrordamage_virtual)
+                                                       {
+                                                               vector v  = healtharmor_applydamage(GetResource(attacker, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, mirrordamage);
+                                                               attacker.dmg_take += v.x;
+                                                               attacker.dmg_save += v.y;
+                                                               attacker.dmg_inflictor = inflictor;
+                                                               mirrordamage = v.z;
+                                                               mirrorforce = 0;
+                                                       }
+                                                       if(autocvar_g_friendlyfire_virtual)
+                                                       {
+                                                               vector v = healtharmor_applydamage(GetResource(targ, RES_ARMOR), autocvar_g_balance_armor_blockpercent, deathtype, damage);
+                                                               targ.dmg_take += v.x;
+                                                               targ.dmg_save += v.y;
+                                                               targ.dmg_inflictor = inflictor;
+                                                               damage = 0;
+                                                               if(!autocvar_g_friendlyfire_virtual_force)
+                                                                       force = '0 0 0';
+                                                       }
+                                               }
+                                               else if(!targ.canteamdamage)
+                                                       damage = 0;
+                                       }
+                               }
+                       }
+               }
+               if (!DEATH_ISSPECIAL(deathtype))
+               {
+                       damage *= autocvar_g_weapondamagefactor;
+                       mirrordamage *= autocvar_g_weapondamagefactor;
+                       complainteamdamage *= autocvar_g_weapondamagefactor;
+                       force = force * autocvar_g_weaponforcefactor;
+                       mirrorforce *= autocvar_g_weaponforcefactor;
+               }
+               // should this be changed at all? If so, in what way?
+               MUTATOR_CALLHOOK(Damage_Calculate, inflictor, attacker, targ, deathtype, damage, mirrordamage, force, attacker.(weaponentity));
+               damage = M_ARGV(4, float);
+               mirrordamage = M_ARGV(5, float);
+               force = M_ARGV(6, vector);
+               if(IS_PLAYER(targ) && damage > 0 && attacker)
+               {
+                       for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
+                   {
+                       .entity went = weaponentities[slot];
+                       if(targ.(went).hook && targ.(went).hook.aiment == attacker)
+                               RemoveHook(targ.(went).hook);
+                   }
+               }
+               if(STAT(FROZEN, targ) && !ITEM_DAMAGE_NEEDKILL(deathtype)
+                       && deathtype != DEATH_TEAMCHANGE.m_id && deathtype != DEATH_AUTOTEAMCHANGE.m_id)
+               {
+                       if(autocvar_g_frozen_revive_falldamage > 0 && deathtype == DEATH_FALL.m_id && damage >= autocvar_g_frozen_revive_falldamage)
+                       {
+                               Unfreeze(targ, false);
+                               SetResource(targ, RES_HEALTH, autocvar_g_frozen_revive_falldamage_health);
+                               Send_Effect(EFFECT_ICEORGLASS, targ.origin, '0 0 0', 3);
+                               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_REVIVED_FALL, targ.netname);
+                               Send_Notification(NOTIF_ONE, targ, MSG_CENTER, CENTER_FREEZETAG_REVIVE_SELF);
+                       }
+                       damage = 0;
+                       force *= autocvar_g_frozen_force;
+               }
+               if(IS_PLAYER(targ) && STAT(FROZEN, targ)
+                       && ITEM_DAMAGE_NEEDKILL(deathtype) && !autocvar_g_frozen_damage_trigger)
+               {
+                       Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
+                       entity spot = SelectSpawnPoint(targ, false);
+                       if(spot)
+                       {
+                               damage = 0;
+                               targ.deadflag = DEAD_NO;
+                               targ.angles = spot.angles;
+                               targ.effects = 0;
+                               targ.effects |= EF_TELEPORT_BIT;
+                               targ.angles_z = 0; // never spawn tilted even if the spot says to
+                               targ.fixangle = true; // turn this way immediately
+                               targ.velocity = '0 0 0';
+                               targ.avelocity = '0 0 0';
+                               targ.punchangle = '0 0 0';
+                               targ.punchvector = '0 0 0';
+                               targ.oldvelocity = targ.velocity;
+                               targ.spawnorigin = spot.origin;
+                               setorigin(targ, spot.origin + '0 0 1' * (1 - targ.mins.z - 24));
+                               // don't reset back to last position, even if new position is stuck in solid
+                               targ.oldorigin = targ.origin;
+                               Send_Effect(EFFECT_TELEPORT, targ.origin, '0 0 0', 1);
+                       }
+               }
+               if(!MUTATOR_IS_ENABLED(mutator_instagib))
+               {
+                       // apply strength multiplier
+                       if (attacker.items & ITEM_Strength.m_itemid)
+                       {
+                               if(targ == attacker)
+                               {
+                                       damage = damage * autocvar_g_balance_powerup_strength_selfdamage;
+                                       force = force * autocvar_g_balance_powerup_strength_selfforce;
+                               }
+                               else
+                               {
+                                       damage = damage * autocvar_g_balance_powerup_strength_damage;
+                                       force = force * autocvar_g_balance_powerup_strength_force;
+                               }
+                       }
+                       // apply invincibility multiplier
+                       if (targ.items & ITEM_Shield.m_itemid)
+                       {
+                               damage = damage * autocvar_g_balance_powerup_invincible_takedamage;
+                               if (targ != attacker)
+                               {
+                                       force = force * autocvar_g_balance_powerup_invincible_takeforce;
+                               }
+                       }
+               }
+               if (targ == attacker)
+                       damage = damage * autocvar_g_balance_selfdamagepercent; // Partial damage if the attacker hits himself
+               // count the damage
+               if(attacker)
+               if(!IS_DEAD(targ))
+               if(deathtype != DEATH_BUFF.m_id)
+               if(targ.takedamage == DAMAGE_AIM)
+               if(targ != attacker)
+               {
+                       entity victim;
+                       if(IS_VEHICLE(targ) && targ.owner)
+                               victim = targ.owner;
+                       else
+                               victim = targ;
+                       if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker))
+                       {
 -                              else if(IS_PLAYER(attacker))
++                              if (DIFF_TEAM(victim, attacker))
+                               {
+                                       if(damage > 0)
+                                       {
+                                               if(deathtype != DEATH_FIRE.m_id)
+                                               {
+                                                       if(PHYS_INPUT_BUTTON_CHAT(victim))
+                                                               attacker.typehitsound += 1;
+                                                       else
+                                                               attacker.damage_dealt += damage;
+                                               }
+                                               damage_goodhits += 1;
+                                               damage_gooddamage += damage;
+                                               if (!DEATH_ISSPECIAL(deathtype))
+                                               {
+                                                       if(IS_PLAYER(targ)) // don't do this for vehicles
+                                                       if(IsFlying(victim))
+                                                               yoda = 1;
+                                               }
+                                       }
+                               }
 -                                      // if enemy gets frozen in this frame and receives other damage don't
 -                                      // play the typehitsound e.g. when hit by multiple bullets of the shotgun
 -                                      if (deathtype != DEATH_FIRE.m_id && (!STAT(FROZEN, victim) || time > victim.freeze_time))
++                              else if (IS_PLAYER(attacker) && !STAT(FROZEN, victim)) // same team
+                               {
++                                      if (deathtype != DEATH_FIRE.m_id)
+                                       {
+                                               attacker.typehitsound += 1;
+                                       }
+                                       if(complainteamdamage > 0)
+                                               if(time > CS(attacker).teamkill_complain)
+                                               {
+                                                       CS(attacker).teamkill_complain = time + 5;
+                                                       CS(attacker).teamkill_soundtime = time + 0.4;
+                                                       CS(attacker).teamkill_soundsource = targ;
+                                               }
+                               }
+                       }
+               }
+       }
+       // apply push
+       if (targ.damageforcescale)
+       if (force)
+       if (!IS_PLAYER(targ) || time >= targ.spawnshieldtime || targ == attacker)
+       {
+               vector farce = damage_explosion_calcpush(targ.damageforcescale * force, targ.velocity, autocvar_g_balance_damagepush_speedfactor);
+               if(targ.move_movetype == MOVETYPE_PHYSICS)
+               {
+                       entity farcent = new(farce);
+                       farcent.enemy = targ;
+                       farcent.movedir = farce * 10;
+                       if(targ.mass)
+                               farcent.movedir = farcent.movedir * targ.mass;
+                       farcent.origin = hitloc;
+                       farcent.forcetype = FORCETYPE_FORCEATPOS;
+                       farcent.nextthink = time + 0.1;
+                       setthink(farcent, SUB_Remove);
+               }
+               else if(targ.move_movetype != MOVETYPE_NOCLIP)
+               {
+                       targ.velocity = targ.velocity + farce;
+               }
+               UNSET_ONGROUND(targ);
+               UpdateCSQCProjectile(targ);
+       }
+       // apply damage
+       if (damage != 0 || (targ.damageforcescale && force))
+       if (targ.event_damage)
+               targ.event_damage (targ, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force);
+       // apply mirror damage if any
+       if(!autocvar_g_mirrordamage_onlyweapons || DEATH_WEAPONOF(deathtype) != WEP_Null)
+       if(mirrordamage > 0 || mirrorforce > 0)
+       {
+               attacker = attacker_save;
+               force = normalize(attacker.origin + attacker.view_ofs - hitloc) * mirrorforce;
+               Damage(attacker, inflictor, attacker, mirrordamage, DEATH_MIRRORDAMAGE.m_id, weaponentity, attacker.origin, force);
+       }
+ }
+ float RadiusDamageForSource (entity inflictor, vector inflictororigin, vector inflictorvelocity, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe,
+                                                               float inflictorselfdamage, float forceintensity, float forcezscale, int deathtype, .entity weaponentity, entity directhitentity)
+       // Returns total damage applies to creatures
+ {
+       entity  targ;
+       vector  force;
+       float   total_damage_to_creatures;
+       entity  next;
+       float   tfloordmg;
+       float   tfloorforce;
+       float stat_damagedone;
+       if(RadiusDamage_running)
+       {
+               backtrace("RadiusDamage called recursively! Expect stuff to go HORRIBLY wrong.");
+               return 0;
+       }
+       RadiusDamage_running = 1;
+       tfloordmg = autocvar_g_throughfloor_damage;
+       tfloorforce = autocvar_g_throughfloor_force;
+       total_damage_to_creatures = 0;
+       if(deathtype != (WEP_HOOK.m_id | HITTYPE_SECONDARY | HITTYPE_BOUNCE)) // only send gravity bomb damage once
+               if(!(deathtype & HITTYPE_SOUND)) // do not send radial sound damage (bandwidth hog)
+               {
+                       force = inflictorvelocity;
+                       if(force == '0 0 0')
+                               force = '0 0 -1';
+                       else
+                               force = normalize(force);
+                       if(forceintensity >= 0)
+                               Damage_DamageInfo(inflictororigin, coredamage, edgedamage, rad, forceintensity * force, deathtype, 0, attacker);
+                       else
+                               Damage_DamageInfo(inflictororigin, coredamage, edgedamage, -rad, (-forceintensity) * force, deathtype, 0, attacker);
+               }
+       stat_damagedone = 0;
+       targ = WarpZone_FindRadius (inflictororigin, rad + MAX_DAMAGEEXTRARADIUS, false);
+       while (targ)
+       {
+               next = targ.chain;
+               if ((targ != inflictor) || inflictorselfdamage)
+               if (((cantbe != targ) && !mustbe) || (mustbe == targ))
+               if (targ.takedamage)
+               {
+                       vector nearest;
+                       vector diff;
+                       float power;
+                       // LordHavoc: measure distance to nearest point on target (not origin)
+                       // (this guarentees 100% damage on a touch impact)
+                       nearest = targ.WarpZone_findradius_nearest;
+                       diff = targ.WarpZone_findradius_dist;
+                       // round up a little on the damage to ensure full damage on impacts
+                       // and turn the distance into a fraction of the radius
+                       power = 1 - ((vlen (diff) - bound(MIN_DAMAGEEXTRARADIUS, targ.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
+                       //bprint(" ");
+                       //bprint(ftos(power));
+                       //if (targ == attacker)
+                       //      print(ftos(power), "\n");
+                       if (power > 0)
+                       {
+                               float finaldmg;
+                               if (power > 1)
+                                       power = 1;
+                               finaldmg = coredamage * power + edgedamage * (1 - power);
+                               if (finaldmg > 0)
+                               {
+                                       float a;
+                                       float c;
+                                       vector hitloc;
+                                       vector myblastorigin;
+                                       vector center;
+                                       myblastorigin = WarpZone_TransformOrigin(targ, inflictororigin);
+                                       // if it's a player, use the view origin as reference
+                                       center = CENTER_OR_VIEWOFS(targ);
+                                       force = normalize(center - myblastorigin);
+                                       force = force * (finaldmg / coredamage) * forceintensity;
+                                       hitloc = nearest;
+                                       // apply special scaling along the z axis if set
+                                       // NOTE: 0 value is not allowed for compatibility, in the case of weapon cvars not being set
+                                       if(forcezscale)
+                                               force.z *= forcezscale;
+                                       if(targ != directhitentity)
+                                       {
+                                               float hits;
+                                               float total;
+                                               float hitratio;
+                                               float mininv_f, mininv_d;
+                                               // test line of sight to multiple positions on box,
+                                               // and do damage if any of them hit
+                                               hits = 0;
+                                               // we know: max stddev of hitratio = 1 / (2 * sqrt(n))
+                                               // so for a given max stddev:
+                                               // n = (1 / (2 * max stddev of hitratio))^2
+                                               mininv_d = (finaldmg * (1-tfloordmg)) / autocvar_g_throughfloor_damage_max_stddev;
+                                               mininv_f = (vlen(force) * (1-tfloorforce)) / autocvar_g_throughfloor_force_max_stddev;
+                                               if(autocvar_g_throughfloor_debug)
+                                                       LOG_INFOF("THROUGHFLOOR: D=%f F=%f max(dD)=1/%f max(dF)=1/%f", finaldmg, vlen(force), mininv_d, mininv_f);
+                                               total = 0.25 * (max(mininv_f, mininv_d) ** 2);
+                                               if(autocvar_g_throughfloor_debug)
+                                                       LOG_INFOF(" steps=%f", total);
+                                               if (IS_PLAYER(targ))
+                                                       total = ceil(bound(autocvar_g_throughfloor_min_steps_player, total, autocvar_g_throughfloor_max_steps_player));
+                                               else
+                                                       total = ceil(bound(autocvar_g_throughfloor_min_steps_other, total, autocvar_g_throughfloor_max_steps_other));
+                                               if(autocvar_g_throughfloor_debug)
+                                                       LOG_INFOF(" steps=%f dD=%f dF=%f", total, finaldmg * (1-tfloordmg) / (2 * sqrt(total)), vlen(force) * (1-tfloorforce) / (2 * sqrt(total)));
+                                               for(c = 0; c < total; ++c)
+                                               {
+                                                       //traceline(targ.WarpZone_findradius_findorigin, nearest, MOVE_NOMONSTERS, inflictor);
+                                                       WarpZone_TraceLine(inflictororigin, WarpZone_UnTransformOrigin(targ, nearest), MOVE_NOMONSTERS, inflictor);
+                                                       if (trace_fraction == 1 || trace_ent == targ)
+                                                       {
+                                                               ++hits;
+                                                               if (hits > 1)
+                                                                       hitloc = hitloc + nearest;
+                                                               else
+                                                                       hitloc = nearest;
+                                                       }
+                                                       nearest.x = targ.origin.x + targ.mins.x + random() * targ.size.x;
+                                                       nearest.y = targ.origin.y + targ.mins.y + random() * targ.size.y;
+                                                       nearest.z = targ.origin.z + targ.mins.z + random() * targ.size.z;
+                                               }
+                                               nearest = hitloc * (1 / max(1, hits));
+                                               hitratio = (hits / total);
+                                               a = bound(0, tfloordmg + (1-tfloordmg) * hitratio, 1);
+                                               finaldmg = finaldmg * a;
+                                               a = bound(0, tfloorforce + (1-tfloorforce) * hitratio, 1);
+                                               force = force * a;
+                                               if(autocvar_g_throughfloor_debug)
+                                                       LOG_INFOF(" D=%f F=%f", finaldmg, vlen(force));
+                                       }
+                                       //if (targ == attacker)
+                                       //{
+                                       //      print("hits ", ftos(hits), " / ", ftos(total));
+                                       //      print(" finaldmg ", ftos(finaldmg), " force ", vtos(force));
+                                       //      print(" (", ftos(a), ")\n");
+                                       //}
+                                       if(finaldmg || force)
+                                       {
+                                               if(targ.iscreature)
+                                               {
+                                                       total_damage_to_creatures += finaldmg;
+                                                       if(accuracy_isgooddamage(attacker, targ))
+                                                               stat_damagedone += finaldmg;
+                                               }
+                                               if(targ == directhitentity || DEATH_ISSPECIAL(deathtype))
+                                                       Damage(targ, inflictor, attacker, finaldmg, deathtype, weaponentity, nearest, force);
+                                               else
+                                                       Damage(targ, inflictor, attacker, finaldmg, deathtype | HITTYPE_SPLASH, weaponentity, nearest, force);
+                                       }
+                               }
+                       }
+               }
+               targ = next;
+       }
+       RadiusDamage_running = 0;
+       if(!DEATH_ISSPECIAL(deathtype))
+               accuracy_add(attacker, DEATH_WEAPONOF(deathtype), 0, min(coredamage, stat_damagedone));
+       return total_damage_to_creatures;
+ }
+ float RadiusDamage(entity inflictor, entity attacker, float coredamage, float edgedamage, float rad, entity cantbe, entity mustbe, float forceintensity, int deathtype, .entity weaponentity, entity directhitentity)
+ {
+       return RadiusDamageForSource(inflictor, (inflictor.origin + (inflictor.mins + inflictor.maxs) * 0.5), inflictor.velocity, attacker, coredamage, edgedamage, rad, 
+                                                                       cantbe, mustbe, false, forceintensity, 1, deathtype, weaponentity, directhitentity);
+ }
+ bool Heal(entity targ, entity inflictor, float amount, float limit)
+ {
+       if(game_stopped || (IS_CLIENT(targ) && CS(targ).killcount == FRAGS_SPECTATOR) || STAT(FROZEN, targ) || IS_DEAD(targ))
+               return false;
+       bool healed = false;
+       if(targ.event_heal)
+               healed = targ.event_heal(targ, inflictor, amount, limit);
+       // TODO: additional handling? what if the healing kills them? should this abort if healing would do so etc
+       // TODO: healing fx!
+       // TODO: armor healing?
+       return healed;
+ }
+ float Fire_IsBurning(entity e)
+ {
+       return (time < e.fire_endtime);
+ }
+ float Fire_AddDamage(entity e, entity o, float d, float t, float dt)
+ {
+       float dps;
+       float maxtime, mintime, maxdamage, mindamage, maxdps, mindps, totaldamage, totaltime;
+       if(IS_PLAYER(e))
+       {
+               if(IS_DEAD(e))
+                       return -1;
+       }
+       else
+       {
+               if(!e.fire_burner)
+               {
+                       // print("adding a fire burner to ", e.classname, "\n");
+                       e.fire_burner = new(fireburner);
+                       setthink(e.fire_burner, fireburner_think);
+                       e.fire_burner.nextthink = time;
+                       e.fire_burner.owner = e;
+               }
+       }
+       t = max(t, 0.1);
+       dps = d / t;
+       if(Fire_IsBurning(e))
+       {
+               mintime = e.fire_endtime - time;
+               maxtime = max(mintime, t);
+               mindps = e.fire_damagepersec;
+               maxdps = max(mindps, dps);
+               if(maxtime > mintime || maxdps > mindps)
+               {
+                       // Constraints:
+                       // damage we have right now
+                       mindamage = mindps * mintime;
+                       // damage we want to get
+                       maxdamage = mindamage + d;
+                       // but we can't exceed maxtime * maxdps!
+                       totaldamage = min(maxdamage, maxtime * maxdps);
+                       // LEMMA:
+                       // Look at:
+                       // totaldamage = min(mindamage + d, maxtime * maxdps)
+                       // We see:
+                       // totaldamage <= maxtime * maxdps
+                       // ==> totaldamage / maxdps <= maxtime.
+                       // We also see:
+                       // totaldamage / mindps = min(mindamage / mindps + d, maxtime * maxdps / mindps)
+                       //                     >= min(mintime, maxtime)
+                       // ==> totaldamage / maxdps >= mintime.
+                       /*
+                       // how long do we damage then?
+                       // at least as long as before
+                       // but, never exceed maxdps
+                       totaltime = max(mintime, totaldamage / maxdps); // always <= maxtime due to lemma
+                       */
+                       // alternate:
+                       // at most as long as maximum allowed
+                       // but, never below mindps
+                       totaltime = min(maxtime, totaldamage / mindps); // always >= mintime due to lemma
+                       // assuming t > mintime, dps > mindps:
+                       // we get d = t * dps = maxtime * maxdps
+                       // totaldamage = min(maxdamage, maxtime * maxdps) = min(... + d, maxtime * maxdps) = maxtime * maxdps
+                       // totaldamage / maxdps = maxtime
+                       // totaldamage / mindps > totaldamage / maxdps = maxtime
+                       // FROM THIS:
+                       // a) totaltime = max(mintime, maxtime) = maxtime
+                       // b) totaltime = min(maxtime, totaldamage / maxdps) = maxtime
+                       // assuming t <= mintime:
+                       // we get maxtime = mintime
+                       // a) totaltime = max(mintime, ...) >= mintime, also totaltime <= maxtime by the lemma, therefore totaltime = mintime = maxtime
+                       // b) totaltime = min(maxtime, ...) <= maxtime, also totaltime >= mintime by the lemma, therefore totaltime = mintime = maxtime
+                       // assuming dps <= mindps:
+                       // we get mindps = maxdps.
+                       // With this, the lemma says that mintime <= totaldamage / mindps = totaldamage / maxdps <= maxtime.
+                       // a) totaltime = max(mintime, totaldamage / maxdps) = totaldamage / maxdps
+                       // b) totaltime = min(maxtime, totaldamage / mindps) = totaldamage / maxdps
+                       e.fire_damagepersec = totaldamage / totaltime;
+                       e.fire_endtime = time + totaltime;
+                       if(totaldamage > 1.2 * mindamage)
+                       {
+                               e.fire_deathtype = dt;
+                               if(e.fire_owner != o)
+                               {
+                                       e.fire_owner = o;
+                                       e.fire_hitsound = false;
+                               }
+                       }
+                       if(accuracy_isgooddamage(o, e))
+                               accuracy_add(o, DEATH_WEAPONOF(dt), 0, max(0, totaldamage - mindamage));
+                       return max(0, totaldamage - mindamage); // can never be negative, but to make sure
+               }
+               else
+                       return 0;
+       }
+       else
+       {
+               e.fire_damagepersec = dps;
+               e.fire_endtime = time + t;
+               e.fire_deathtype = dt;
+               e.fire_owner = o;
+               e.fire_hitsound = false;
+               if(accuracy_isgooddamage(o, e))
+                       accuracy_add(o, DEATH_WEAPONOF(dt), 0, d);
+               return d;
+       }
+ }
+ void Fire_ApplyDamage(entity e)
+ {
+       float t, d, hi, ty;
+       entity o;
+       if (!Fire_IsBurning(e))
+               return;
+       for(t = 0, o = e.owner; o.owner && t < 16; o = o.owner, ++t);
+       if(IS_NOT_A_CLIENT(o))
+               o = e.fire_owner;
+       // water and slime stop fire
+       if(e.waterlevel)
+       if(e.watertype != CONTENT_LAVA)
+               e.fire_endtime = 0;
+       // ice stops fire
+       if(STAT(FROZEN, e))
+               e.fire_endtime = 0;
+       t = min(frametime, e.fire_endtime - time);
+       d = e.fire_damagepersec * t;
+       hi = e.fire_owner.damage_dealt;
+       ty = e.fire_owner.typehitsound;
+       Damage(e, e, e.fire_owner, d, e.fire_deathtype, DMG_NOWEP, e.origin, '0 0 0');
+       if(e.fire_hitsound && e.fire_owner)
+       {
+               e.fire_owner.damage_dealt = hi;
+               e.fire_owner.typehitsound = ty;
+       }
+       e.fire_hitsound = true;
+       if(!IS_INDEPENDENT_PLAYER(e) && !STAT(FROZEN, e))
+       {
+               IL_EACH(g_damagedbycontents, it.damagedbycontents && it != e,
+               {
+                       if(!IS_DEAD(it) && it.takedamage && !IS_INDEPENDENT_PLAYER(it))
+                       if(boxesoverlap(e.absmin, e.absmax, it.absmin, it.absmax))
+                       {
+                               t = autocvar_g_balance_firetransfer_time * (e.fire_endtime - time);
+                               d = autocvar_g_balance_firetransfer_damage * e.fire_damagepersec * t;
+                               Fire_AddDamage(it, o, d, t, DEATH_FIRE.m_id);
+                       }
+               });
+       }
+ }
+ void Fire_ApplyEffect(entity e)
+ {
+       if(Fire_IsBurning(e))
+               e.effects |= EF_FLAME;
+       else
+               e.effects &= ~EF_FLAME;
+ }
+ void fireburner_think(entity this)
+ {
+       // for players, this is done in the regular loop
+       if(wasfreed(this.owner))
+       {
+               delete(this);
+               return;
+       }
+       Fire_ApplyEffect(this.owner);
+       if(!Fire_IsBurning(this.owner))
+       {
+               this.owner.fire_burner = NULL;
+               delete(this);
+               return;
+       }
+       Fire_ApplyDamage(this.owner);
+       this.nextthink = time;
+ }
diff --combined qcsrc/server/player.qc
index 0741d4156eeed381aefc78963ecabac94c66e0ff,a632aab01c7625452f5ff887bd21fb578cf43f43..1be6b62a3f2b8903775047ab2af91d2fdb758d2e
@@@ -3,14 -3,19 +3,19 @@@
  #include <common/effects/all.qh>
  #include "bot/api.qh"
  #include "cheats.qh"
+ #include "client.qh"
  #include "clientkill.qh"
- #include "g_damage.qh"
+ #include "damage.qh"
+ #include <server/mutators/_mod.qh>
+ #include "world.qh"
  #include "handicap.qh"
- #include "miscfunctions.qh"
  #include "portals.qh"
  #include "teamplay.qh"
+ #include <server/main.qh>
+ #include "weapons/common.qh"
  #include "weapons/throwing.qh"
  #include "command/common.qh"
+ #include "command/vote.qh"
  #include "../common/state.qh"
  #include "../common/anim.qh"
  #include "../common/animdecide.qh"
@@@ -18,6 -23,7 +23,7 @@@
  #include "../common/gamemodes/sv_rules.qh"
  #include "../common/deathtypes/all.qh"
  #include "../common/mapobjects/subs.qh"
+ #include <common/mapobjects/teleporters.qh>
  #include "../common/playerstats.qh"
  #include "../lib/csqcmodel/sv_model.qh"
  
@@@ -32,6 -38,7 +38,7 @@@
  #include "../common/wepent.qh"
  
  #include "weapons/weaponstats.qh"
+ #include <server/weapons/weaponsystem.qh>
  
  #include "../common/animdecide.qh"
  
@@@ -165,7 -172,7 +172,7 @@@ void player_anim(entity this
                animbits |= ANIMSTATE_FROZEN;
        if(this.move_movetype == MOVETYPE_FOLLOW)
                animbits |= ANIMSTATE_FOLLOW;
-       if(this.crouch)
+       if(IS_DUCKED(this))
                animbits |= ANIMSTATE_DUCK;
        animdecide_setstate(this, animbits, false);
        animdecide_setimplicitstate(this, IS_ONGROUND(this));
@@@ -222,92 -229,6 +229,6 @@@ void PlayerCorpseDamage(entity this, en
        }
  }
  
- void calculate_player_respawn_time(entity this)
- {
-       if(MUTATOR_CALLHOOK(CalculateRespawnTime, this))
-               return;
-       float gametype_setting_tmp;
-       float sdelay_max = GAMETYPE_DEFAULTED_SETTING(respawn_delay_max);
-       float sdelay_small = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small);
-       float sdelay_large = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large);
-       float sdelay_small_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_small_count);
-       float sdelay_large_count = GAMETYPE_DEFAULTED_SETTING(respawn_delay_large_count);
-       float waves = GAMETYPE_DEFAULTED_SETTING(respawn_waves);
-       float pcount = 1;  // Include myself whether or not team is already set right and I'm a "player".
-       if (teamplay)
-       {
-               FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
-                       if(it.team == this.team)
-                               ++pcount;
-               });
-               if (sdelay_small_count == 0)
-                       sdelay_small_count = 1;
-               if (sdelay_large_count == 0)
-                       sdelay_large_count = 1;
-       }
-       else
-       {
-               FOREACH_CLIENT(IS_PLAYER(it) && it != this, {
-                       ++pcount;
-               });
-               if (sdelay_small_count == 0)
-               {
-                       if (IS_INDEPENDENT_PLAYER(this))
-                       {
-                               // Players play independently. No point in requiring enemies.
-                               sdelay_small_count = 1;
-                       }
-                       else
-                       {
-                               // Players play AGAINST each other. Enemies required.
-                               sdelay_small_count = 2;
-                       }
-               }
-               if (sdelay_large_count == 0)
-               {
-                       if (IS_INDEPENDENT_PLAYER(this))
-                       {
-                               // Players play independently. No point in requiring enemies.
-                               sdelay_large_count = 1;
-                       }
-                       else
-                       {
-                               // Players play AGAINST each other. Enemies required.
-                               sdelay_large_count = 2;
-                       }
-               }
-       }
-       float sdelay;
-       if (pcount <= sdelay_small_count)
-               sdelay = sdelay_small;
-       else if (pcount >= sdelay_large_count)
-               sdelay = sdelay_large;
-       else  // NOTE: this case implies sdelay_large_count > sdelay_small_count.
-               sdelay = sdelay_small + (sdelay_large - sdelay_small) * (pcount - sdelay_small_count) / (sdelay_large_count - sdelay_small_count);
-       if(waves)
-               this.respawn_time = ceil((time + sdelay) / waves) * waves;
-       else
-               this.respawn_time = time + sdelay;
-       if(sdelay < sdelay_max)
-               this.respawn_time_max = time + sdelay_max;
-       else
-               this.respawn_time_max = this.respawn_time;
-       if((sdelay + waves >= 5.0) && (this.respawn_time - time > 1.75))
-               this.respawn_countdown = 10; // first number to count down from is 10
-       else
-               this.respawn_countdown = -1; // do not count down
-       if(autocvar_g_forced_respawn)
-               this.respawn_flags = this.respawn_flags | RESPAWN_FORCE;
- }
  void PlayerDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
  {
        vector v;
                        }
                }
  
 -              if (time < this.spawnshieldtime && autocvar_g_spawnshield_blockdamage < 1)
 +              if (STAT(FROZEN, this))
 +              {
 +                      if (!ITEM_DAMAGE_NEEDKILL(deathtype))
 +                              damage = 0;
 +              }
 +              else if (time < this.spawnshieldtime && autocvar_g_spawnshield_blockdamage < 1)
                        damage *= 1 - max(0, autocvar_g_spawnshield_blockdamage);
  
                if(deathtype & HITTYPE_SOUND) // sound based attacks cause bleeding from the ears
                        if(take)
                                this.pauseregen_finished = max(this.pauseregen_finished, time + autocvar_g_balance_pause_health_regen);
  
 -                      if (time > this.pain_finished)          //Don't switch pain sequences like crazy
 +                      if (time > this.pain_finished && !STAT(FROZEN, this)) // Don't switch pain sequences like crazy
                        {
                                this.pain_finished = time + 0.5;        //Supajoe
  
                        }
  
                        float realdmg = damage - excess;
 -                      if (this != attacker && realdmg)
 -                      if (!(round_handler_IsActive() && !round_handler_IsRoundStarted()) && time >= game_starttime)
 +                      if (this != attacker && realdmg && !STAT(FROZEN, this)
 +                              && (!(round_handler_IsActive() && !round_handler_IsRoundStarted()) && time >= game_starttime))
                        {
                                if (IS_PLAYER(attacker) && DIFF_TEAM(attacker, this)) {
                                        GameRules_scoring_add(attacker, DMG, realdmg);
        if(vbot || IS_REAL_CLIENT(this))
        if(abot || IS_REAL_CLIENT(attacker))
        if(attacker && this != attacker)
 -      if(DIFF_TEAM(this, attacker))
 +      if (DIFF_TEAM(this, attacker) && (!STAT(FROZEN, this) || this.freeze_time > time))
        {
                if(DEATH_ISSPECIAL(deathtype))
                        awep = attacker.(weaponentity).m_weapon;
                valid_damage_for_weaponstats = true;
        }
  
 -      dh = dh - max(GetResource(this, RES_HEALTH), 0);
 -      da = da - max(GetResource(this, RES_ARMOR), 0);
 +      dh -= max(GetResource(this, RES_HEALTH), 0); // health difference
 +      da -= max(GetResource(this, RES_ARMOR), 0); // armor difference
        if(valid_damage_for_weaponstats)
        {
                WeaponStats_LogDamage(awep.m_id, abot, this.(weaponentity).m_weapon.m_id, vbot, dh + da);
                this.avelocity = '0 0 0';
                // view from the floor
                this.view_ofs = '0 0 -8';
-               // toss the corpse
-               set_movetype(this, MOVETYPE_TOSS);
+               if(this.move_movetype == MOVETYPE_NOCLIP)
+               {
+                       // don't toss the corpse in this case, it can get stuck in solid (causing low fps)
+                       // or fall indefinitely into the void if out of the map
+                       this.velocity = '0 0 0';
+               }
+               else
+               {
+                       // toss the corpse
+                       set_movetype(this, MOVETYPE_TOSS);
+               }
                // shootable corpse
                this.solid = SOLID_CORPSE;
                PS(this).ballistics_density = autocvar_g_ballistics_density_corpse;
@@@ -684,3 -609,78 +614,78 @@@ bool PlayerHeal(entity targ, entity inf
        GiveResourceWithLimit(targ, RES_HEALTH, amount, limit);
        return true;
  }
+ void precache_playermodel(string m)
+ {
+       int globhandle, i, n;
+       string f;
+       // remove :<skinnumber> suffix
+       int j = strstrofs(m, ":", 0);
+       if(j >= 0)
+               m = substring(m, 0, j);
+       if(substring(m, -9, 5) == "_lod1")
+               return;
+       if(substring(m, -9, 5) == "_lod2")
+               return;
+       precache_model(m);
+       f = strcat(substring(m, 0, -5), "_lod1", substring(m, -4, -1));
+       if(fexists(f))
+               precache_model(f);
+       f = strcat(substring(m, 0, -5), "_lod2", substring(m, -4, -1));
+       if(fexists(f))
+               precache_model(f);
+       globhandle = search_begin(strcat(m, "_*.sounds"), true, false);
+       if (globhandle < 0)
+               return;
+       n = search_getsize(globhandle);
+       for (i = 0; i < n; ++i)
+       {
+               //print(search_getfilename(globhandle, i), "\n");
+               f = search_getfilename(globhandle, i);
+               PrecachePlayerSounds(f);
+       }
+       search_end(globhandle);
+ }
+ void precache_all_playermodels(string pattern)
+ {
+       int globhandle = search_begin(pattern, true, false);
+       if (globhandle < 0) return;
+       int n = search_getsize(globhandle);
+       for (int i = 0; i < n; ++i)
+       {
+               string s = search_getfilename(globhandle, i);
+               precache_playermodel(s);
+       }
+       search_end(globhandle);
+ }
+ void precache_playermodels(string s)
+ {
+       FOREACH_WORD(s, true, { precache_playermodel(it); });
+ }
+ PRECACHE(PlayerModels)
+ {
+     // Precache all player models if desired
+     if (autocvar_sv_precacheplayermodels)
+     {
+         PrecachePlayerSounds("sound/player/default.sounds");
+         precache_all_playermodels("models/player/*.zym");
+         precache_all_playermodels("models/player/*.dpm");
+         precache_all_playermodels("models/player/*.md3");
+         precache_all_playermodels("models/player/*.psk");
+         precache_all_playermodels("models/player/*.iqm");
+     }
+     if (autocvar_sv_defaultcharacter)
+     {
+               precache_playermodels(autocvar_sv_defaultplayermodel_red);
+               precache_playermodels(autocvar_sv_defaultplayermodel_blue);
+               precache_playermodels(autocvar_sv_defaultplayermodel_yellow);
+               precache_playermodels(autocvar_sv_defaultplayermodel_pink);
+               precache_playermodels(autocvar_sv_defaultplayermodel);
+     }
+ }