]> git.rm.cloudns.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'master' into Juhu/battle-royale
authorJuhu <5894800-Juhu_@users.noreply.gitlab.com>
Fri, 1 Jul 2022 02:46:01 +0000 (04:46 +0200)
committerJuhu <5894800-Juhu_@users.noreply.gitlab.com>
Fri, 1 Jul 2022 02:46:01 +0000 (04:46 +0200)
30 files changed:
1  2 
.gitlab-ci.yml
_hud_descriptions.cfg
balance-xdf.cfg
gamemodes-client.cfg
gamemodes-server.cfg
hud_luma.cfg
hud_luminos.cfg
hud_luminos_minimal.cfg
hud_luminos_minimal_xhair.cfg
hud_luminos_old.cfg
hud_nexuiz.cfg
qcsrc/client/csqcmodel_hooks.qc
qcsrc/client/hud/panel/scoreboard.qc
qcsrc/common/deathtypes/all.inc
qcsrc/common/ent_cs.qc
qcsrc/common/gamemodes/gamemode/br/sv_br.qc
qcsrc/common/gamemodes/gamemode/br/sv_dropship.qc
qcsrc/common/gamemodes/gamemode/br/sv_ring.qc
qcsrc/common/mutators/mutator/buffs/sv_buffs.qc
qcsrc/common/notifications/all.inc
qcsrc/common/notifications/all.qh
qcsrc/common/scores.qh
qcsrc/common/stats.qh
qcsrc/common/vehicles/sv_vehicles.qc
qcsrc/menu/xonotic/util.qc
qcsrc/server/chat.qc
qcsrc/server/client.qc
qcsrc/server/damage.qc
qcsrc/server/impulse.qc
qcsrc/server/world.qc

diff --cc .gitlab-ci.yml
index 48a269313029caefc00db00edd016ddd65db8f20,8b56b81ff39f4e9dda7e29a01cca19b8d679fd06..26bdd574e918531d4e605326bd952dc59eab30a5
@@@ -31,9 -54,9 +54,9 @@@ test_sv_game
      - wget -O data/maps/stormkeep.mapinfo https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.mapinfo\r
      - wget -O data/maps/stormkeep.waypoints https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints\r
      - wget -O data/maps/stormkeep.waypoints.cache https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints.cache\r
