Загрузка данных


#include <amxmisc>
#include <fakemeta>
#include <hamsandwich>
#include <json>
#include <reapi>
#include <bitmask>

native bio_get_user_money(id);
native bio_set_user_money(id, amount, bool:flash = true);

#if AMXX_VERSION_NUM < 190
    #assert Outdated version of AMX Mod X (AMXX_VERSION_NUM), please update in order to use this plugin properly.
#endif

#pragma semicolon 1

new const TEXT_MESSAGE[][] =
{
    "#Game_Commencing",
    "#Cstrike_Tutor_Round_Over",
    "#Round_Draw",
    "#Game_will_restart_in",
    "#CTs_Win",
    "#Terrorists_Win"
};

new const SEND_AUDIO[][] =
{
    "%!MRAD_rounddraw",
    "%!MRAD_terwin",
    "%!MRAD_ctwin"
};

new const ZOMBIE_SCREAM[][] =
{
    "biohazard/scream1.wav",
    "biohazard/scream2.wav",
    "biohazard/scream3.wav"
};

new const IMMUTABLE_CVARS[][][] =
{
    {"mp_limitteams", "0"},
    {"mp_autoteambalance", "0"},
    {"mp_round_infinite", "b"},
    {"mp_roundover", "1"},
    {"mp_auto_join_team", "1"},
    {"humans_join_team", "CT"},
    {"mp_autokick", "0"}
};

new m_cImmutableCvars[sizeof IMMUTABLE_CVARS];

enum
{
    TASKID_WARMUP
};

enum _:ClassData
{
    ClassName[32],
    Float:ClassHealth,
    Float:ClassSpeed,
    Float:ClassGravity,
    ClassModel[64],
    ClassClaw[64]
};

new Array:m_arrClassesVec = Invalid_Array;
new Array:m_arrSpawnPoints = Invalid_Array;

new m_iNumClasses;
new m_iMenuPosition[MAX_CLIENTS + 1];
new m_iLastSpawn;

new Trie:m_trUselessEntities = Invalid_Trie;
new Trie:m_trTextMessage = Invalid_Trie;
new Trie:m_trSendAudio = Invalid_Trie;

new bool:m_bWarmup = true;
new bool:m_bZombieRespawnAllowed;

new m_pCvar_FreezeTime;
new m_pCvar_RoundTime;
new m_pCvar_OverHumansWin;
new m_pCvar_ZombieRespawnTime;
new m_pCvar_ZombieRespawnMinDistance;
new m_pCvar_ZombieRespawnLimit;
new m_pCvar_ZombieRespawnChanceDecay;
new m_pCvar_MoneyDamageRatio;
new m_pCvar_MoneyInfection;

new m_fwdSpawn;

new m_varClass[MAX_CLIENTS + 1];
new m_iZombieRespawn[MAX_CLIENTS + 1];
new m_iMoney[MAX_CLIENTS + 1];

new m_bitZombie;
new m_bitZombieRespawn;


/*============================================================
                        PLUGIN
============================================================*/

public plugin_precache()
{
    register_plugin("Biohazard", "0.0.1", "oxsi. x Vision");

    m_arrClassesVec = ArrayCreate(ClassData);
    m_arrSpawnPoints = ArrayCreate(1, 0);

    m_trTextMessage = TrieCreate();
    for (new i; i < sizeof TEXT_MESSAGE; i++)
        TrieSetCell(m_trTextMessage, TEXT_MESSAGE[i], 0);

    m_trSendAudio = TrieCreate();
    for (new i; i < sizeof SEND_AUDIO; i++)
        TrieSetCell(m_trSendAudio, SEND_AUDIO[i], 0);

    for (new i; i < sizeof ZOMBIE_SCREAM; i++)
        precache_sound(ZOMBIE_SCREAM[i]);

    for (new i; i < sizeof IMMUTABLE_CVARS; i++)
    {
        m_cImmutableCvars[i] = get_cvar_pointer(IMMUTABLE_CVARS[i][0]);
        set_pcvar_string(m_cImmutableCvars[i], IMMUTABLE_CVARS[i][1]);
        hook_cvar_change(m_cImmutableCvars[i], "@Hook_ForcingCvarChange");
    }

    m_pCvar_FreezeTime = get_cvar_pointer("mp_freezetime");
    m_pCvar_RoundTime = get_cvar_pointer("mp_roundtime");

    set_pcvar_flags(
        m_pCvar_FreezeTime,
        get_pcvar_flags(m_pCvar_FreezeTime) | FCVAR_UNLOGGED
    );

    set_pcvar_flags(
        m_pCvar_RoundTime,
        get_pcvar_flags(m_pCvar_RoundTime) | FCVAR_UNLOGGED
    );

    if (m_bWarmup)
    {
        set_pcvar_num(m_pCvar_FreezeTime, 0);
        set_pcvar_float(m_pCvar_RoundTime, 20.0 / 60.0);
        set_task_ex(1.0, "@Task_Warmup", TASKID_WARMUP, .flags = SetTask_Repeat);
    }
    else
    {
        set_pcvar_num(m_pCvar_FreezeTime, 15);
        set_pcvar_float(m_pCvar_RoundTime, 180.0 / 60.0);
    }

    if (!Read_JsonField("biohazard"))
        set_fail_state("Failed to read biohazard config.");
}

public plugin_init()
{
    m_pCvar_OverHumansWin = register_cvar("bio_over_humans_win", "1");
    m_pCvar_ZombieRespawnTime = register_cvar("bio_zombie_respawn", "5.0");
    m_pCvar_ZombieRespawnMinDistance = register_cvar("bio_zombie_respawn_min_distance", "1200.0");
    m_pCvar_ZombieRespawnLimit = register_cvar("bio_zombie_respawn_limit", "4");
    m_pCvar_ZombieRespawnChanceDecay = register_cvar("bio_zombie_chance_decay", "25.0");
    m_pCvar_MoneyDamageRatio = register_cvar("bio_money_damage_ratio", "0.5");
    m_pCvar_MoneyInfection = register_cvar("bio_money_infection", "150");

    register_clcmd("say /class", "@Open_ClassesMenu");
    register_clcmd("say_team /class", "@Open_ClassesMenu");
    register_clcmd("infect", "cmd_infect");
    register_clcmd("antidote", "cmd_antidote");

    register_menucmd(
        register_menuid("@Display_ClassesMenu"),
        (1 << 0 | 1 << 1 | 1 << 2 | 1 << 3 | 1 << 4 |
         1 << 5 | 1 << 6 | 1 << 7 | 1 << 8 | 1 << 9),
        "@Handle_ClassesMenu"
    );

    register_message(get_user_msgid("TextMsg"), "@Message_TextMsg");
    register_message(get_user_msgid("SendAudio"), "@Message_SendAudio");
    register_message(get_user_msgid("Money"), "@Message_Money");

    RegisterHookChain(RH_SV_ActivateServer, "@SV_ActivateServer_Post", true);

    RegisterHookChain(RG_RoundEnd, "@RoundEnd_Pre");
    RegisterHookChain(RG_RoundEnd, "@RoundEnd_Post", true);

    RegisterHookChain(RG_CSGameRules_RestartRound, "@CSGameRules_RestartRound_Pre");
    RegisterHookChain(RG_CSGameRules_RestartRound, "@CSGameRules_RestartRound_Post", true);
    RegisterHookChain(RG_CSGameRules_CheckWinConditions, "@CSGameRules_CheckWinConditions_Pre");
    RegisterHookChain(RG_CSGameRules_FPlayerCanTakeDamage, "@CSGameRules_FPlayerCanTakeDamage_Pre");
    RegisterHookChain(RG_CSGameRules_OnRoundFreezeEnd, "@CSGameRules_OnRoundFreezeEnd_Pre");
    RegisterHookChain(RG_CSGameRules_GetPlayerSpawnSpot, "@CSGameRules_GetPlayerSpawnSpot_Pre");

    RegisterHookChain(RG_CBasePlayer_Spawn, "@CBasePlayer_Spawn_Pre");
    RegisterHookChain(RG_CBasePlayer_Spawn, "@CBasePlayer_Spawn_Post", true);
    RegisterHookChain(RG_CBasePlayer_HasRestrictItem, "@CBasePlayer_HasRestrictItem_Pre");
    RegisterHookChain(RG_CBasePlayer_TakeDamage, "@CBasePlayer_TakeDamage_Pre");
    RegisterHookChain(RG_CBasePlayer_ResetMaxSpeed, "@CBasePlayer_ResetMaxSpeed_Pre");
    RegisterHookChain(RG_CBasePlayer_Killed, "@CBasePlayer_Killed_Post", true);

    @Forcing_LevelInitialize();
}

public plugin_natives()
{
    register_native("bio_get_user_money", "@native_get_user_money");
    register_native("bio_set_user_money", "@native_set_user_money");
}

public plugin_end()
{
    if (m_trUselessEntities != Invalid_Trie)
        TrieDestroy(m_trUselessEntities);

    if (m_trTextMessage != Invalid_Trie)
        TrieDestroy(m_trTextMessage);

    if (m_trSendAudio != Invalid_Trie)
        TrieDestroy(m_trSendAudio);

    if (m_arrClassesVec != Invalid_Array)
        ArrayDestroy(m_arrClassesVec);

    if (m_arrSpawnPoints != Invalid_Array)
        ArrayDestroy(m_arrSpawnPoints);
}


/*============================================================
                        CLIENT
============================================================*/

public client_putinserver(id)
{
    m_varClass[id] = 0;
}

public client_disconnected(id)
{
    m_varClass[id] = 0;
    m_iZombieRespawn[id] = 0;

    BIT_SUB(m_bitZombie, BIT_PLAYER(id));
    BIT_SUB(m_bitZombieRespawn, BIT_PLAYER(id));
}


/*============================================================
                        COMMANDS
============================================================*/