-     - make\r
-     - EXPECT=2ed9b21b906b98ba3e9c358b1f41d547\r
-     - HASH=$(${ENGINE} -noconfig -nohome +timestamps 1 +exec serverbench.cfg\r
\r
 -    - EXPECT=8e36763a3b4590356bcc449b2452d0ea\r
++    - EXPECT=79ebc686ac31153de17e6b8b268c3ac2\r
+     - HASH=$(${ENGINE} +timestamps 1 +exec serverbench.cfg\r
        | tee /dev/stderr\r
        | sed -e 's,^\[[^]]*\] ,,'\r
        | grep '^:'\r
Simple merge
diff --cc balance-xdf.cfg
Simple merge
index cf3e35b69b0e80138f6a69872ed1a46b7dd82776,71d272a174e7c50dcb868fa34e472d043668a250..ad35029ec3f01292d801df317b635edc6419eb72
@@@ -32,7 -32,6 +32,7 @@@ alias cl_hook_gamestart_k
  alias cl_hook_gamestart_ft
  alias cl_hook_gamestart_inv
  alias cl_hook_gamestart_duel
- alias cl_hook_gameend "rpn /cl_matchcount dup load 1 + =" // increase match count every time a game ends
 +alias cl_hook_gamestart_br
+ alias cl_hook_gameend
  alias cl_hook_shutdown
  alias cl_hook_activeweapon
Simple merge
diff --cc hud_luma.cfg
Simple merge
diff --cc hud_luminos.cfg
Simple merge
Simple merge
Simple merge
Simple merge
diff --cc hud_nexuiz.cfg
Simple merge
Simple merge
Simple merge
index c877f8862405d8bf7d3b096181fa09d0f830784e,ac42a5bb25a15644f420f482ea6e3c4f56767a71..a5b6b2b0bb9c1a0ac4431d253dd02a5b2e901132
@@@ -10,12 -10,11 +10,12 @@@ REGISTER_DEATHTYPE(GENERIC
  REGISTER_DEATHTYPE(HURTTRIGGER,             DEATH_SELF_VOID,                DEATH_MURDER_VOID,              "")
  REGISTER_DEATHTYPE(KILL,                    DEATH_SELF_SUICIDE,             NULL,                           "")
  REGISTER_DEATHTYPE(LAVA,                    DEATH_SELF_LAVA,                DEATH_MURDER_LAVA,              "")
 +REGISTER_DEATHTYPE(RING,                    DEATH_SELF_RING,                DEATH_MURDER_RING,              "")
  REGISTER_DEATHTYPE(MIRRORDAMAGE,            DEATH_SELF_BETRAYAL,            NULL,                           "")
  REGISTER_DEATHTYPE(MONSTER_MAGE,            DEATH_SELF_MON_MAGE,            DEATH_MURDER_MONSTER,           "monster")
- REGISTER_DEATHTYPE(MONSTER_SHAMBLER_CLAW,   DEATH_SELF_MON_SHAMBLER_CLAW,   DEATH_MURDER_MONSTER,           "monster")
- REGISTER_DEATHTYPE(MONSTER_SHAMBLER_SMASH,  DEATH_SELF_MON_SHAMBLER_SMASH,  DEATH_MURDER_MONSTER,           "monster")
- REGISTER_DEATHTYPE(MONSTER_SHAMBLER_ZAP,    DEATH_SELF_MON_SHAMBLER_ZAP,    DEATH_MURDER_MONSTER,           "monster")
+ REGISTER_DEATHTYPE(MONSTER_GOLEM_CLAW,      DEATH_SELF_MON_GOLEM_CLAW,      DEATH_MURDER_MONSTER,           "monster")
+ REGISTER_DEATHTYPE(MONSTER_GOLEM_SMASH,     DEATH_SELF_MON_GOLEM_SMASH,     DEATH_MURDER_MONSTER,           "monster")
+ REGISTER_DEATHTYPE(MONSTER_GOLEM_ZAP,       DEATH_SELF_MON_GOLEM_ZAP,       DEATH_MURDER_MONSTER,           "monster")
  REGISTER_DEATHTYPE(MONSTER_SPIDER,          DEATH_SELF_MON_SPIDER,          DEATH_MURDER_MONSTER,           "monster")
  REGISTER_DEATHTYPE(MONSTER_WYVERN,          DEATH_SELF_MON_WYVERN,          DEATH_MURDER_MONSTER,           "monster")
  REGISTER_DEATHTYPE(MONSTER_ZOMBIE_JUMP,     DEATH_SELF_MON_ZOMBIE_JUMP,     DEATH_MURDER_MONSTER,           "monster")
index b976d651ed8b7cf5465a2360765bfcec02ed381e,5366371458fe52fadb86e3c2d7fc3dd8b2ca44db..222e74900298be4c56dd708b2f1234ebaab368ac
@@@ -188,8 -188,7 +188,8 @@@ ENTCS_PROP(SOLID, true, sv_solid, solid
                        {
                                if (radar_showenemies) break;
                                if (SAME_TEAM(to, player)) break;
-                               if (!(IS_PLAYER(to) || to.caplayer)) break;
 +                              if (SAME_SQUAD(to, player)) break;
+                               if (!(IS_PLAYER(to) || INGAME(to))) break;
                        }
                        sf &= ENTCS_PUBLICMASK; // no private updates
                } while (0);
index 83c2f202fcbb9dceea3f223d1ae10a4b534cd4c2,0000000000000000000000000000000000000000..dee0dbfebae040aeadd04002fb9600a2fff56c1c
mode 100644,000000..100644
--- /dev/null
@@@ -1,1221 -1,0 +1,1221 @@@
- #include <server/resources.qh>
 +// battle royale
 +// author: Juhu
 +
 +#include "sv_br.qh"
 +#include <server/elimination.qh>
-     entity player = M_ARGV(0, entity);
++#include <common/resources/sv_resources.qh>
 +#include <common/mutators/base.qh>
 +
 +#define BR_KILLS_INSTANTLY(pl, dt) \
 +    (!IN_SQUAD((pl)) || (br_SquadFindLastAlive((pl).br_squad, true) == (pl)) || ((dt) == DEATH_HURTTRIGGER.m_id) \
 +    || ((dt) == DEATH_KILL.m_id) || ((dt) == DEATH_TEAMCHANGE.m_id) || ((dt) == DEATH_AUTOTEAMCHANGE.m_id) \
 +    || DEATH_ISWEAPON(dt, WEP_VAPORIZER))
 +
 +float br_CalculatePlayerDropAngle(entity this);
 +void br_LastPlayerForSquad_Notify(entity squad);
 +void br_RemovePlayer(entity player);
 +void br_Revive(entity player);
 +void br_Start();
 +bool br_CheckPlayers();
 +int br_WinningCondition();
 +
 +entity dropship;
 +
 +bool squads_colored = false;
 +
 +const float br_drop_time_secs = 1;
 +const float drop_speed_vertical_max = 0.9;
 +bool br_started = false;
 +.bool br_ring_warned;
 +.float br_drop_time;
 +.float br_force_drop_distance;
 +.int br_drop_launch;
 +.int br_drop_detached;
 +.bool br_drop_instructions;
 +.float br_ring_damage_time;
 +
 +.entity br_bleeding_inflictor;
 +.entity br_bleeding_attacker;
 +.int br_bleeding_deathtype;
 +..entity br_bleeding_weaponentity;
 +
 +// weapon set restoring for revive/drop
 +.WepSet br_wepset_old;
 +.Weapon br_weapon_prev[MAX_WEAPONSLOTS];
 +.float br_lastweapon_prev[MAX_WEAPONSLOTS];
 +
 +float autocvar_g_br_revive_health = 0.25;
 +float autocvar_g_br_bleeding_health = 0.5;
 +float autocvar_g_br_bleeding_armor = 50;
 +float autocvar_g_br_drop_damage = 0.5;
 +float autocvar_g_br_drop_speed_max = 2.5;
 +float autocvar_g_br_drop_speed_min = 1.25;
 +float autocvar_g_br_drop_speed_vertical_min = 0.1;
 +bool autocvar_g_br_squad_colors = true;
 +float autocvar_g_br_drop_accel_dive = 50;
 +float autocvar_g_br_drop_accel_turn = 600;
 +bool autocvar_g_br_startweapons = false;
 +float autocvar_g_br_squad_waypoint_distance = 1500;
 +
 +MUTATOR_HOOKFUNCTION(br, reset_map_global)
 +{
 +    dropship_path_length = 0; // this should kill the dropship
 +    dropship_path_direction = '0 0 0';
 +
 +    if(ring)
 +        delete(ring);
 +    ring = dropship = NULL;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, reset_map_players)
 +{
 +    FOREACH_CLIENT(true, {
 +        GameRules_scoring_add(it, BR_RANK, -GameRules_scoring_add(it, BR_RANK, 0));
 +        GameRules_scoring_add(it, BR_SQUAD, -GameRules_scoring_add(it, BR_SQUAD, 0));
 +        GameRules_scoring_add(it, BR_REVIVALS, -GameRules_scoring_add(it, BR_REVIVALS, 0));
 +
 +        STAT(DROP, it) = DROP_LANDED;
 +        STAT(BLEEDING, it) = false;
 +
 +        br_RemovePlayer(it);
 +
 +        it.br_wepset_old = start_weapons;
 +        for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
 +        {
 +            it.br_weapon_prev[slot] = WEP_Null;
 +            it.br_lastweapon_prev[slot] = 0;
 +        }
 +    });
 +
 +    br_SquadUpdateInfo();
 +    return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, CheckRules_World)
 +{
 +    if(!br_started && !warmup_stage && (time > game_starttime) && br_CheckPlayers())
 +        br_Start();
 +
 +    M_ARGV(0, float) = br_WinningCondition();
 +    return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, GiveFragsForKill, CBC_ORDER_FIRST)
 +{
 +    M_ARGV(2, float) = 0; // no frags counted in Battle Royale
 +    return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, ForbidPlayerScore_Clear)
 +{
 +    // don't clear player score
 +    return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, GetPlayerStatus)
 +{
 +    entity player = M_ARGV(0, entity);
 +    return IN_SQUAD(player);
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, ClientConnect)
 +{
 +    entity player = M_ARGV(0, entity);
 +
 +    STAT(SQUADCOLORS, player) = squads_colored;
 +
 +    if(ring)
 +        ring_timelimit(ring);
 +
 +    br_SquadUpdateInfo();
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, ClientDisconnect)
 +{
 +    entity player = M_ARGV(0, entity);
 +
 +    br_RemovePlayer(player);
 +    br_SquadUpdateInfo();
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, PutClientInServer)
 +{
 +    entity player = M_ARGV(0, entity);
 +
 +    if (!br_started)
 +    {
 +        if(!warmup_stage && (time > game_starttime) && br_CheckPlayers())
 +            STAT(DROP, player) = DROP_TRANSPORT; // inhibits the spawn effect when the match is about to start
 +        return false;
 +    }
 +
 +    if (IN_SQUAD(player))
 +        Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_JOIN_DEAD);
 +    else
 +        Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_JOIN_LATE);
 +
 +    TRANSMUTE(Observer, player);
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, MakePlayerObserver)
 +{
 +    entity player = M_ARGV(0, entity);
 +    bool is_forced = M_ARGV(1, bool);
 +
 +    if(is_forced && IN_SQUAD(player))
 +    {
 +        br_SquadMember_Remove(player);
 +        br_SquadUpdateInfo();
 +    }
 +
 +    if(IN_SQUAD(player))
 +    {
 +        player.frags = FRAGS_PLAYER_OUT_OF_GAME;
 +        return true;
 +    }
 +
 +    return false;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, SpectateCopy)
 +{
 +    entity spectatee = M_ARGV(0, entity);
 +    entity client = M_ARGV(1, entity);
 +
 +    STAT(DROP, client) = STAT(DROP, spectatee);
 +    STAT(BLEEDING, client) = STAT(BLEEDING, spectatee);
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, SpectateSet)
 +{
 +    entity client = M_ARGV(0, entity);
 +    entity target = M_ARGV(1, entity);
 +
 +    return (IN_SQUAD(client) && !client.br_squad.br_squad_dead && DIFF_SQUAD(client, target));
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, SpectateNext)
 +{
 +    entity client = M_ARGV(0, entity);
 +
 +    if(!IS_REAL_CLIENT(client) || !IN_SQUAD(client) || client.br_squad.br_squad_dead)
 +        return false;
 +
 +    entity new_target;
 +
 +    if(client.enemy && client.enemy.br_squad_next)
 +        new_target = client.enemy.br_squad_next;
 +    else
 +        new_target = client.br_squad.br_squad_first;
 +
 +    while((new_target == client) || IS_DEAD(new_target) || !IS_PLAYER(new_target))
 +    {
 +        new_target = new_target.br_squad_next;
 +        if(!new_target)
 +            new_target = client.br_squad.br_squad_first;
 +    }
 +    M_ARGV(1, entity) = new_target;
 +
 +    return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, SpectatePrev)
 +{
 +    entity client = M_ARGV(0, entity);
 +
 +    if(!IS_REAL_CLIENT(client) || !IN_SQUAD(client) || client.br_squad.br_squad_dead)
 +        return MUT_SPECPREV_CONTINUE;
 +
 +    entity new_target;
 +
 +    if(client.enemy && client.enemy.br_squad_prev)
 +        new_target = client.enemy.br_squad_prev;
 +    else
 +        new_target = client.br_squad.br_squad_last;
 +
 +    while((new_target == client) || IS_DEAD(new_target) || !IS_PLAYER(new_target))
 +    {
 +        new_target = new_target.br_squad_prev;
 +        if(!new_target)
 +            new_target = client.br_squad.br_squad_last;
 +    }
 +    M_ARGV(1, entity) = new_target;
 +
 +    return MUT_SPECPREV_FOUND;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, ForbidSpawn)
 +{
 +    return br_started;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, WantWeapon)
 +{
 +    if(autocvar_g_br_startweapons)
 +        return false;
 +
 +    M_ARGV(1, float) = 0;
 +    return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, SetStartItems)
 +{
 +    start_items &= ~(IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS);
 +
 +    start_health       = warmup_start_health       = cvar("g_br_start_health");
 +    start_armorvalue   = warmup_start_armorvalue   = cvar("g_br_start_armor");
 +    start_ammo_shells  = warmup_start_ammo_shells  = cvar("g_br_start_ammo_shells");
 +    start_ammo_nails   = warmup_start_ammo_nails   = cvar("g_br_start_ammo_nails");
 +    start_ammo_rockets = warmup_start_ammo_rockets = cvar("g_br_start_ammo_rockets");
 +    start_ammo_cells   = warmup_start_ammo_cells   = cvar("g_br_start_ammo_cells");
 +    start_ammo_plasma  = warmup_start_ammo_plasma  = cvar("g_br_start_ammo_plasma");
 +    start_ammo_fuel    = warmup_start_ammo_fuel    = cvar("g_br_start_ammo_fuel");
 +}
 +
 +// adjusted freezetag reviving code
 +#ifdef IN_REVIVING_RANGE
 +    #undef IN_REVIVING_RANGE
 +#endif
 +
 +#define IN_REVIVING_RANGE(player, it, revive_extra_size) \
 +    (it != player && IS_PLAYER(it) && !IS_DEAD(it) && SAME_SQUAD(it, player) \
 +    && boxesoverlap(player.absmin - revive_extra_size, player.absmax + revive_extra_size, it.absmin, it.absmax))
 +
 +MUTATOR_HOOKFUNCTION(br, PlayerPreThink, CBC_ORDER_FIRST)
 +{
 +    entity player = M_ARGV(0, entity);
 +
 +    if (game_stopped || !frametime || !IS_PLAYER(player))
 +        return true;
 +
 +    if(ring)
 +    {
 +        const float ring_damage_interval = 0.75;
 +        vector current_origin;
 +        if(!player.vehicle)
 +            current_origin = player.origin + player.view_ofs;
 +        else
 +            current_origin = player.vehicle.origin;
 +        if(vlen(current_origin - ring.origin) > ring_calculate_current_radius(ring))
 +        {
 +            if(!player.br_ring_warned)
 +            {
 +                player.br_ring_warned = true;
 +                player.br_ring_damage_time = time + ring_damage_interval;
 +                Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_BR_RING_WARN);
 +            }
 +
 +            // ring damage
 +            if (player.br_ring_damage_time < time)
 +            {
 +                if(player.vehicle) // if the player is controlling a vehicle
 +                {
 +                    if(autocvar_g_br_ring_exitvehicle)
 +                        vehicles_exit(player.vehicle, VHEF_RELEASE); // begone!
 +                    else
 +                        vehicles_damage(player.vehicle, ring, ring, 10 * ring.strength * ring_damage_interval, DEATH_RING.m_id, DMG_NOWEP, player.vehicle.origin, '0 0 0');
 +                }
 +                Damage(player, ring, ring, ring.strength * ring_damage_interval, DEATH_RING.m_id, DMG_NOWEP, player.origin, '0 0 0');
 +                player.br_ring_damage_time = time + ring_damage_interval;
 +            }
 +        }
 +        else
 +        {
 +            player.br_ring_warned = false;
 +        }
 +    }
 +
 +    if((STAT(DROP, player) == DROP_FALLING) && (player.br_drop_detached != 2) && IN_SQUAD(player) && (player != player.br_squad.br_squad_drop_leader)){
 +        // atck2 has to be released then pressed to detach
 +        if(!(STAT(PRESSED_KEYS, player) & KEY_ATCK2)){
 +            if(player.br_drop_detached == 0){
 +                player.br_drop_detached = 1;
 +            }
 +        }
 +        else{
 +            if(player.br_drop_detached == 1){
 +                player.br_drop_detached = 2;
 +                Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP);
 +            }
 +        }
 +    }
 +
 +    if(STAT(DROP, player) == DROP_TRANSPORT){
 +        if(time > (player.br_squad.br_drop_time + br_drop_time_secs))
 +        {
 +            if(!player.br_drop_instructions)
 +            {
 +                player.br_drop_instructions = true;
 +                Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_DROPSHIP);
 +            }
 +
 +            // jump has to be released then pressed to launch
 +            if(!(STAT(PRESSED_KEYS, player) & KEY_JUMP)){
 +                if(player.br_drop_launch == 0){
 +                    player.br_drop_launch = 1;
 +                }
 +            }
 +            else{
 +                if(player.br_drop_launch == 1){
 +                    player.br_drop_launch = 2;
 +                }
 +            }
 +        }
 +
 +        if(!(IS_REAL_CLIENT(player) && (player.br_drop_launch == 2)) && (dropship_path_length > player.br_squad.br_force_drop_distance)){
 +            player.velocity = dropship_path_direction * dropship_speed;
 +        }
 +        else{
 +            if(!(IN_SQUAD(player) && player.br_squad.br_squad_drop_leader))
 +            {
 +                player.effects &= ~EF_NODRAW;
 +                player.takedamage = DAMAGE_AIM;
 +                player.solid = SOLID_SLIDEBOX;
 +                if(!autocvar__notarget)
 +                    player.flags &= ~FL_NOTARGET;
 +                Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP);
 +                STAT(DROP, player) = DROP_FALLING;
 +                player.br_drop_detached = 0;
 +                float mindropspeed = PHYS_MAXAIRSPEED(player) * max(autocvar_g_br_drop_speed_min, 0); // no maxspeed_mod available here
 +                float maxdropspeed_ratio = drop_speed_vertical_max; // moving straight down is glitchy
 +                float mindropspeed_ratio = bound(0, autocvar_g_br_drop_speed_vertical_min, drop_speed_vertical_max);
 +                float pitch_view = max(player.v_angle.x, 0);
 +
 +                // pitch_view angle needs to be between 0 and 90 degrees
 +                if(pitch_view > 90)
 +                    pitch_view = 180 - pitch_view;
 +
 +                player.velocity.x = cos(player.angles.y * DEG2RAD);
 +                player.velocity.y = sin(player.angles.y * DEG2RAD);
 +                player.velocity.z = -tan(bound(asin(mindropspeed_ratio), pitch_view * DEG2RAD, asin(maxdropspeed_ratio)));
 +
 +                player.velocity = normalize(player.velocity) * mindropspeed;
 +
 +                player.angles.x = br_CalculatePlayerDropAngle(player) - 90;
 +                player.angles.y = vectoangles(vec2(player.velocity)).y + 180;
 +                player.angles.z = 180;
 +
 +                if(IN_SQUAD(player) && ((IS_REAL_CLIENT(player) && (player.br_drop_launch == 2)) || br_SquadIsBotsOnly(player.br_squad)))
 +                {
 +                    player.br_squad.br_squad_drop_leader = player;
 +
 +                    vector drop_base_offset;
 +                    drop_base_offset.x = cos((player.angles.y + 90) * DEG2RAD);
 +                    drop_base_offset.y = sin((player.angles.y + 90) * DEG2RAD);
 +                    drop_base_offset.z = 0;
 +                    drop_base_offset = drop_base_offset * vlen(vec2(player.maxs - player.mins)) + drop_base_offset * 32; // I hope individual players never get different mins/maxs
 +
 +                    vector drop_offset = drop_base_offset;
 +
 +                    FOREACH_CLIENT(IS_PLAYER(it) && (it != player) && SAME_SQUAD(it, player) && (STAT(DROP, it) == DROP_TRANSPORT), {
 +                        it.effects &= ~EF_NODRAW;
 +                        it.takedamage = DAMAGE_AIM;
 +                        it.solid = SOLID_SLIDEBOX;
 +                        if(!autocvar__notarget)
 +                            it.flags &= ~FL_NOTARGET;
 +                        Kill_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CPID_BR_DROP);
 +                        STAT(DROP, it) = DROP_FALLING;
 +                        it.br_drop_detached = 0;
 +                        Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_BR_DROP_DETACH);
 +
 +                        setorigin(it, player.origin + drop_offset); // FIXME: this can teleport players into brushes/void
 +                        drop_offset += drop_base_offset;
 +
 +                        it.velocity = player.velocity;
 +                        it.angles = player.angles;
 +                    });
 +                }
 +            }
 +        }
 +    }
 +
 +    // adjusted freezetag reviving code
 +    entity revivers_last = NULL;
 +    entity revivers_first = NULL;
 +
 +    bool player_is_reviving = false;
 +    bool player_is_being_revived = false;
 +    vector revive_extra_size = '1 1 1' * max(autocvar_g_br_revive_extra_size, 0);
 +    FOREACH_CLIENT(IS_PLAYER(it), {
 +        // check if player is reviving anyone
 +        if (STAT(BLEEDING, it))
 +        {
 +            if (STAT(BLEEDING, player))
 +                continue;
 +            if (!IN_REVIVING_RANGE(player, it, revive_extra_size))
 +                continue;
 +            player_is_reviving = true;
 +            break;
 +        }
 +
 +        if (!STAT(BLEEDING, player))
 +            continue; // both player and it are NOT bleeding
 +        if (!IN_REVIVING_RANGE(player, it, revive_extra_size))
 +            continue;
 +
 +        // found a squadmate that is reviving player
 +        if (revivers_last)
 +            revivers_last.chain = it;
 +        revivers_last = it;
 +        if (!revivers_first)
 +            revivers_first = it;
 +        player_is_being_revived = true;
 +    });
 +    if (revivers_last)
 +        revivers_last.chain = NULL;
 +
 +    if (!player_is_being_revived) // no squadmate nearby
 +    {
 +        float clearspeed = max(autocvar_g_br_revive_clearspeed, 0);
 +        if (STAT(BLEEDING, player))
 +            STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) - frametime * clearspeed, 1);
 +        else if (!player_is_reviving)
 +            STAT(REVIVE_PROGRESS, player) = 0; // reviving nobody
 +    }
 +    else // OK, there is at least one squadmate reviving us
 +    {
 +        float spd = max(autocvar_g_br_revive_speed, 0);
 +        STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) + frametime * spd, 1);
 +
 +        if(STAT(REVIVE_PROGRESS, player) >= 1)
 +        {
 +            br_Revive(player);
 +
 +            // EVERY squad mate nearby gets a point (even if multiple!)
 +            for(entity it = revivers_first; it; it = it.chain)
 +            {
 +                GameRules_scoring_add(it, BR_REVIVALS, +1);
 +            }
 +
 +            Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_BR_REVIVED, revivers_first.netname);
 +            Send_Notification(NOTIF_ONE, revivers_first, MSG_CENTER, CENTER_BR_REVIVE, player.netname);
 +            Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_BR_REVIVED, player.netname, revivers_first.netname);
 +            if(autocvar_sv_eventlog)
 +            {
 +                string revivers = "";
 +                for(entity it = revivers_first; it; it = it.chain)
 +                    revivers = strcat(revivers, ftos(it.playerid), ",");
 +                revivers = substring(revivers, 0, strlen(revivers) - 1);
 +                GameLogEcho(strcat(":br:revival:", ftos(player.playerid), ":", revivers));
 +            }
 +        }
 +
 +        for(entity it = revivers_first; it; it = it.chain)
 +            STAT(REVIVE_PROGRESS, it) = STAT(REVIVE_PROGRESS, player);
 +    }
 +
 +    if (STAT(BLEEDING, player))
 +    {
 +        entity player_wp = player.waypointsprite_attached;
 +        if (player_is_being_revived)
 +        {
 +            WaypointSprite_UpdateSprites(player_wp, WP_BRReviving, WP_Null, WP_Null);
 +            WaypointSprite_UpdateTeamRadar(player_wp, RADARICON_WAYPOINT, WP_BR_REVIVING_COLOR);
 +        }
 +        else
 +        {
 +            WaypointSprite_UpdateSprites(player_wp, WP_BRBleeding, WP_Null, WP_Null);
 +            WaypointSprite_UpdateTeamRadar(player_wp, RADARICON_WAYPOINT, WP_BR_BLEEDING_COLOR);
 +        }
 +
 +        WaypointSprite_UpdateMaxHealth(player_wp, 1);
 +        WaypointSprite_UpdateHealth(player_wp, STAT(REVIVE_PROGRESS, player));
 +    }
 +
 +    return true;
 +}
 +
 +#undef IN_REVIVING_RANGE
 +
 +MUTATOR_HOOKFUNCTION(br, PM_Physics)
 +{
 +    entity player = M_ARGV(0, entity);
 +    float maxspeed_mod = M_ARGV(1, float);
 +    float dt = M_ARGV(2, float); // tick rate
 +
 +    if(STAT(DROP, player) == DROP_TRANSPORT)
 +        return true;
 +
 +    // set the drop stat to landed on the next frame if it was set on landing
 +    if(STAT(DROP, player) == DROP_LANDING)
 +        STAT(DROP, player) = DROP_LANDED;
 +
 +    // TODO: improve dropping physics
 +    if(STAT(DROP, player) == DROP_FALLING){
 +        if(!IS_ONGROUND(player) && (player.waterlevel < WATERLEVEL_SWIMMING) && ((tracebox(player.origin, player.mins, player.maxs, player.origin - '0 0 1', MOVE_NOMONSTERS, player), trace_fraction) >= 1)) // IS_ONGROUND doesn't work if jump is held (jump is theoretically blocked until landed)
 +        {
 +            ITEMS_STAT(player) |= IT_USING_JETPACK;
 +            bool has_drop_leader = IN_SQUAD(player) && (player.br_drop_detached != 2) && (player.br_squad.br_squad_drop_leader && (STAT(DROP, player.br_squad.br_squad_drop_leader) == DROP_FALLING));
 +            bool player_is_drop_leader = has_drop_leader && (player == player.br_squad.br_squad_drop_leader);
 +            if(player_is_drop_leader || !has_drop_leader)
 +            {
 +                float maxairspeed = PHYS_MAXAIRSPEED(player) * max(maxspeed_mod, 1);
 +                float maxdropspeed = maxairspeed * max(autocvar_g_br_drop_speed_max, 0);
 +                float mindropspeed = maxairspeed * max(autocvar_g_br_drop_speed_min, 0);
 +                float maxdropspeed_ratio = drop_speed_vertical_max; // moving straight down is glitchy
 +                float mindropspeed_ratio = bound(0, autocvar_g_br_drop_speed_vertical_min, drop_speed_vertical_max);
 +                float accel_dive = max(autocvar_g_br_drop_accel_dive, 0);
 +                float accel_turn = max(autocvar_g_br_drop_accel_turn, 0);
 +                float dropspeed = vlen(player.velocity);
 +                float dropspeed_xy = vlen(vec2(player.velocity));
 +                float pitch_current = br_CalculatePlayerDropAngle(player);
 +                float pitch_view = max(player.v_angle.x, 0);
 +
 +                // pitch_view angle needs to be between 0 and 90 degrees
 +                if(pitch_view > 90)
 +                    pitch_view = 180 - pitch_view;
 +
 +                float pitch_diff = pitch_current - pitch_view;
 +                float pitch_ratio_wish = 0;
 +
 +                // calculate how much the player wants to change pitch (ratio is at least 0.1)
 +                // ratio is between -1 (looking straight down) and +1 (looking straight ahead or up)
 +                if((pitch_diff < 0) && (pitch_current < 90))
 +                    pitch_ratio_wish = bound(-1, sin(pitch_diff / (90 - pitch_current) * M_PI_2), -0.1);
 +                else if((pitch_diff > 0) && (pitch_current > 0))
 +                    pitch_ratio_wish = bound(0.1, sin(pitch_diff / pitch_current * M_PI_2), 1);
 +
 +                makevectors(player.v_angle);
 +                // horizontal wishvel as usual
 +                vector wishvel = v_forward * CS(player).movement.x + v_right * CS(player).movement.y;
 +                wishvel = normalize(wishvel) * min(1, vlen(wishvel) / maxairspeed);
 +                // vertical wishvel using forward movement and the previously calculated ratio
 +                wishvel.z = pitch_ratio_wish * bound(0, CS(player).movement.x / maxairspeed, 1);
 +                // apply turn acceleration to wishvel
 +                wishvel.x *= accel_turn;
 +                wishvel.y *= accel_turn;
 +                wishvel.z *= accel_turn;
 +                player.velocity += wishvel * dt;
 +                player.velocity = normalize(eZ * player.velocity.z + normalize(vec2(player.velocity)) * dropspeed_xy);
 +
 +                // if there is no horizontal movement point the horizontal vector towards the view direction
 +                if(vlen(vec2(player.velocity)) == 0)
 +                    player.velocity += (eX * cos(player.angles.y * DEG2RAD) + eY * sin(player.angles.y * DEG2RAD)) * sqrt(1 - pow(maxdropspeed_ratio, 2));
 +
 +                // modify mindropspeed_ratio and maxdropspeed_ratio so that the player does not rotate beyond the view angle
 +                float pitch_ratio_view = sin(pitch_view * DEG2RAD);
 +                if(pitch_ratio_wish > 0)
 +                    mindropspeed_ratio = bound(mindropspeed_ratio, pitch_ratio_view, maxdropspeed_ratio);
 +                else if(pitch_ratio_wish < 0)
 +                    maxdropspeed_ratio = bound(mindropspeed_ratio, pitch_ratio_view, maxdropspeed_ratio);
 +
 +                // constrain to vertical min/maxdropspeed
 +                if(player.velocity.z > -mindropspeed_ratio)
 +                    player.velocity.z = -mindropspeed_ratio;
 +                if(player.velocity.z < -maxdropspeed_ratio)
 +                    player.velocity.z = -maxdropspeed_ratio;
 +
 +                // adjust horizontal speed so that vertical speed + horizontal speed = maxdropspeed
 +                float dropangle = br_CalculatePlayerDropAngle(player);
 +                const float accelangle = 20;
 +                dropspeed = bound(mindropspeed, dropspeed + accel_dive * (dropangle - accelangle) / accelangle * dt, maxdropspeed);
 +                player.velocity = normalize(player.velocity) * dropspeed;
 +
 +                player.angles.x = dropangle - 90;
 +                player.angles.y = vectoangles(vec2(player.velocity)).y + 180;
 +                player.angles.z = 180;
 +
 +                if(player_is_drop_leader)
 +                {
 +                    FOREACH_CLIENT(IS_PLAYER(it) && (it != player) && SAME_SQUAD(it, player) && (it.br_drop_detached != 2) && (STAT(DROP, it) == DROP_FALLING), {
 +                        it.velocity = player.velocity;
 +                        it.angles = player.angles;
 +                    });
 +                }
 +                else if((player.br_drop_detached != 2) && IN_SQUAD(player))
 +                {
 +                    player.br_drop_detached = 2;
 +                    Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP);
 +                }
 +            }
 +            else
 +            {
 +                player.velocity = player.br_squad.br_squad_drop_leader.velocity;
 +                player.angles = player.br_squad.br_squad_drop_leader.angles; // no fixangles, only moves the player model not the player view
 +            }
 +
 +            return true;
 +        }
 +        else
 +        {
 +            if((player.br_drop_detached != 2) && IN_SQUAD(player) && (player != player.br_squad.br_squad_drop_leader))
 +            {
 +                player.br_drop_detached = 2;
 +                Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP);
 +            }
 +
 +            STAT(DROP, player) = DROP_LANDING;
 +            set_movetype(player, MOVETYPE_WALK);
 +            ITEMS_STAT(player) &= ~IT_USING_JETPACK;
 +            player.flags |= FL_PICKUPITEMS;
 +            player.dphitcontentsmask |= DPCONTENTS_BODY;
 +
 +            STAT(WEAPONS, player) = player.br_wepset_old;
 +
 +            .entity weaponentity = weaponentities[0];
 +            W_SwitchWeapon_Force(player, w_getbestweapon(player, weaponentity), weaponentity);
 +        }
 +    }
 +
 +    // injured players can't swim
 +    if(STAT(BLEEDING, player)){
 +        if(player.waterlevel >= WATERLEVEL_SWIMMING)
 +        {
 +            CS(player).movement.z = -60; // drift towards bottom
 +            player.v_angle.x = 0;
 +            player.com_in_jump = false;
 +        }
 +    }
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, Damage_Calculate)
 +{
 +    entity frag_target = M_ARGV(2, entity);
 +    float frag_deathtype = M_ARGV(3, float);
 +
 +    if(STAT(DROP, frag_target) != DROP_LANDED)
 +    {
 +        // weapon impact has no push force while dropping
 +        M_ARGV(6, vector) = '0 0 0';
 +
 +        if(STAT(DROP, frag_target) == DROP_TRANSPORT)
 +            M_ARGV(4, float) = M_ARGV(5, float) = 0; // can't take damage while on the dropship
 +        else if(DEATH_ISWEAPON(frag_deathtype, WEP_VAPORIZER)); // do not adjust vaporizer damage
 +        else
 +        {
 +            switch(frag_deathtype)
 +            {
 +                case DEATH_FALL.m_id:
 +                case DEATH_SHOOTING_STAR.m_id:
 +                    // do not take fall damage when landing from dropship
 +                    M_ARGV(4, float) = M_ARGV(5, float) = 0;
 +                    break;
 +                default:
 +                    // only take half of the usual damage
 +                    M_ARGV(4, float) *= max(autocvar_g_br_drop_damage, 0);
 +                    M_ARGV(5, float) *= max(autocvar_g_br_drop_damage, 0);
 +            }
 +        }
 +    }
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, PlayerDies, CBC_ORDER_FIRST)
 +{
 +    entity frag_attacker = M_ARGV(1, entity);
 +    entity frag_target = M_ARGV(2, entity);
 +    float frag_deathtype = M_ARGV(3, float); // float for some reason, breaks if changed to int
 +
 +    if(!IS_PLAYER(frag_target))
 +        return true;
 +
 +    if(STAT(DROP, frag_target) == DROP_TRANSPORT)
 +    {
 +        frag_target.effects &= ~EF_NODRAW;
 +        frag_target.takedamage = DAMAGE_AIM;
 +        frag_target.solid = SOLID_SLIDEBOX;
 +        if(!autocvar__notarget)
 +            frag_target.flags &= ~FL_NOTARGET;
 +        Kill_Notification(NOTIF_ONE_ONLY, frag_target, MSG_CENTER, CPID_BR_DROP);
 +    }
 +
 +    if(STAT(DROP, frag_target) == DROP_FALLING)
 +    {
 +        if((frag_target.br_drop_detached != 2) && IN_SQUAD(frag_target) && (frag_target != frag_target.br_squad.br_squad_drop_leader))
 +        {
 +            frag_target.br_drop_detached = 2;
 +            Kill_Notification(NOTIF_ONE_ONLY, frag_target, MSG_CENTER, CPID_BR_DROP);
 +        }
 +    }
 +
 +    if(STAT(DROP, frag_target) != DROP_LANDED)
 +    {
 +        set_movetype(frag_target, MOVETYPE_WALK);
 +        frag_target.dphitcontentsmask |= DPCONTENTS_BODY;
 +        STAT(WEAPONS, frag_target) = frag_target.br_wepset_old;
 +        STAT(DROP, frag_target) = DROP_LANDED;
 +    }
 +
 +    if(STAT(BLEEDING, frag_target) || BR_KILLS_INSTANTLY(frag_target, frag_deathtype))
 +    {
 +        if(STAT(BLEEDING, frag_target))
 +        {
 +            Kill_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CPID_BR_DOWN);
 +            STAT(BLEEDING, frag_target) = false;
 +
 +            // restore weapons on death to make weapon drop work
 +            STAT(WEAPONS, frag_target) = frag_target.br_wepset_old;
 +            for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
 +            {
 +                .entity weaponentity = weaponentities[slot];
 +                frag_target.(weaponentity).m_weapon = frag_target.br_weapon_prev[slot];
 +            }
 +        }
 +        WaypointSprite_Kill(frag_target.br_allywaypoint);
 +
 +        frag_target.respawn_flags = RESPAWN_SILENT | RESPAWN_FORCE;
 +        frag_target.respawn_time = time + 2;
 +        return true;
 +    }
 +
 +    frag_target.flags &= ~FL_PICKUPITEMS;
 +    RemoveGrapplingHooks(frag_target);
 +    StatusEffects_removeall(frag_target, STATUSEFFECT_REMOVE_NORMAL);
 +
 +    SetResource(frag_target, RES_HEALTH, start_health * max(autocvar_g_br_bleeding_health, 0));
 +    SetResource(frag_target, RES_ARMOR, max(autocvar_g_br_bleeding_armor, 0));
 +    Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_BR_DOWN_WAIT);
 +    STAT(BLEEDING, frag_target) = true;
 +
 +    FOREACH_CLIENT(IS_PLAYER(it),
 +    {
 +        for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
 +        {
 +            .entity weaponentity = weaponentities[slot];
 +            if(it.(weaponentity).hook.aiment == frag_target)
 +                RemoveHook(it.(weaponentity).hook);
 +        }
 +    });
 +
 +    frag_target.br_wepset_old = STAT(WEAPONS, frag_target);
 +    for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
 +    {
 +        .entity weaponentity = weaponentities[slot];
 +        frag_target.br_weapon_prev[slot] = frag_target.(weaponentity).m_switchweapon;
 +        frag_target.br_lastweapon_prev[slot] = frag_target.(weaponentity).cnt;
 +    }
 +    STAT(WEAPONS, frag_target) = '0 0 0';
 +
 +    WaypointSprite_Spawn(WP_BRBleeding, 0, 0, frag_target, '0 0 64', NULL, 0, frag_target, waypointsprite_attached, true, RADARICON_WAYPOINT);
 +
 +    if(frag_attacker == frag_target || !frag_attacker || ITEM_DAMAGE_NEEDKILL(frag_deathtype))
 +    {
 +        if(IS_PLAYER(frag_target))
 +            Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_BR_DOWN_SELF);
 +        Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_BR_DOWN_SELF, frag_target.netname);
 +    }
 +    else
 +    {
 +        if(IS_PLAYER(frag_target))
 +            Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_BR_DOWN, frag_attacker.netname);
 +        Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_BR_DOWN, frag_target.netname, frag_attacker.netname);
 +    }
 +
 +    br_SquadUpdateInfo();
 +
 +    return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, PlayerDied)
 +{
 +    entity player = M_ARGV(0, entity);
 +    if(br_started)
 +    {
 +        br_LastPlayerForSquad_Notify(player.br_squad);
 +        br_SquadUpdateInfo();
 +    }
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, ClientObituary)
 +{
 +    entity frag_inflictor = M_ARGV(0, entity);
 +    entity frag_attacker = M_ARGV(1, entity);
 +    entity frag_target = M_ARGV(2, entity);
 +    float frag_deathtype = M_ARGV(3, float); // float for some reason, breaks if changed to int
 +    //entity frag_weaponentity = M_ARGV(4, entity);
 +
 +    if(!STAT(BLEEDING, frag_target) && !BR_KILLS_INSTANTLY(frag_target, frag_deathtype))
 +    {
 +        frag_target.br_bleeding_inflictor = frag_inflictor;
 +        frag_target.br_bleeding_attacker = frag_attacker;
 +        frag_target.br_bleeding_deathtype = frag_deathtype;
 +        //frag_target.br_bleeding_weaponentity = frag_weaponentity; // TODO: get entity field
 +        return true;
 +    }
 +
 +    if(STAT(BLEEDING, frag_target) && frag_target.br_bleeding_attacker)
 +    {
 +        entity new_inflictor = frag_target.br_bleeding_inflictor;
 +        entity new_attacker = frag_target.br_bleeding_attacker;
 +        int new_deathtype = frag_target.br_bleeding_deathtype;
 +        .entity new_weaponentity = frag_target.br_bleeding_weaponentity;
 +        frag_target.br_bleeding_attacker = frag_target.br_bleeding_inflictor = NULL;
 +
 +        Obituary(new_attacker, new_inflictor, frag_target, new_deathtype, new_weaponentity);
 +        return true;
 +    }
 +
 +    return false;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, SetResource)
 +{
-     int res_type = M_ARGV(1, int);
-     float amount = M_ARGV(2, float);
++    entity player = M_ARGV(7, entity);
 +    if(!IS_PLAYER(player))
 +        return false;
 +
-     entity player = M_ARGV(0, entity);
++    entity res_type = M_ARGV(8, entity);
++    float amount = M_ARGV(9, float);
 +
 +    if(STAT(BLEEDING, player) && (res_type == RES_HEALTH || res_type == RES_ARMOR))
 +    {
 +        if(amount > GetResource(player, res_type)) // prevent the player from getting health or armor in any way
 +            return true;
 +    }
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, GetResourceLimit)
 +{
-     int res_type = M_ARGV(1, int);
++    entity player = M_ARGV(7, entity);
 +
 +    if(!IS_PLAYER(player) || !STAT(BLEEDING, player))
 +        return false;
 +
-             M_ARGV(2, float) *= max(autocvar_g_br_bleeding_health, 0);
++    entity res_type = M_ARGV(8, entity);
 +
 +    switch(res_type)
 +    {
 +        case RES_HEALTH:
-             M_ARGV(2, float) = max(autocvar_g_br_bleeding_armor, 0);
++            M_ARGV(9, float) *= max(autocvar_g_br_bleeding_health, 0);
 +            break;
 +        case RES_ARMOR:
-     reset_map(true);
++            M_ARGV(9, float) = max(autocvar_g_br_bleeding_armor, 0);
 +    }
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, PlayerRegen, CBC_ORDER_FIRST)
 +{
 +    entity player = M_ARGV(0, entity);
 +
 +    if(STAT(BLEEDING, player)){
 +        M_ARGV(7, float) = max(autocvar_g_br_bleed, 0);
 +        M_ARGV(8, float) = max(autocvar_g_br_bleedlinear, 0);
 +        M_ARGV(2, float) = M_ARGV(10, float) = 0;
 +    }
 +    else{
 +        M_ARGV(2, float) = M_ARGV(3, float) = 0; // no regeneration or rot in battle royale
 +    }
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, PlayerCanCrouch)
 +{
 +    entity player = M_ARGV(0, entity);
 +    if(STAT(BLEEDING, player))
 +        M_ARGV(1, bool) = true;
 +    else if(STAT(DROP, player) != DROP_LANDED)
 +        M_ARGV(1, bool) = false;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, PlayerJump)
 +{
 +    entity player = M_ARGV(0, entity);
 +    return STAT(BLEEDING, player) || (STAT(DROP, player) != DROP_LANDED);
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, BotShouldAttack)
 +{
 +    entity bot = M_ARGV(0, entity);
 +    entity target = M_ARGV(1, entity);
 +
 +    return SAME_SQUAD(bot, target) || (STAT(DROP, target) == DROP_TRANSPORT);
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, TurretValidateTarget)
 +{
 +    entity turret = M_ARGV(0, entity);
 +    entity target = M_ARGV(1, entity);
 +
 +    if(!br_started || SAME_SQUAD(turret, target) || (STAT(DROP, target) == DROP_TRANSPORT))
 +    {
 +        M_ARGV(3, float) = -1;
 +        return true;
 +    }
 +
 +    return false;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, AccuracyTargetValid)
 +{
 +    entity attacker = M_ARGV(0, entity);
 +    entity target = M_ARGV(1, entity);
 +
 +    if(SAME_SQUAD(attacker, target) || (STAT(DROP, target) == DROP_TRANSPORT))
 +        return MUT_ACCADD_INDIFFERENT;
 +    return MUT_ACCADD_VALID;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, CustomizeWaypoint)
 +{
 +    entity wp = M_ARGV(0, entity);
 +    entity player = M_ARGV(1, entity);
 +
 +    if(wp.owner == NULL)
 +        return true;
 +
 +    if((wp == wp.owner.br_allywaypoint) && (vdist(wp.owner.origin - player.origin, <, autocvar_g_br_squad_waypoint_distance) || STAT(BLEEDING, wp.owner)))
 +        return true;
 +
 +    if(!IS_PLAYER(player) || DIFF_SQUAD(wp.owner, player))
 +        return true;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, ClientKill)
 +{
 +    entity player = M_ARGV(0, entity);
 +
 +    if(br_started)
 +    {
 +        // no forfeiting once the game started
 +        Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_FORFEIT);
 +        return true;
 +    }
 +
 +    return false;
 +}
 +
 +MUTATOR_HOOKFUNCTION(br, ClientCommand_Spectate)
 +{
 +    entity player = M_ARGV(0, entity);
 +
 +    if(br_started)
 +    {
 +        // no forfeiting once the game started
 +        Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_FORFEIT);
 +        return MUT_SPECCMD_RETURN;
 +    }
 +    return MUT_SPECCMD_CONTINUE;
 +}
 +
 +float br_CalculatePlayerDropAngle(entity this)
 +{
 +    if(this.velocity.z < 0)
 +    {
 +        float dropspeed_xy = vlen(vec2(this.velocity));
 +        float dropspeed_z = fabs(this.velocity.z);
 +        return 90 - atan(dropspeed_xy / dropspeed_z) * RAD2DEG;
 +    }
 +
 +    return 0;
 +}
 +
 +void br_LastPlayerForSquad_Notify(entity squad)
 +{
 +    entity player = br_SquadFindLastAlive(squad, false);
 +    if(player)
 +        Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_ALONE);
 +}
 +
 +void br_RemovePlayer(entity player)
 +{
 +    br_SquadMember_Remove(player);
 +
 +    FOREACH_CLIENT((it.br_bleeding_attacker == player) || (it.br_bleeding_inflictor == player), {
 +        it.br_bleeding_attacker = it.br_bleeding_inflictor = NULL;
 +    });
 +}
 +
 +void br_Revive(entity player)
 +{
 +    if(STAT(BLEEDING, player))
 +    {
 +        Kill_Notification(NOTIF_ONE, player, MSG_CENTER, CPID_BR_DOWN);
 +        STAT(BLEEDING, player) = false;
 +    }
 +    player.flags |= FL_PICKUPITEMS;
 +    SetResource(player, RES_HEALTH, start_health * max(autocvar_g_br_revive_health, 0));
 +    SetResource(player, RES_ARMOR, 0);
 +
 +    STAT(WEAPONS, player) = player.br_wepset_old;
 +    for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
 +    {
 +        .entity weaponentity = weaponentities[slot];
 +        W_SwitchWeapon_Force(player, player.br_weapon_prev[slot], weaponentity);
 +        player.(weaponentity).cnt = player.br_lastweapon_prev[slot];
 +    }
 +
 +    player.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
 +
 +    STAT(REVIVE_PROGRESS, player) = 0;
 +    player.revival_time = time;
 +
 +    WaypointSprite_Kill(player.waypointsprite_attached);
 +
 +    br_SquadUpdateInfo();
 +}
 +
 +int br_WinningCondition()
 +{
 +    int total_squads = br_SquadUpdateInfo();
 +
 +    if ((total_squads > 1) || !br_started)
 +        return WINNING_NEVER;
 +
 +    entity winner_squad = NULL;
 +    IL_EACH(squads, !it.br_squad_dead, winner_squad = it);
 +
 +    for(entity member = winner_squad.br_squad_first; member; member = member.br_squad_next)
 +    {
 +        GameRules_scoring_add(member, BR_RANK, 1);
 +    }
 +
 +    delete(round_handler);
 +    round_handler = NULL;
 +
 +    return WINNING_YES;
 +}
 +
 +bool br_isEliminated(entity e)
 +{
 +    return (IN_SQUAD(e) && (IS_DEAD(e) || !IS_PLAYER(e)));
 +}
 +
 +bool br_CheckPlayers()
 +{
 +    total_players = 0;
 +    FOREACH_CLIENT(IS_PLAYER(it), ++total_players);
 +
 +    static int prev_players = 0;
 +    if (total_players >= autocvar_g_br_minplayers || total_players == 0)
 +    {
 +        if(prev_players > 0)
 +            Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS);
 +        prev_players = 0;
 +        return (total_players >= autocvar_g_br_minplayers);
 +    }
 +
 +    if(prev_players != total_players)
 +    {
 +        Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_PLAYERS, autocvar_g_br_minplayers - total_players);
 +        prev_players = total_players;
 +    }
 +
 +    return false;
 +}
 +
 +void br_Start(){
 +    // battle royale does not need those, besides, the timelimit won't be visible anymore after the game started
 +    cvar_set("timelimit", "0");
 +    cvar_set("fraglimit", "0");
 +    cvar_set("leadlimit", "0");
 +
++    reset_map(true, false);
 +
 +    ring = ring_initialize();
 +
 +    dropship = dropship_initialize();
 +
 +    if(!dropship)
 +    {
 +        br_started = true;
 +
 +        delete(ring);
 +        ring = NULL;
 +
 +        FOREACH_CLIENT(IS_PLAYER(it), {
 +            TRANSMUTE(Observer, it);
 +            PutClientInServer(it);
 +        });
 +        LOG_SEVERE("Failed to determine dropship route, aborting...");
 +    }
 +
 +    int num_players = 0;
 +
 +    FOREACH_CLIENT(IS_PLAYER(it), {
 +        STAT(DROP, it) = DROP_TRANSPORT;
 +        PutPlayerInServer(it);
 +
 +        it.br_wepset_old = STAT(WEAPONS, it);
 +        STAT(WEAPONS, it) = '0 0 0';
 +        for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
 +        {
 +            it.br_weapon_prev[slot] = WEP_Null;
 +            it.br_lastweapon_prev[slot] = 0;
 +        }
 +
 +        ++num_players;
 +    });
 +
 +    max_squad_size = max(autocvar_g_br_squad_size, 1);
 +    if(num_players <= max_squad_size)
 +        max_squad_size = ceil(num_players / 2);
 +
 +    for(int num_squads = 0; (num_squads * max_squad_size) < num_players; ++num_squads)
 +    {
 +        entity new_squad = new_pure(squad);
 +        new_squad.br_squad_drop_leader = NULL;
 +        new_squad.br_squad_id = num_squads + 1;
 +
 +        IL_PUSH(squads, new_squad);
 +    }
 +
 +    FOREACH_CLIENT(IS_PLAYER(it), {
 +        entity current_squad = br_SquadGetRandomAvail();
 +        br_SquadMember_Add(current_squad, it);
 +        GameRules_scoring_add(it, BR_SQUAD, current_squad.br_squad_id);
 +
 +        setorigin(it, dropship.origin + eZ * (dropship.mins.z - it.maxs.z - 64)); // FIXME: this can teleport players into brushes/void
 +        it.angles = vectoangles(dropship_path_direction) + '45 0 0';
 +        it.fixangle = true;
 +        it.velocity = '0 0 0';
 +        set_movetype(it, MOVETYPE_FLY);
 +        it.flags &= ~FL_PICKUPITEMS;
 +        it.flags |= FL_NOTARGET;
 +        it.dphitcontentsmask &= ~DPCONTENTS_BODY;
 +        it.effects |= EF_NODRAW;
 +        it.takedamage = DAMAGE_NO;
 +        it.solid = SOLID_NOT;
 +        it.br_drop_instructions = false;
 +        it.br_drop_launch = 0;
 +        UNSET_ONGROUND(it); // otherwise this isn't unset if the player drops in the same frame
 +
 +        WaypointSprite_Spawn(WP_BRAlly, 0, 0, it, '0 0 64', NULL, 0, it, br_allywaypoint, true, RADARICON_WAYPOINT);
 +    });
 +
 +    squads_colored = autocvar_g_br_squad_colors;
 +
 +    FOREACH_CLIENT(IS_REAL_CLIENT(it),
 +    {
 +        STAT(SQUADCOLORS, it) = squads_colored;
 +    });
 +
 +    IL_EACH(squads, true,
 +    {
 +        if(squads_colored)
 +        {
 +            float squad_color;
 +            squad_color = 16 * floor(random() * 15) + floor(random() * 15); // color 15 is special, don't select it as a squad color
 +
 +            for(entity member = it.br_squad_first; member; member = member.br_squad_next)
 +            {
 +                member.colormap = 1024 + squad_color;
 +            }
 +        }
 +
 +        it.br_drop_time = time;
 +
 +        float min_distance = max(autocvar_g_br_drop_distance_force, 0);
 +        if(!br_SquadIsBotsOnly(it))
 +            it.br_force_drop_distance = min_distance;
 +        else
 +            it.br_force_drop_distance = min_distance + random() * max(dropship_path_length - (min_distance + dropship_speed * br_drop_time_secs), 0);
 +    });
 +
 +    round_handler.cnt = 0; // emulate round handler round start
 +    br_started = true;
 +}
 +
 +void br_dummy_Think(entity this){}
 +
 +void br_Initialize()
 +{
 +    br_started = false;
 +    squads_colored = autocvar_g_br_squad_colors;
 +
 +    // emulate the round handler, useful because this will cause a lot of code to correctly treat the stage before the match starts
 +    round_handler = new_pure(round_handler);
 +    round_handler.count = 0;
 +    round_handler.cnt = 1;
 +    round_handler.wait = false;
 +    setthink(round_handler, br_dummy_Think);
 +
 +    EliminatedPlayers_Init(br_isEliminated);
 +}