public cmd_infect(id)
{
    @ZombieAttributes(id, 0);
}

public cmd_antidote(id)
{
    @HumanAttributes(id);
}


/*============================================================
                        CLASS MENU
============================================================*/

@Open_ClassesMenu(id)
{
    if (!m_iNumClasses)
    {
        client_print(id, print_chat, "Классы зомби не найдены!");
        return PLUGIN_HANDLED;
    }

    m_iMenuPosition[id] = 0;
    return @Display_ClassesMenu(id, 0);
}

@Display_ClassesMenu(id, pos)
{
    if (pos < 0)
        return PLUGIN_HANDLED;

    new start = pos * 7;
    if (start > m_iNumClasses)
        start = m_iNumClasses;

    start -= start % 7;
    pos = start / 7;

    new end = start + 7;
    if (end > m_iNumClasses)
        end = m_iNumClasses;

    new page = (m_iNumClasses / 7) + (m_iNumClasses % 7 ? 1 : 0);
    new key = 1 << 9;
    new menu[512];
    new data[ClassData];
    new len = formatex(
        menu,
        charsmax(menu),
        "\yClasses %d/%d^n^n",
        pos + 1,
        page
    );

    new item;
    while (start < end)
    {
        ArrayGetArray(m_arrClassesVec, start, data);

        if (m_varClass[id] != start)
            key |= 1 << item;

        len += formatex(
            menu[len],
            charsmax(menu) - len,
            "\y%d. %s%s^n",
            ++item,
            m_varClass[id] == start ? "\d" : "\w",
            data[ClassName]
        );

        start++;
    }

    if (pos)
        key |= 1 << 7;

    if (end < m_iNumClasses)
        key |= 1 << 8;

    len += formatex(
        menu[len],
        charsmax(menu) - len,
        "^n\y8. %sНазад",
        pos ? "\w" : "\d"
    );

    len += formatex(
        menu[len],
        charsmax(menu) - len,
        "^n\y9. %sВперёд",
        end < m_iNumClasses ? "\w" : "\d"
    );

    formatex(
        menu[len],
        charsmax(menu) - len,
        "^n\y0. \wВыход"
    );

    return show_menu(id, key, menu, -1, "@Display_ClassesMenu");
}

@Handle_ClassesMenu(id, key)
{
    switch (key)
    {
        case 7:
            return @Display_ClassesMenu(id, --m_iMenuPosition[id]);

        case 8:
            return @Display_ClassesMenu(id, ++m_iMenuPosition[id]);

        case 9:
            return PLUGIN_HANDLED;
    }

    new class = m_iMenuPosition[id] * 7 + key;
    if (class >= m_iNumClasses)
        return PLUGIN_HANDLED;

    m_varClass[id] = class;

    new data[ClassData];
    ArrayGetArray(m_arrClassesVec, class, data);

    client_print(
        id,
        print_chat,
        "Вы выбрали класс %s %d",
        data[ClassName],
        class
    );

    return PLUGIN_CONTINUE;
}


/*============================================================
                        MESSAGES / CVARS
============================================================*/

@Hook_ForcingCvarChange(pcvar, old_value[], new_value[])
{
    for (new i; i < sizeof IMMUTABLE_CVARS; i++)
    {
        if (m_cImmutableCvars[i] != pcvar)
            continue;

        if (!equal(new_value, IMMUTABLE_CVARS[i][1]))
            set_pcvar_string(pcvar, IMMUTABLE_CVARS[i][1]);

        break;
    }
}

@Message_TextMsg(msg_id, msg_dest, receiver)
{
    return @BlockMessage(m_trTextMessage);
}

@Message_SendAudio(msg_id, msg_dest, receiver)
{
    return @BlockMessage(m_trSendAudio);
}

@BlockMessage(Trie:trie)
{
    new text[32], value;
    get_msg_arg_string(2, text, charsmax(text));

    return TrieGetCell(trie, text, value)
        ? PLUGIN_HANDLED
        : PLUGIN_CONTINUE;
}

@Message_Money(msg_id, msg_dest, receiver)
{
    set_member(receiver, m_iAccount, m_iMoney[receiver]);
    set_msg_arg_int(1, get_msg_argtype(1), m_iMoney[receiver]);
}


/*============================================================
                        WARMUP / ROUND
============================================================*/

@Task_Warmup(task)
{
    if (get_member_game(m_bRoundTerminating))
        return;

    new timeLeft = floatround(
        float(get_member_game(m_iRoundTimeSecs))
        - get_gametime()
        + Float:get_member_game(m_fRoundStartTimeReal)
    );

    if (timeLeft == 11)
        client_cmd(0, "spk ^"plats/elevbell1.wav^"");
    else if (timeLeft <= 4 && timeLeft > 1)
        client_cmd(0, "spk ^"buttons/bell1.wav^"");

    if (timeLeft < 1)
        rg_round_end(3.0, WINSTATUS_DRAW, ROUND_GAME_COMMENCE, .trigger = true);
}