index 767b5512102837d65cd33ac30c9dee7233b5560f,0000000000000000000000000000000000000000..01ce3bd8ef8746d37f9e3069b69e84474e62ed48
mode 100644,000000..100644
--- /dev/null
@@@ -1,174 -1,0 +1,174 @@@
-         if(!MoveToRandomLocationWithinBounds(this, world.mins, world.maxs, this.dphitcontentsmask, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 100, 8192, 1024))
 +#include "sv_dropship.qh"
 +
 +float autocvar_g_br_dropship_scale = 3;
 +vector autocvar_g_br_dropship_color = '0.5 0 0.5';
 +float autocvar_g_br_dropship_speed = -1;
 +
 +entity dropship_spawn(Vehicle info, float entity_scale, vector color);
 +void dropship_think(entity this);
 +vector dropship_getMultipliers();
 +vector dropship_seekPoint(entity this, vector orig, int axis, int direction, float multiplier);
 +
 +entity dropship_initialize()
 +{
 +    entity this = dropship_spawn(VEH_RACER, autocvar_g_br_dropship_scale, autocvar_g_br_dropship_color);
 +
 +    for(int i = 0; i < 100; ++i) // try to find a dropship path multiple times
 +    {
++        if(!MoveToRandomLocationWithinBounds(this, world.mins, world.maxs, this.dphitcontentsmask, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 100, 8192, 1024, false))
 +            continue;
 +
 +        vector mult;
 +
 +        vector startorigin;
 +        startorigin = dropship_seekPoint(this, this.origin, 2, 2, 1);
 +        startorigin = dropship_seekPoint(this, startorigin, 0, 0, 1);
 +        startorigin = dropship_seekPoint(this, startorigin, 1, 0, 1);
 +        mult = dropship_getMultipliers();
 +        startorigin = dropship_seekPoint(this, startorigin, 0, 1, mult.x);
 +        startorigin = dropship_seekPoint(this, startorigin, 1, 1, mult.y);
 +
 +        vector endorigin;
 +        mult = dropship_getMultipliers();
 +        endorigin = dropship_seekPoint(this, startorigin, 0, 1, 1 - mult.x);
 +        endorigin = dropship_seekPoint(this, endorigin,   1, 1, 1 - mult.y);
 +
 +        endorigin = startorigin + normalize(endorigin - startorigin) * vlen(vec2(world.maxs - world.mins));
 +
 +        tracebox(startorigin, this.mins, this.maxs, endorigin, MOVE_NORMAL, this);
 +        dropship_path_length = trace_fraction * vlen(endorigin - startorigin);
 +        if(dropship_path_length < (vlen(vec2(world.maxs - world.mins)) / 4)) // if the dropship path isn't atleast one quarter of the diagonal length of the map, retry, we're probably in a building
 +            continue;
 +        endorigin = trace_endpos;
 +        dropship_path_direction = normalize(endorigin - startorigin);
 +
 +        setorigin(this, startorigin);
 +        this.angles = vectoangles(dropship_path_direction);
 +        this.velocity = '0 0 0';
 +
 +        dropship_speed = autocvar_g_br_dropship_speed;
 +        // if dropship_speed is negative adjust speed dependant on map size
 +        if(dropship_speed < 0)
 +            dropship_speed = vlen(vec2(world.maxs - world.mins)) / 60; // dropship needs one minute to diagonally fly over the whole map
 +
 +        return this;
 +    }
 +
 +    delete(this);
 +    return NULL;
 +}
 +
 +entity dropship_spawn(Vehicle info, float entity_scale, vector color)
 +{
 +    entity this = new(vehicle);
 +    this.active = ACTIVE_ACTIVE;
 +
 +    _setmodel(this, info.model);
 +
 +    this.vehicle_flags |= VHF_ISVEHICLE;
 +
 +    this.takedamage             = DAMAGE_NO;
 +    this.bot_attack             = false;
 +    this.iscreature             = true;
 +    this.teleportable           = false;
 +    this.damagedbycontents      = false;
 +    this.vehicleid              = info.vehicleid;
 +    this.vehicledef             = info;
 +    this.dphitcontentsmask      = DPCONTENTS_SOLID;
 +    if(autocvar_g_playerclip_collisions)
 +        this.dphitcontentsmask |= DPCONTENTS_PLAYERCLIP;
 +    this.flags                  = FL_NOTARGET;
 +    this.nextthink              = time;
 +    setthink(this, dropship_think);
 +
 +    this.scale = entity_scale;
 +    setsize(this, info.m_mins * entity_scale, info.m_maxs * entity_scale);
 +    set_movetype(this, MOVETYPE_FLY_WORLDONLY);
 +
 +    this.colormod = color;
 +    this.alpha = 1;
 +
 +    CSQCMODEL_AUTOINIT(this);
 +
 +    return this;
 +}
 +
 +void dropship_think(entity this)
 +{
 +    this.nextthink = time;
 +
 +    if(dropship_path_length > 0){
 +        this.alpha = bound(0.01, dropship_path_length / autocvar_g_br_drop_distance_force, 1);
 +        this.velocity = dropship_path_direction * dropship_speed;
 +        dropship_path_length -= dropship_speed * frametime;
 +    }
 +    else{
 +        delete(this);
 +    }
 +
 +    CSQCMODEL_AUTOUPDATE(this);
 +}
 +
 +vector dropship_getMultipliers()
 +{
 +    vector mult;
 +    mult.x = (1 - cos(random() * 90 * DEG2RAD)) * 0.5;
 +    mult.y = min((1 - cos(random() * 90 * DEG2RAD)) * 0.5, 0.5 - mult.x);
 +    mult.z = 0;
 +    bool multswap = (random() >= 0.5);
 +    if(multswap){
 +        float tmp;
 +        tmp = mult.x;
 +        mult.x = mult.y;
 +        mult.y = tmp;
 +    }
 +
 +    return mult;
 +}
 +
 +vector dropship_seekPoint(entity this, vector orig, int axis, int direction, float multiplier)
 +{
 +    vector vec_axis;
 +    switch(axis)
 +    {
 +        default: case 0:
 +            vec_axis = eX;
 +            break;
 +        case 1:
 +            vec_axis = eY;
 +            break;
 +        case 2:
 +            vec_axis = eZ;
 +    }
 +
 +    float first_fraction;
 +    float second_fraction = 0;
 +    vector first_end;
 +    vector second_end = '0 0 0';
 +
 +    first_end = orig;
 +    first_end = first_end - first_end * vec_axis * vec_axis + world.maxs * vec_axis * vec_axis;
 +    first_fraction = (tracebox(orig, this.mins, this.maxs, first_end, MOVE_NORMAL, this), trace_fraction);
 +
 +    if(direction != 2)
 +    {
 +        second_end = orig;
 +        second_end = second_end - second_end * vec_axis * vec_axis + world.mins * vec_axis * vec_axis;
 +        second_fraction = (tracebox(orig, this.mins, this.maxs, second_end, MOVE_NORMAL, this), trace_fraction);
 +    }
 +
 +    float dist_to_edge;
 +    if(((direction == 0) && (first_fraction < second_fraction)) ||
 +       ((direction == 1) && (first_fraction > second_fraction)) ||
 +        (direction == 2))
 +    {
 +        dist_to_edge = (first_end * vec_axis - orig * vec_axis) * first_fraction * multiplier;
 +    }
 +    else
 +    {
 +        dist_to_edge = (second_end * vec_axis - orig * vec_axis) * second_fraction * multiplier;
 +    }
 +    orig = orig + dist_to_edge * vec_axis;
 +
 +    return orig;
 +}