@SV_ActivateServer_Post()
{
    if (!m_fwdSpawn)
        return;

    unregister_forward(FM_Spawn, m_fwdSpawn);
    m_fwdSpawn = 0;

    TrieDestroy(m_trUselessEntities);
    m_trUselessEntities = Invalid_Trie;
}

@RoundEnd_Pre(WinStatus:status, ScenarioEventEndRound:event, Float:delay)
{
    if (m_bWarmup)
    {
        SetHookChainArg(2, ATYPE_INTEGER, ROUND_GAME_COMMENCE);
        SetHookChainArg(3, ATYPE_FLOAT, 3.0);
        return;
    }

    if (!get_member_game(m_bGameStarted))
        return;

    stop_zombie_respawn();

    if (status == WINSTATUS_NONE)
        status = WINSTATUS_DRAW;

    if (status == WINSTATUS_DRAW && get_pcvar_bool(m_pCvar_OverHumansWin))
        status = WINSTATUS_CTS;

    SetHookChainArg(1, ATYPE_INTEGER, status);
}

@RoundEnd_Post(WinStatus:status, ScenarioEventEndRound:event, Float:delay)
{
    if (m_bWarmup)
        return;

    switch (status)
    {
        case WINSTATUS_CTS:
        {
            client_print(0, print_center, "Humans won!");
            client_cmd(0, "spk ^"ambience/3dmstart.wav^"");
        }

        case WINSTATUS_TERRORISTS:
        {
            client_print(0, print_center, "Zombies won!");
            client_cmd(0, "spk ^"ambience/the_horror1.wav^"");
        }

        case WINSTATUS_DRAW:
        {
            client_print(0, print_center, "No one won!");
            client_cmd(0, "spk ^"ambience/3dmstart.wav^"");
        }
    }
}

@CSGameRules_RestartRound_Pre()
{
    if (m_bWarmup)
    {
        m_bWarmup = false;
        remove_task(TASKID_WARMUP);
    }

    set_pcvar_num(m_pCvar_FreezeTime, 15);
    set_pcvar_float(m_pCvar_RoundTime, 180.0 / 60.0);
}

@CSGameRules_RestartRound_Post()
{
    if (!m_bWarmup)
        set_member_game(m_bGameStarted, true);

    stop_zombie_respawn();
    m_bitZombie = 0;

    for (new i = 1; i <= MaxClients; i++)
    {
        if (is_user_connected(i))
            m_iZombieRespawn[i] = 0;
    }

    @Forcing_LevelInitialize();
}


/*============================================================
                    CUSTOM WIN CONDITIONS
============================================================*/

@CSGameRules_CheckWinConditions_Pre()
{
    if (m_bWarmup ||
        !get_member_game(m_bGameStarted) ||
        get_member_game(m_bRoundTerminating))
        return HC_SUPERCEDE;

    return HC_SUPERCEDE;
}

@RequestFrame_CheckChangeClassWinConditions()
{
    if (get_member_game(m_iRoundWinStatus) != WINSTATUS_NONE ||
        !get_member_game(m_bGameStarted) ||
        get_member_game(m_bRoundTerminating))
        return;

    new zombies = get_zombies_in_round();
    new humans = get_alive_humans();

    if (!zombies)
    {
        stop_zombie_respawn();
        rg_round_end(3.0, WINSTATUS_CTS, ROUND_CTS_WIN, .trigger = true);
        return;
    }

    if (!humans)
    {
        stop_zombie_respawn();
        rg_round_end(3.0, WINSTATUS_TERRORISTS, ROUND_TERRORISTS_WIN, .trigger = true);
    }
}


/*============================================================
                    GAME RULES
============================================================*/

@CSGameRules_FPlayerCanTakeDamage_Pre(id, attacker)
{
    if (get_member_game(m_bRoundTerminating) || m_bWarmup)
    {
        SetHookChainReturn(ATYPE_INTEGER, false);
        return HC_SUPERCEDE;
    }

    return HC_CONTINUE;
}

@CSGameRules_OnRoundFreezeEnd_Pre()
{
    if (!get_member_game(m_bGameStarted))
        return;

    m_bZombieRespawnAllowed = true;

    new players[MAX_CLIENTS];
    new count;

    for (new i = 1; i <= MaxClients; i++)
    {
        if (is_user_alive(i))
            players[count++] = i;
    }

    if (count < 2)
        return;

    new need = floatround(float(count) * 0.20, floatround_ceil);

    while (need--)
    {
        new pos = random(count);
        new id = players[pos];

        players[pos] = players[--count];
        @ZombieAttributes(id, 0);
    }
}

@CSGameRules_GetPlayerSpawnSpot_Pre(id)
{
    if (!IsPlaying(id))
        return HC_CONTINUE;

    new spot = IsZombie(id)
        ? @Select_ZombieSpawnPoint(id)
        : @Select_SpawnPoint(id);

    if (is_nullent(spot))
        return HC_CONTINUE;

    new Float:origin[3], Float:angles[3];

    get_entvar(spot, var_origin, origin);
    get_entvar(spot, var_angles, angles);

    origin[2] += 1.0;

    set_entvar(id, var_origin, origin);
    set_entvar(id, var_v_angle, NULL_VECTOR);
    set_entvar(id, var_velocity, NULL_VECTOR);
    set_entvar(id, var_angles, angles);
    set_entvar(id, var_punchangle, NULL_VECTOR);
    set_entvar(id, var_fixangle, 1);

    SetHookChainReturn(ATYPE_INTEGER, spot);
    return HC_SUPERCEDE;
}


/*============================================================
                    PLAYER SPAWN / ITEMS
============================================================*/

@CBasePlayer_Spawn_Pre(id)
{
    if (get_member(id, m_bJustConnected) || !IsPlaying(id))
        return;

    RequestFrame("@RequestFrame_ApplyPlayerAttributes", id);
}

@CBasePlayer_Spawn_Post(id)
{
    if (get_member(id, m_bJustConnected))
    {
        set_member(id, m_fNextSuicideTime, 999999999.0);
        return;
    }

    if (!IsPlaying(id))
        return;

    if (get_member_game(m_bFreezePeriod))
        set_member(id, m_bCanShootOverride, true);

    if (IsZombieRespawning(id))
    {
        BIT_SUB(m_bitZombieRespawn, BIT_PLAYER(id));
        set_member(id, m_flRespawnPending, 0.0);
    }
}

@CBasePlayer_HasRestrictItem_Pre(id)
{
    if (get_member(id, m_iTeam) != TEAM_TERRORIST)
        return HC_CONTINUE;

    SetHookChainReturn(ATYPE_BOOL, true);
    return HC_SUPERCEDE;
}


/*============================================================
                        DAMAGE
============================================================*/

@CBasePlayer_TakeDamage_Pre(victim, inflictor, attacker, Float:damage, damage_type)
{
    if (victim == attacker || !is_user_connected(attacker))
        return;

    if (!rg_is_player_can_takedamage(victim, attacker))
        return;

    if (IsZombie(attacker))
    {
        new weapon = get_member(attacker, m_pActiveItem);

        if (!is_nullent(weapon) && get_member(weapon, m_iId) == WEAPON_KNIFE)
        {
            @ZombieAttributes(victim, attacker);

            bio_set_user_money(
                attacker,
                bio_get_user_money(attacker) + get_pcvar_num(m_pCvar_MoneyInfection)
            );

            SetHookChainArg(4, ATYPE_FLOAT, 0.0);
            return;
        }
    }

    new money = floatround(
        damage * get_pcvar_float(m_pCvar_MoneyDamageRatio),
        floatround_floor
    );

    bio_set_user_money(
        attacker,
        bio_get_user_money(attacker) + money
    );

    SetHookChainArg(4, ATYPE_FLOAT, damage * 10.0);
}


/*============================================================
                        SPEED
============================================================*/

@CBasePlayer_ResetMaxSpeed_Pre(id)
{
    if (!is_user_alive(id))
        return HC_CONTINUE;

    if (IsZombie(id))
    {
        new data[ClassData];
        ArrayGetArray(m_arrClassesVec, m_varClass[id], data);

        set_entvar(id, var_maxspeed, data[ClassSpeed]);
        return HC_SUPERCEDE;
    }

    new weapon = get_member(id, m_pActiveItem);
    new Float:speed;

    if (!is_nullent(weapon))
        ExecuteHamB(Ham_CS_Item_GetMaxSpeed, weapon, speed);
    else
        speed = 240.0;

    set_entvar(id, var_maxspeed, speed);
    return HC_SUPERCEDE;
}


/*============================================================
                        KILLED
============================================================*/

@CBasePlayer_Killed_Post(id, attacker, gib)
{
    if (is_user_alive(id))
        return;

    new TeamName:team = get_member(id, m_iTeam);

    if (team == TEAM_SPECTATOR || team == TEAM_UNASSIGNED)
        return;

    if (!get_member_game(m_bGameStarted) ||
        get_member_game(m_bRoundTerminating))
        return;

    if (IsZombie(id) &&
        m_bZombieRespawnAllowed &&
        CanZombieRespawn(id))
    {
        new Float:time = get_pcvar_float(m_pCvar_ZombieRespawnTime);

        if (time > 0.0)
        {
            BIT_ADD(m_bitZombieRespawn, BIT_PLAYER(id));
            m_iZombieRespawn[id]++;

            set_member(
                id,
                m_flRespawnPending,
                get_gametime() + time
            );

            return;
        }
    }

    RequestFrame("@RequestFrame_CheckChangeClassWinConditions");
}


/*============================================================
                    ENTITY SPAWN
============================================================*/

@FM_Spawn_Pre(entity)
{
    new classname[32];
    get_entvar(entity, var_classname, classname, charsmax(classname));

    if (!TrieKeyExists(m_trUselessEntities, classname))
        return FMRES_IGNORED;

    forward_return(FMV_CELL, NULLENT);
    return FMRES_SUPERCEDE;
}