index 815b2633516cf347b39bd6c383d361c295cfaa34,0000000000000000000000000000000000000000..2814c454afda79a29faa5d7fbb93c7370c75481f
mode 100644,000000..100644
--- /dev/null
@@@ -1,225 -1,0 +1,225 @@@
-     MoveToRandomLocationWithinBounds(this, world.mins, world.maxs, DPCONTENTS_SOLID, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 2500, 0, 0);
 +#include "sv_ring.qh"
 +
 +.float br_ring_stage;
 +.float br_ring_stage_strength[BR_RING_STAGE_MAX];
 +.float br_ring_timelimit;
 +
 +void ring_link(entity this);
 +bool ring_send(entity this, entity to, float sf);
 +void ring_think(entity this);
 +void ring_newStage(entity this);
 +bool ring_parseTiming(entity this);
 +void ring_parseStrength(entity this, bool has_invalid);
 +void ring_alignPosition(entity this);
 +
 +float autocvar_g_br_ring_duration = 150;
 +vector autocvar_g_br_ring_color = '1 0 0'; // FIXME: put visual-only cvar client side?
 +float autocvar_g_br_ring_alpha = 0.5; // FIXME: put visual-only cvar client side?
 +float autocvar_g_br_ring_fadedistance = 0.5; // FIXME: put visual-only cvar client side?
 +float autocvar_g_br_ring_fadedistance_min = 2000; // FIXME: put visual-only cvar client side?
 +float autocvar_g_br_ring_radius = -1; // useful for per map settings
 +string autocvar_g_br_ring_timing = "0.6 0.8 0.9";
 +string autocvar_g_br_ring_strength = "2.5 5 10 20 50";
 +float autocvar_g_br_ring_wait = 30;
 +float autocvar_g_br_ring_center_factor = 0.25;
 +
 +entity ring_initialize()
 +{
 +    entity this = spawn();
 +    this.netname = BR_RING_NAME;
 +
 +    setsize(this, '0 0 0', '0 0 0');
 +    set_movetype(this, MOVETYPE_NOCLIP);
 +
 +    this.classname = "ring";
 +    this.br_ring_start = time;
 +    this.br_ring_duration = max(autocvar_g_br_ring_duration, 1);
 +    this.radius = (autocvar_g_br_ring_radius <= 0) ? vlen(vec2(world.maxs - world.mins)) / 2 : autocvar_g_br_ring_radius;
 +    this.br_ring_stage = -1;
 +    this.colormod = autocvar_g_br_ring_color; // TODO: color changing ring
 +    this.alpha = this.br_ring_alpha = bound(0.01, autocvar_g_br_ring_alpha, 1);
 +    this.br_ring_fadedistance = max(autocvar_g_br_ring_fadedistance, 0);
 +    this.br_ring_fadedistance_min = max(autocvar_g_br_ring_fadedistance_min, 1);
 +    this.br_ring_stage_waittime = max(autocvar_g_br_ring_wait, 0);
 +
 +    bool has_invalid_timings = ring_parseTiming(this);
 +    ring_parseStrength(this, has_invalid_timings);
 +
 +    this.strength = this.br_ring_stage_strength[0];
++    MoveToRandomLocationWithinBounds(this, world.mins, world.maxs, DPCONTENTS_SOLID, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 2500, 0, 0, false);
 +    ring_alignPosition(this);
 +    ring_link(this);
 +
 +    return this;
 +}
 +
 +void ring_link(entity this)
 +{
 +    Net_LinkEntity(this, false, 0, ring_send);
 +    this.nextthink = this.br_ring_start + this.br_ring_stage_waittime;
 +    this.br_ring_timelimit = this.nextthink - game_starttime;
 +    ring_timelimit(this);
 +    setthink(this, ring_think);
 +}
 +
 +bool ring_send(entity this, entity to, float sf)
 +{
 +    WriteHeader(MSG_ENTITY, ENT_CLIENT_RING);
 +    WriteByte(MSG_ENTITY, sf);
 +    if(sf & BR_RING_SETUP)
 +    {
 +        WriteVector(MSG_ENTITY, this.origin);
 +        WriteCoord(MSG_ENTITY, this.br_ring_start);
 +        WriteCoord(MSG_ENTITY, this.br_ring_duration);
 +        WriteCoord(MSG_ENTITY, this.radius);
 +        WriteVector(MSG_ENTITY, this.colormod);
 +        WriteCoord(MSG_ENTITY, this.br_ring_alpha);
 +        WriteCoord(MSG_ENTITY, this.br_ring_fadedistance);
 +        WriteCoord(MSG_ENTITY, this.br_ring_fadedistance_min);
 +
 +        WriteByte(MSG_ENTITY, this.br_ring_stage_count);
 +        WriteCoord(MSG_ENTITY, this.br_ring_stage_waittime);
 +        for(int i = 0; i < (this.br_ring_stage_count + 1); ++i)
 +            WriteCoord(MSG_ENTITY, this.br_ring_stage_timing[i]);
 +    }
 +
 +    if(sf & BR_RING_MOVE)
 +    {
 +        WriteVector(MSG_ENTITY, this.origin);
 +        WriteVector(MSG_ENTITY, this.velocity);
 +    }
 +
 +    return true;
 +}
 +
 +void ring_think(entity this)
 +{
 +    float time_elapsed = time - this.br_ring_start;
 +
 +    if(time_elapsed >= (this.br_ring_duration + this.br_ring_stage_waittime * this.br_ring_stage_count))
 +    {
 +        this.velocity = '0 0 0';
 +        this.SendFlags |= BR_RING_MOVE; // not really necessary but for completeness sake
 +        this.nextthink = 0; // ring reached its final state, no further thinking required
 +    }
 +    else
 +    {
 +        for(int stage = this.br_ring_stage_count - 1; stage >= 0; --stage)
 +        {
 +            float stage_duration_current = this.br_ring_duration * this.br_ring_stage_timing[stage];
 +            stage_duration_current += this.br_ring_stage_waittime * stage;
 +
 +            float stage_duration_next = this.br_ring_duration * this.br_ring_stage_timing[stage + 1];
 +            stage_duration_next += this.br_ring_stage_waittime * (stage + 1);
 +
 +            if(time_elapsed >= (stage_duration_current + this.br_ring_stage_waittime))
 +            {
 +                if(stage != this.br_ring_stage)
 +                {
 +                    this.br_ring_stage = stage;
 +                    ring_newStage(this);
 +                    this.SendFlags |= BR_RING_MOVE;
 +
 +                    Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_BR_RING_CLOSE, stage + 1);
 +                }
 +                this.nextthink = this.br_ring_start + stage_duration_next;
 +                break;
 +            }
 +            else if(time_elapsed >= stage_duration_current)
 +            {
 +                if(vlen(this.velocity) > 0)
 +                {
 +                    this.velocity = '0 0 0';
 +                    this.SendFlags |= BR_RING_MOVE;
 +                }
 +                this.nextthink = this.br_ring_start + stage_duration_current + this.br_ring_stage_waittime;
 +                this.br_ring_timelimit = this.nextthink - game_starttime;
 +                ring_timelimit(this);
 +                break;
 +            }
 +        }
 +    }
 +}
 +
 +void ring_newStage(entity this)
 +{
 +    this.strength = this.br_ring_stage_strength[this.br_ring_stage + 1];
 +    if(this.br_ring_stage > 0) // first stage should center the ring a bit, no moving required
 +        this.velocity = (this.radius / this.br_ring_duration) * normalize(eX * (random() * 2 - 1) + eY * (random() * 2 - 1));
 +}
 +
 +bool ring_parseTiming(entity this)
 +{
 +    int num_timings = tokenize(autocvar_g_br_ring_timing);
 +    if(num_timings > (BR_RING_STAGE_MAX - 2))
 +    {
 +        LOG_INFO("too many stages defined by g_br_ring_timing");
 +        num_timings = BR_RING_STAGE_MAX - 2;
 +    }
 +    int invalid_timings = 0;
 +    for(int i = 0; i < num_timings; ++i)
 +    {
 +        float current_timing = stof(argv(i));
 +        if((current_timing > this.br_ring_stage_timing[i - invalid_timings]) && (current_timing < 1))
 +            this.br_ring_stage_timing[i + 1 - invalid_timings] = current_timing;
 +        else
 +        {
 +            ++invalid_timings;
 +            LOG_INFO("invalid timing value \"", argv(i), "\" at position ", itos(i + 1), " in g_br_ring_timing was discarded");
 +        }
 +    }
 +    this.br_ring_stage_count = num_timings + 1 - invalid_timings;
 +    this.br_ring_stage_timing[0] = 0;
 +    this.br_ring_stage_timing[this.br_ring_stage_count] = 1;
 +
 +    return (invalid_timings > 0);
 +}
 +
 +void ring_parseStrength(entity this, bool has_invalid)
 +{
 +    int num_strength = tokenize(autocvar_g_br_ring_strength);
 +    if(!has_invalid) // don't warn about this if we already got errors in the timing list
 +    {
 +        if(num_strength < (this.br_ring_stage_count + 1))
 +        {
 +            LOG_INFO("not enough strength values in g_br_ring_strength for the defined stages in g_br_ring_timing");
 +        }
 +        if(num_strength > (this.br_ring_stage_count + 1))
 +        {
 +            LOG_INFO("too many strength values in g_br_ring_strength for the defined stages in g_br_ring_timing");
 +        }
 +    }
 +    for(int i = 0; i < (this.br_ring_stage_count + 1); ++i)
 +    {
 +        float current_strength;
 +        float prev_strength = ((i > 0) ? this.br_ring_stage_strength[i - 1] : 1);
 +        if(i < num_strength)
 +            current_strength = stof(argv(i));
 +        else
 +            current_strength = prev_strength;
 +
 +        if(current_strength > 0)
 +            this.br_ring_stage_strength[i] = current_strength;
 +        else
 +        {
 +            this.br_ring_stage_strength[i] = prev_strength;
 +            LOG_INFO("invalid strength value \"", argv(i), "\" at position ", itos(i + 1), " in g_br_ring_strength replaced with preceeding value \"", ftos(prev_strength), "\"");
 +        }
 +    }
 +}
 +
 +void ring_timelimit(entity this)
 +{
 +    WriteByte(MSG_ALL, 3); // svc_updatestat
 +    WriteByte(MSG_ALL, 236); // STAT_TIMELIMIT
 +    WriteCoord(MSG_ALL, this.br_ring_timelimit / 60);
 +}
 +
 +void ring_alignPosition(entity this)
 +{
 +    float f = bound(0, autocvar_g_br_ring_center_factor, 1);
 +    vector ringorigin = world.mins + (world.maxs - world.mins) * ((0.5 - f/2) + random() * f);
 +    ringorigin.z = this.origin.z;
 +
 +    setorigin(this, ringorigin);
 +}