/*============================================================
                    ZOMBIE / HUMAN
============================================================*/

@ZombieAttributes(id, attacker)
{
    BIT_ADD(m_bitZombie, BIT_PLAYER(id));

    new data[ClassData];
    ArrayGetArray(m_arrClassesVec, m_varClass[id], data);

    set_entvar(id, var_health, data[ClassHealth]);
    set_entvar(id, var_gravity, data[ClassGravity]);
    set_entvar(id, var_armorvalue, 0.0);
    set_member(id, m_iKevlar, ARMOR_NONE);

    rg_set_user_model(id, data[ClassModel]);

    rg_remove_all_items(id);
    rg_give_item(id, "weapon_knife");

    set_entvar(id, var_viewmodel, data[ClassClaw]);
    set_entvar(id, var_weaponmodel, NULL_STRING);

    rh_emit_sound2(
        id,
        0,
        CHAN_VOICE,
        ZOMBIE_SCREAM[random(sizeof ZOMBIE_SCREAM)]
    );

    @UTIL_DeathMsg(
        attacker ? attacker : id,
        id,
        0,
        "infection"
    );

    @UTIL_ScoreAttrib(id, 0);
    rg_set_user_team(id, TEAM_TERRORIST);

    if (attacker)
    {
        ExecuteHamB(Ham_AddPoints, id, 0, true);
        ExecuteHamB(Ham_AddPoints, attacker, 1, true);

        set_member(
            id,
            m_iDeaths,
            get_member(id, m_iDeaths) + 1
        );
    }

    if (get_member_game(m_bGameStarted) &&
        !get_member_game(m_bFreezePeriod))
        RequestFrame("@RequestFrame_CheckChangeClassWinConditions");
}

@HumanAttributes(id)
{
    rg_set_user_team(id, TEAM_CT);
    BIT_SUB(m_bitZombie, BIT_PLAYER(id));

    set_entvar(id, var_health, 100.0);
    set_entvar(id, var_gravity, 1.0);

    rg_reset_user_model(id);

    set_entvar(id, var_viewmodel, "models/v_knife.mdl");
    set_entvar(id, var_weaponmodel, "models/p_knife.mdl");

    rg_remove_all_items(id);
    rg_give_item(id, "weapon_knife");
    rg_give_item(id, "weapon_g3sg1");
    rg_set_user_bpammo(id, WEAPON_G3SG1, 999);

    if (get_member_game(m_bGameStarted) &&
        !get_member_game(m_bFreezePeriod))
        RequestFrame("@RequestFrame_CheckChangeClassWinConditions");
}

@RequestFrame_ApplyPlayerAttributes(id)
{
    if (!is_user_connected(id))
        return;

    if (IsZombie(id))
        @ZombieAttributes(id, 0);
    else
        @HumanAttributes(id);
}


/*============================================================
                    SPAWN POINTS
============================================================*/

@Select_ZombieSpawnPoint(id)
{
    new numPoints = ArraySize(m_arrSpawnPoints);

    if (!numPoints)
        return 0;

    new safeSpot;
    new farthestSpot;

    new Float:safeDistance = -1.0;
    new Float:farthestDistance = -1.0;

    new Float:minDistance =
        get_pcvar_float(m_pCvar_ZombieRespawnMinDistance);

    minDistance *= minDistance;

    new Float:spawnOrigin[3];
    new Float:playerOrigin[3];

    for (new i; i < numPoints; i++)
    {
        new spot = ArrayGetCell(m_arrSpawnPoints, i);

        if (is_nullent(spot))
            continue;

        get_entvar(spot, var_origin, spawnOrigin);

        if (!IsHullVacant(id, spawnOrigin, HULL_HUMAN))
            continue;

        new Float:nearest = 999999999.0;

        for (new j = 1; j <= MaxClients; j++)
        {
            if (!is_user_alive(j) || IsZombie(j))
                continue;

            get_entvar(j, var_origin, playerOrigin);

            new Float:distance =
                get_distance_squared(spawnOrigin, playerOrigin);

            if (distance < nearest)
                nearest = distance;
        }

        if (nearest > farthestDistance)
        {
            farthestDistance = nearest;
            farthestSpot = spot;
        }

        if (nearest < minDistance)
            continue;

        if (nearest > safeDistance)
        {
            safeDistance = nearest;
            safeSpot = spot;
        }
    }

    if (!is_nullent(safeSpot))
        return safeSpot;

    if (!is_nullent(farthestSpot))
        return farthestSpot;

    return @Select_SpawnPoint(id);
}

@Select_SpawnPoint(id)
{
    new numPoints = ArraySize(m_arrSpawnPoints);

    if (!numPoints)
        return 0;

    new spot;
    new lastSpot = m_iLastSpawn;
    new Float:origin[3];

    do
    {
        if (++lastSpot >= numPoints)
            lastSpot = 0;

        spot = ArrayGetCell(m_arrSpawnPoints, lastSpot);

        if (is_nullent(spot))
            continue;

        get_entvar(spot, var_origin, origin);

        if (!IsHullVacant(id, origin, HULL_HUMAN))
            continue;

        break;
    }
    while (lastSpot != m_iLastSpawn);

    if (is_nullent(spot))
        return 0;

    m_iLastSpawn = lastSpot;
    return spot;
}

@Forcing_LevelInitialize()
{
    if (!ArraySize(m_arrSpawnPoints))
    {
        new entity = NULLENT;

        while ((entity = rg_find_ent_by_class(
            entity,
            "info_player_start",
            true
        )))
            ArrayPushCell(m_arrSpawnPoints, entity);

        entity = NULLENT;

        while ((entity = rg_find_ent_by_class(
            entity,
            "info_player_deathmatch",
            true
        )))
            ArrayPushCell(m_arrSpawnPoints, entity);
    }

    new count = ArraySize(m_arrSpawnPoints);

    set_member_game(m_iSpawnPointCount_Terrorist, count);
    set_member_game(m_iSpawnPointCount_CT, count);
    set_member_game(m_bLevelInitialized, true);
}


/*============================================================
                    ZOMBIE RESPAWN
============================================================*/

static stock bool:CanZombieRespawn(id)
{
    new alive = get_alive_players() + 1;
    new zombies = get_alive_zombies();
    new humans = get_alive_humans();

    if (alive <= 1 || humans <= 1 || zombies <= 0)
        return false;

    if (zombies >= humans)
        return false;

    if (zombies + 1 >
        floatround(
            float(alive) * get_max_zombie_count(),
            floatround_ceil
        ))
        return false;

    if (m_iZombieRespawn[id] >=
        get_pcvar_num(m_pCvar_ZombieRespawnLimit))
        return false;

    new Float:chance =
        100.0
        - float(m_iZombieRespawn[id])
        * get_pcvar_float(m_pCvar_ZombieRespawnChanceDecay);

    return chance > 0.0 &&
        random_float(0.0, 100.0) <= chance;
}

static stock stop_zombie_respawn()
{
    m_bZombieRespawnAllowed = false;

    for (new i = 1; i <= MaxClients; i++)
    {
        if (is_user_connected(i) && IsZombieRespawning(i))
            set_member(i, m_flRespawnPending, 0.0);
    }

    m_bitZombieRespawn = 0;
}


/*============================================================
                    PLAYER COUNTS
============================================================*/

static stock get_alive_players()
{
    new count;

    for (new i = 1; i <= MaxClients; i++)
        if (is_user_alive(i))
            count++;

    return count;
}

static stock get_alive_zombies()
{
    new count;

    for (new i = 1; i <= MaxClients; i++)
        if (is_user_alive(i) && IsZombie(i))
            count++;

    return count;
}

static stock get_alive_humans()
{
    new count;

    for (new i = 1; i <= MaxClients; i++)
        if (is_user_alive(i) && !IsZombie(i))
            count++;

    return count;
}

/*
 * Зомби, которые ещё считаются участниками раунда:
 *  - живые зомби;
 *  - мёртвые зомби с ожидающим респавном.
 *
 * Это важно: мёртвый zombie с m_bitZombieRespawn
 * не должен мгновенно давать победу людям.
 */
static stock get_zombies_in_round()
{
    new count;

    for (new i = 1; i <= MaxClients; i++)
    {
        if (!IsZombie(i))
            continue;

        if (is_user_alive(i) || IsZombieRespawning(i))
            count++;
    }

    return count;
}

static stock get_zombie_player_ratio()
{
    new players = get_alive_players();

    if (players <= 4)
        return 33;

    if (players <= 9)
        return 40;

    if (players <= 19)
        return 45;

    return 50;
}

static stock get_zombie_time_ratio()
{
    new Float:start = get_member_game(m_fRoundStartTime);

    if (start <= 0.0)
        return 50;

    new Float:time = get_gametime() - start;

    if (time < 30.0)
        return 50;

    if (time < 60.0)
        return 45;

    if (time < 120.0)
        return 40;

    return 35;
}

static stock get_max_zombie_count()
{
    new players = get_alive_players();

    if (!players)
        return 0;

    new playerRatio = get_zombie_player_ratio();
    new timeRatio = get_zombie_time_ratio();
    new ratio = playerRatio < timeRatio
        ? playerRatio
        : timeRatio;

    new count = floatround(
        float(players) * float(ratio) / 100.0,
        floatround_ceil
    );

    return count < 1 ? 1 : count;
}


/*============================================================
                    HELPERS
============================================================*/

static stock bool:IsZombie(id)
{
    return BIT_VALID(m_bitZombie, BIT_PLAYER(id));
}

static stock bool:IsZombieRespawning(id)
{
    return BIT_VALID(m_bitZombieRespawn, BIT_PLAYER(id));
}

static stock bool:IsPlaying(id)
{
    new TeamName:team = get_member(id, m_iTeam);
    return team == TEAM_CT || team == TEAM_TERRORIST;
}