index 6cf30547a78ee2302e3690b33563488e18ad3371,87398b11d222367c1aeafa752eecab84b02e3f0d..2ba2a86ad5fa0dabcc4979f57b3e2f8c0bb41ba7
@@@ -300,11 -299,10 +300,11 @@@ string multiteam_info_sprintf(string in
      MSG_INFO_NOTIF(DEATH_SELF_FIRE,                         N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_death",         _("^BG%s^K1 became a bit too crispy%s%s"), _("^BG%s^K1 felt a little hot%s%s"))
      MSG_INFO_NOTIF(DEATH_SELF_GENERIC,                      N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_selfkill",      _("^BG%s^K1 died%s%s"), "")
      MSG_INFO_NOTIF(DEATH_SELF_LAVA,                         N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_lava",          _("^BG%s^K1 turned into hot slag%s%s"), _("^BG%s^K1 found a hot place%s%s"))
 +    MSG_INFO_NOTIF(DEATH_SELF_RING,                         N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_death",         _("^BG%s^K1 couldn't escape from ^K2the ring^K1%s%s"), "")
      MSG_INFO_NOTIF(DEATH_SELF_MON_MAGE,                     N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_death",         _("^BG%s^K1 was exploded by a Mage%s%s"), "")
-     MSG_INFO_NOTIF(DEATH_SELF_MON_SHAMBLER_CLAW,            N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_death",         _("^BG%s^K1's innards became outwards by a Shambler%s%s"), "")
-     MSG_INFO_NOTIF(DEATH_SELF_MON_SHAMBLER_SMASH,           N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_death",         _("^BG%s^K1 was smashed by a Shambler%s%s"), "")
-     MSG_INFO_NOTIF(DEATH_SELF_MON_SHAMBLER_ZAP,             N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_death",         _("^BG%s^K1 was zapped to death by a Shambler%s%s"), "")
+     MSG_INFO_NOTIF(DEATH_SELF_MON_GOLEM_CLAW,               N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_death",         _("^BG%s^K1's innards became outwards by a Golem%s%s"), "")
+     MSG_INFO_NOTIF(DEATH_SELF_MON_GOLEM_SMASH,              N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_death",         _("^BG%s^K1 was smashed by a Golem%s%s"), "")
+     MSG_INFO_NOTIF(DEATH_SELF_MON_GOLEM_ZAP,                N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_death",         _("^BG%s^K1 was zapped to death by a Golem%s%s"), "")
      MSG_INFO_NOTIF(DEATH_SELF_MON_SPIDER,                   N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_death",         _("^BG%s^K1 was bitten by a Spider%s%s"), "")
      MSG_INFO_NOTIF(DEATH_SELF_MON_WYVERN,                   N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_death",         _("^BG%s^K1 was fireballed by a Wyvern%s%s"), "")
      MSG_INFO_NOTIF(DEATH_SELF_MON_ZOMBIE_JUMP,              N_CONSOLE,  2, 1, "s1 s2loc spree_lost", "s1",      "notify_death",         _("^BG%s^K1 joins the Zombies%s%s"), "")
      MULTITEAM_CENTER(KEYHUNT_START,                     N_ENABLE,    0, 0, "",               CPID_KEYHUNT,           "0 0",  _("^BGYou are starting with the ^TC^TT Key"), "", KEY)
  
      MSG_CENTER_NOTIF(LMS_NOLIVES,                       N_ENABLE,    0, 0, "",               CPID_LMS,               "0 0",  _("^BGYou have no lives left, you must wait until the next match"), "")
+     MSG_CENTER_NOTIF(LMS_SPECWARN,                      N_ENABLE,    0, 0, "",               CPID_LMS,               "0 0",  _("^F4WARNING:^BG you can't rejoin this match after spectating.\nUse the same command again to spectate anyway."), "")
  
 +    MSG_CENTER_NOTIF(BR_JOIN_LATE,                      N_ENABLE,    0, 0, "",               CPID_Null,              "0 0",  _("^BGMatch already started, you must wait until the next match"), "")
 +    MSG_CENTER_NOTIF(BR_JOIN_DEAD,                      N_ENABLE,    0, 0, "",               CPID_Null,              "0 0",  _("^BGYou are dead, you must wait until the next match"), "")
 +    MSG_CENTER_NOTIF(BR_FORFEIT,                        N_ENABLE,    0, 0, "",               CPID_Null,              "0 0",  _("^BGForfeiting not possible"), "")
 +    MSG_CENTER_NOTIF(BR_DOWN_WAIT,                      N_ENABLE,    0, 0, "",               CPID_BR_DOWN,          "-1 0",  _("^F1Waiting for revive..."), "")
 +    MSG_CENTER_NOTIF(BR_DEAD_SQUAD,                     N_ENABLE,    0, 0, "",               CPID_Null,              "0 0",  _("^K1Your squad has been eliminated"), "")
 +    MSG_CENTER_NOTIF(BR_REVIVE,                         N_ENABLE,    1, 0, "s1",             CPID_Null,              "0 0",  _("^K3You revived ^BG%s"), "")
 +    MSG_CENTER_NOTIF(BR_REVIVED,                        N_ENABLE,    1, 0, "s1",             CPID_Null,              "0 0",  _("^K3You were revived by ^BG%s"), "")
 +    MSG_CENTER_NOTIF(BR_DOWN,                           N_ENABLE,    1, 0, "s1",             CPID_Null,              "0 0",  _("^K1You were downed by ^BG%s"), "")
 +    MSG_CENTER_NOTIF(BR_DOWN_SELF,                      N_ENABLE,    0, 0, "",               CPID_Null,              "0 0",  _("^K1You downed yourself"), "")
 +    MSG_CENTER_NOTIF(BR_DROPSHIP,                       N_ENABLE,    0, 0, "",               CPID_BR_DROP,           "-1 0",  _("^BG^F1Jump^BG to drop"), "")
 +    MSG_CENTER_NOTIF(BR_DROP_DETACH,                    N_ENABLE,    0, 0, "",               CPID_BR_DROP,           "-1 0",  _("^BG^F1Secondary fire^BG to detach"), "")
 +    MSG_CENTER_NOTIF(BR_RING_WARN,                      N_ENABLE,    0, 0, "",               CPID_Null,              "0 0",  _("^F4You are outside of ^F2the ring^F4, get back in fast!"), "")
 +    MSG_CENTER_NOTIF(BR_RING_CLOSE,                     N_ENABLE,    0, 1, "f1",             CPID_Null,              "0 0",  _("^F2Ring^F4 is closing! (strength: ^F1%s^F4)"), "")
 +
      MSG_CENTER_NOTIF(MISSING_TEAMS,                     N_ENABLE,    0, 1, "missing_teams",  CPID_MISSING_TEAMS,     "-1 0", _("^BGWaiting for players to join...\nNeed active players for: %s"), "")
      MSG_CENTER_NOTIF(MISSING_PLAYERS,                   N_ENABLE,    0, 1, "f1",             CPID_MISSING_PLAYERS,   "-1 0", _("^BGWaiting for %s player(s) to join..."), "")
  
      MSG_MULTI_NOTIF(DEATH_SELF_FIRE,                    N_ENABLE,  NULL,           INFO_DEATH_SELF_FIRE,                   CENTER_DEATH_SELF_FIRE)
      MSG_MULTI_NOTIF(DEATH_SELF_GENERIC,                 N_ENABLE,  NULL,           INFO_DEATH_SELF_GENERIC,                CENTER_DEATH_SELF_GENERIC)
      MSG_MULTI_NOTIF(DEATH_SELF_LAVA,                    N_ENABLE,  NULL,           INFO_DEATH_SELF_LAVA,                   CENTER_DEATH_SELF_LAVA)
 +    MSG_MULTI_NOTIF(DEATH_SELF_RING,                    N_ENABLE,  NULL,           INFO_DEATH_SELF_RING,                   CENTER_DEATH_SELF_RING)
      MSG_MULTI_NOTIF(DEATH_SELF_MON_MAGE,                N_ENABLE,  NULL,           INFO_DEATH_SELF_MON_MAGE,               CENTER_DEATH_SELF_MONSTER)
-     MSG_MULTI_NOTIF(DEATH_SELF_MON_SHAMBLER_CLAW,       N_ENABLE,  NULL,           INFO_DEATH_SELF_MON_SHAMBLER_CLAW,      CENTER_DEATH_SELF_MONSTER)
-     MSG_MULTI_NOTIF(DEATH_SELF_MON_SHAMBLER_SMASH,      N_ENABLE,  NULL,           INFO_DEATH_SELF_MON_SHAMBLER_SMASH,     CENTER_DEATH_SELF_MONSTER)
-     MSG_MULTI_NOTIF(DEATH_SELF_MON_SHAMBLER_ZAP,        N_ENABLE,  NULL,           INFO_DEATH_SELF_MON_SHAMBLER_ZAP,       CENTER_DEATH_SELF_MONSTER)
+     MSG_MULTI_NOTIF(DEATH_SELF_MON_GOLEM_CLAW,          N_ENABLE,  NULL,           INFO_DEATH_SELF_MON_GOLEM_CLAW,         CENTER_DEATH_SELF_MONSTER)
+     MSG_MULTI_NOTIF(DEATH_SELF_MON_GOLEM_SMASH,         N_ENABLE,  NULL,           INFO_DEATH_SELF_MON_GOLEM_SMASH,        CENTER_DEATH_SELF_MONSTER)
+     MSG_MULTI_NOTIF(DEATH_SELF_MON_GOLEM_ZAP,           N_ENABLE,  NULL,           INFO_DEATH_SELF_MON_GOLEM_ZAP,          CENTER_DEATH_SELF_MONSTER)
      MSG_MULTI_NOTIF(DEATH_SELF_MON_SPIDER,              N_ENABLE,  NULL,           INFO_DEATH_SELF_MON_SPIDER,             CENTER_DEATH_SELF_MONSTER)
      MSG_MULTI_NOTIF(DEATH_SELF_MON_WYVERN,              N_ENABLE,  NULL,           INFO_DEATH_SELF_MON_WYVERN,             CENTER_DEATH_SELF_MONSTER)
      MSG_MULTI_NOTIF(DEATH_SELF_MON_ZOMBIE_JUMP,         N_ENABLE,  NULL,           INFO_DEATH_SELF_MON_ZOMBIE_JUMP,        CENTER_DEATH_SELF_MONSTER)
Simple merge
Simple merge
Simple merge
Simple merge
index 227aa8ea85612e3a4c08116bee725feb0c4faf87,f98a8de8876e18ea5258fb5e0b6847234fc87de3..9c9ed148ed1d73ef9a0b12c3817cfafbf1481c89
@@@ -682,8 -669,6 +669,7 @@@ float updateCompression(
        GAMETYPE(MAPINFO_TYPE_ONSLAUGHT) \
        GAMETYPE(MAPINFO_TYPE_ASSAULT) \
        /* GAMETYPE(MAPINFO_TYPE_DUEL) */ \
-       /* GAMETYPE(MAPINFO_TYPE_INVASION) */ \
 +      /* GAMETYPE(MAPINFO_TYPE_BR) */ \
        /**/
  
  // hidden gametypes come last so indexing always works correctly
index da75146bacff6934f446b9a07e31763bc1d80d32,a73de2a1f6c58f2ed97e4303ae5179a7354b07db..37cbcdee296200c2070661264379641c4be9fa7f
@@@ -31,7 -31,7 +31,7 @@@ int Say(entity source, int teamsay, ent
                msgin = formatmessage(source, msgin);
  
        string colorstr;
-       if (!(IS_PLAYER(source) || source.caplayer || IN_SQUAD(source)))
 -      if (!(IS_PLAYER(source) || INGAME(source)))
++      if (!(IS_PLAYER(source) || INGAME(source) || IN_SQUAD(source)))
                colorstr = "^0"; // black for spectators
        else if(teamplay)
                colorstr = Team_ColorCode(source.team);
                sourcecmsgstr = cmsgstr;
        }
  
-       if (!privatesay && source && !(IS_PLAYER(source) || source.caplayer || IN_SQUAD(source)) && !game_stopped
 -      if (!privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped
++      if (!privatesay && source && !(IS_PLAYER(source) || INGAME(source) || IN_SQUAD(source)) && !game_stopped
                && (teamsay || CHAT_NOSPECTATORS()))
        {
                teamsay = -1; // spectators
                ret = 1;
        }
  
-       if (privatesay && source && !(IS_PLAYER(source) || source.caplayer || IN_SQUAD(source)) && !game_stopped
-               && (IS_PLAYER(privatesay) || privatesay.caplayer || IN_SQUAD(privatesay)) && CHAT_NOSPECTATORS())
 -      if (privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped
 -              && (IS_PLAYER(privatesay) || INGAME(privatesay)) && CHAT_NOSPECTATORS())
++      if (privatesay && source && !(IS_PLAYER(source) || INGAME(source) || IN_SQUAD(source)) && !game_stopped
++              && (IS_PLAYER(privatesay) || INGAME(privatesay) || IN_SQUAD(privatesay)) && CHAT_NOSPECTATORS())
        {
                ret = -1; // just hide the message completely
        }
                        dedicated_print(msgstr); // send to server console too
                        if(sourcecmsgstr != "")
                                centerprint(source, sourcecmsgstr);
-                       FOREACH_CLIENT((IS_PLAYER(it) || it.caplayer || IN_SQUAD(it)) && IS_REAL_CLIENT(it) && it != source && (((it.team == source.team) && !IN_SQUAD(source)) || SAME_SQUAD(it, source)) && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
 -                      FOREACH_CLIENT((IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && it.team == source.team && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
++                      FOREACH_CLIENT((IS_PLAYER(it) || INGAME(it) || IN_SQUAD(it)) && IS_REAL_CLIENT(it) && it != source && (((it.team == source.team) && !IN_SQUAD(source)) || SAME_SQUAD(it, source)) && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
                                sprint(it, msgstr);
                                if(cmsgstr != "")
                                        centerprint(it, cmsgstr);
                {
                        sprint(source, sourcemsgstr);
                        dedicated_print(msgstr); // send to server console too
-                       FOREACH_CLIENT(!(IS_PLAYER(it) || it.caplayer || IN_SQUAD(it)) && IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
 -                      FOREACH_CLIENT(!(IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
++                      FOREACH_CLIENT(!(IS_PLAYER(it) || INGAME(it) || IN_SQUAD(it)) && IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), {
                                sprint(it, msgstr);
                        });
                        event_log_msg = sprintf(":chat_spec:%d:%s", source.playerid, strreplace("\n", " ", msgin));
index 24f9ef00d7c3ae965481ac15c0b8de0a226b8453,2e6e9e6b852c84a6b6ffe5bec87524fb59e6a260..358b3de4dfa14ca16ef26daed03d8b91d44da3cc
@@@ -234,12 -235,11 +235,13 @@@ void setplayermodel(entity e, string mo
                UpdatePlayerSounds(e);
  }
  
 +bool SpectateNext(entity this);
 +
  /** putting a client as observer in the server */
- void PutObserverInServer(entity this, bool is_forced)
+ void PutObserverInServer(entity this, bool is_forced, bool use_spawnpoint)
  {
        bool mutator_returnvalue = MUTATOR_CALLHOOK(MakePlayerObserver, this, is_forced);
+       bool recount_ready = false;
        PlayerState_detach(this);
  
        if (IS_PLAYER(this))
Simple merge
Simple merge
Simple merge