static stock Float:get_distance_squared(
    const Float:a[3],
    const Float:b[3]
)
{
    new Float:x = a[0] - b[0];
    new Float:y = a[1] - b[1];
    new Float:z = a[2] - b[2];

    return x * x + y * y + z * z;
}

static stock bool:IsHullVacant(
    id,
    Float:origin[3],
    hull
)
{
    engfunc(
        EngFunc_TraceHull,
        origin,
        origin,
        0,
        hull,
        id,
        0
    );

    return !get_tr2(0, TR_StartSolid) &&
           !get_tr2(0, TR_AllSolid) &&
           get_tr2(0, TR_InOpen);
}


/*============================================================
                    ENTITY / CONFIG
============================================================*/

static stock bool:Read_JsonField(const fileName[])
{
    new path[PLATFORM_MAX_PATH];

    get_localinfo("amxx_configsdir", path, charsmax(path));
    add(path, charsmax(path), "/biohazard");

    if (!dir_exists(path))
    {
        set_fail_state("Directory '%s' not found.", path);
        return false;
    }

    formatex(
        path,
        charsmax(path),
        "%s/%s.json",
        path,
        fileName
    );

    if (!file_exists(path))
    {
        set_fail_state("File '%s' not found.", path);
        return false;
    }

    new JSON:jsonParse = json_parse(path, true);

    if (jsonParse == Invalid_JSON)
    {
        set_fail_state("Parsing file '%s' error.", path);
        return false;
    }

    new JSON:value = json_object_get_value(
        jsonParse,
        "useless_entities"
    );

    if (json_is_array(value))
    {
        m_trUselessEntities = TrieCreate();

        new classname[32];

        for (new i; i < json_array_get_count(value); i++)
        {
            json_array_get_string(
                value,
                i,
                classname,
                charsmax(classname)
            );

            TrieSetCell(
                m_trUselessEntities,
                classname,
                1
            );
        }

        json_free(value);

        if (TrieGetSize(m_trUselessEntities))
            m_fwdSpawn = register_forward(
                FM_Spawn,
                "@FM_Spawn_Pre"
            );
        else
        {
            TrieDestroy(m_trUselessEntities);
            m_trUselessEntities = Invalid_Trie;
        }
    }

    new JSON:classes = json_object_get_value(
        jsonParse,
        "classes"
    );

    if (json_is_array(classes))
    {
        new data[ClassData];
        new JSON:item;

        for (new i; i < json_array_get_count(classes); i++)
        {
            item = json_array_get_value(classes, i);

            json_object_get_string(
                item,
                "name",
                data[ClassName],
                charsmax(data[ClassName])
            );

            data[ClassHealth] =
                _:json_object_get_real(item, "health");

            data[ClassSpeed] =
                _:json_object_get_real(item, "speed");

            data[ClassGravity] =
                _:json_object_get_real(item, "gravity");

            json_object_get_string(
                item,
                "model",
                data[ClassModel],
                charsmax(data[ClassModel])
            );

            precache_model(fmt(
                "models/player/%s/%s.mdl",
                data[ClassModel],
                data[ClassModel]
            ));

            json_object_get_string(
                item,
                "claw",
                data[ClassClaw],
                charsmax(data[ClassClaw])
            );

            precache_model(data[ClassClaw]);

            ArrayPushArray(
                m_arrClassesVec,
                data
            );

            json_free(item);
        }

        m_iNumClasses = ArraySize(m_arrClassesVec);
        json_free(classes);
    }

    json_free(jsonParse);
    return true;
}


/*============================================================
                        UTIL
============================================================*/

@UTIL_DeathMsg(killer, victim, headshot, const weapon[])
{
    message_begin(
        MSG_ALL,
        get_user_msgid("DeathMsg")
    );

    write_byte(killer);
    write_byte(victim);
    write_byte(headshot);
    write_string(weapon);

    message_end();
}

@UTIL_ScoreAttrib(id, flags)
{
    message_begin(
        MSG_ALL,
        get_user_msgid("ScoreAttrib")
    );

    write_byte(id);
    write_byte(flags);

    message_end();
}


/*============================================================
                        NATIVES
============================================================*/

@native_get_user_money(amxx, argc)
{
    new id = get_param(1);

    if (!is_user_connected(id))
    {
        log_error(
            AMX_ERR_NATIVE,
            "Invalid index '%d'.",
            id
        );

        return -1;
    }

    return m_iMoney[id];
}

@native_set_user_money(amxx, argc)
{
    new id = get_param(1);

    if (!is_user_connected(id))
    {
        log_error(
            AMX_ERR_NATIVE,
            "Invalid index '%d'.",
            id
        );

        return 0;
    }

    new amount = get_param(2);

    if (amount < 0)
        amount = 0;
    else if (amount > 12000)
        amount = 12000;

    message_begin(
        MSG_ONE,
        get_user_msgid("Money"),
        .player = id
    );

    write_long(amount);
    write_byte(get_param(3));

    message_end();

    set_member(id, m_iAccount, amount);
    m_iMoney[id] = amount;

    return 1;
}