/**
// ====================================================================================================
Change Log:

1.0.6 (12-February-2021)
    - Fixed custom model cvar typo. (thanks "weffer" for reporting)

1.0.5 (11-February-2021)
    - Fixed a bug not rendering custom sprites right after turning it on.
    - Added one more custom sprite option with an alpha background filling the bar.

1.0.4 (11-February-2021)
    - Added custom sprite option.

1.0.3 (08-February-2021)
    - Fixed missing client in-game in visibility check. (thanks to "Krufftys Killers" and "Striker black")

1.0.2 (08-February-2021)
    - Fixed wrong value on max health calculation.
    - Fixed sprite hiding behind tank rocks.
    - Fixed sprite hiding while tank throws rocks (ability use).
    - Moved visibility logic to timer handle.

1.0.1 (30-January-2021)
    - Public release.

1.0.0 (21-April-2019)
    - Private version.

// ====================================================================================================
*/

// ====================================================================================================
// Plugin Info - define
// ====================================================================================================
#define PLUGIN_NAME                   "[L4D1 & L4D2] Tank HP Sprite"
#define PLUGIN_AUTHOR                 "Mart"
#define PLUGIN_DESCRIPTION            "Shows a sprite at the tank head that goes from green to red based on its HP"
#define PLUGIN_VERSION                "1.0.5"
#define PLUGIN_URL                    "https://forums.alliedmods.net/showthread.php?t=330370"

// ====================================================================================================
// Plugin Info
// ====================================================================================================
public Plugin myinfo =
{
    name        = PLUGIN_NAME,
    author      = PLUGIN_AUTHOR,
    description = PLUGIN_DESCRIPTION,
    version     = PLUGIN_VERSION,
    url         = PLUGIN_URL
}

// ====================================================================================================
// Includes
// ====================================================================================================
#include <sourcemod>
#include <sdktools>
#include <sdkhooks>

// ====================================================================================================
// Pragmas
// ====================================================================================================
#pragma semicolon 1
#pragma newdecls required

// ====================================================================================================
// Cvar Flags
// ====================================================================================================
#define CVAR_FLAGS                    FCVAR_NOTIFY
#define CVAR_FLAGS_PLUGIN_VERSION     FCVAR_NOTIFY|FCVAR_DONTRECORD|FCVAR_SPONLY

// ====================================================================================================
// Filenames
// ====================================================================================================
#define CONFIG_FILENAME               "l4d_tank_hp_sprite"

// ====================================================================================================
// Defines
// ====================================================================================================
#define CLASSNAME_ENV_SPRITE          "env_sprite"
#define CLASSNAME_ENV_TEXTURETOGGLE   "env_texturetoggle"
#define CLASSNAME_TANK_ROCK           "tank_rock"

#define TEAM_SPECTATOR                1
#define TEAM_SURVIVOR                 2
#define TEAM_INFECTED                 3
#define TEAM_HOLDOUT                  4

#define FLAG_TEAM_NONE                (0 << 0) // 0 | 0000
#define FLAG_TEAM_SURVIVOR            (1 << 0) // 1 | 0001
#define FLAG_TEAM_INFECTED            (1 << 1) // 2 | 0010
#define FLAG_TEAM_SPECTATOR           (1 << 2) // 4 | 0100
#define FLAG_TEAM_HOLDOUT             (1 << 3) // 8 | 1000

#define L4D1_ZOMBIECLASS_TANK         5
#define L4D2_ZOMBIECLASS_TANK         8

#define MAXENTITIES                   2048

// ====================================================================================================
// Plugin Cvars
// ====================================================================================================
static ConVar g_hCvar_Enabled;
static ConVar g_hCvar_ZAxis;
static ConVar g_hCvar_FadeDistance;
static ConVar g_hCvar_Sight;
static ConVar g_hCvar_AttackDelay;
static ConVar g_hCvar_AliveShow;
static ConVar g_hCvar_AliveModel;
static ConVar g_hCvar_AliveAlpha;
static ConVar g_hCvar_AliveScale;
static ConVar g_hCvar_DeadShow;
static ConVar g_hCvar_DeadModel;
static ConVar g_hCvar_DeadAlpha;
static ConVar g_hCvar_DeadScale;
static ConVar g_hCvar_DeadColor;
static ConVar g_hCvar_Team;
static ConVar g_hCvar_CustomModel;
static ConVar g_hCvar_CustomModelVMT;
static ConVar g_hCvar_CustomModelVTF;
static ConVar g_hCvar_AllSpecials;

// ====================================================================================================
// bool - Plugin Variables
// ====================================================================================================
static bool   g_bL4D2;
static bool   g_bConfigLoaded;
static bool   g_bEventsHooked;
static bool   g_bCvar_Enabled;
static bool   g_bCvar_Sight;
static bool   g_bCvar_AttackDelay;
static bool   g_bCvar_AliveShow;
static bool   g_bCvar_DeadShow;
static bool   g_bCvar_CustomModel;

// ====================================================================================================
// int - Plugin Variables
// ====================================================================================================
static int    g_iTankClass;
static int    g_iCvar_AliveAlpha;
static int    g_iCvar_DeadAlpha;
static int    g_iCvar_FadeDistance;
static int    g_iCvar_Team;

// ====================================================================================================
// float - Plugin Variables
// ====================================================================================================
static float  g_fVPlayerMins[3] = {-16.0, -16.0,  0.0};
static float  g_fVPlayerMaxs[3] = { 16.0,  16.0, 71.0};
static float  g_fVPos[3];
static float  g_fCvar_ZAxis;
static float  g_fCvar_AttackDelay;
static float  g_fCvar_AliveScale;
static float  g_fCvar_DeadScale;

// ====================================================================================================
// string - Plugin Variables
// ====================================================================================================
static char   g_sCvar_AliveModel[100];
static char   g_sCvar_AliveAlpha[4];
static char   g_sCvar_AliveScale[5];
static char   g_sCvar_DeadModel[100];
static char   g_sCvar_DeadAlpha[4];
static char   g_sCvar_DeadScale[5];
static char   g_sCvar_DeadColor[12];
static char   g_sCvar_FadeDistance[5];
static char   g_sCvar_CustomModelVMT[100];
static char   g_sCvar_CustomModelVTF[100];

// ====================================================================================================
// client - Plugin Variables
// ====================================================================================================
static int    gc_iTankSpriteRef[MAXPLAYERS+1] = { INVALID_ENT_REFERENCE, ... };
static int    gc_iTankSpriteFrameRef[MAXPLAYERS+1] = { INVALID_ENT_REFERENCE, ... };
static bool   gc_bVisible[MAXPLAYERS+1][MAXPLAYERS+1];
static float  gc_fLastAttack[MAXPLAYERS+1][MAXPLAYERS+1];

// ====================================================================================================
// entity - Plugin Variables
// ====================================================================================================
static bool   ge_bInvalidTrace[MAXENTITIES+1];
static int    ge_iOwner[MAXENTITIES+1];

// ====================================================================================================
// Plugin Start
// ====================================================================================================
public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max)
{
    EngineVersion engine = GetEngineVersion();

    if (engine != Engine_Left4Dead && engine != Engine_Left4Dead2)
    {
        strcopy(error, err_max, "This plugin only runs in \"Left 4 Dead\" and \"Left 4 Dead 2\" game");
        return APLRes_SilentFailure;
    }

    g_bL4D2 = (engine == Engine_Left4Dead2);
    g_iTankClass = (g_bL4D2 ? L4D2_ZOMBIECLASS_TANK : L4D1_ZOMBIECLASS_TANK);

    return APLRes_Success;
}

/****************************************************************************************************/

public void OnPluginStart()
{
    CreateConVar("l4d_tank_hp_sprite_version", PLUGIN_VERSION, PLUGIN_DESCRIPTION, CVAR_FLAGS_PLUGIN_VERSION);
    g_hCvar_Enabled        = CreateConVar("l4d_tank_hp_sprite_enable", "1", "Enable/Disable the plugin.\n0 = Disable, 1 = Enable.", CVAR_FLAGS, true, 0.0, true, 1.0);
    g_hCvar_ZAxis          = CreateConVar("l4d_tank_hp_sprite_z_axis", "92", "Additional Z distance based on the tank position.", CVAR_FLAGS, true, 0.0);
    g_hCvar_FadeDistance   = CreateConVar("l4d_tank_hp_sprite_fade_distance", "-1", "Minimum distance that a client must be from the tank to see the sprite (both alive and dead sprites).\n-1 = Always visible.", CVAR_FLAGS, true, -1.0, true, 9999.0);
    g_hCvar_Sight          = CreateConVar("l4d_tank_hp_sprite_sight", "1", "Show the sprite to the survivor only if the Tank is on sight.\n0 = OFF, 1 = ON.", CVAR_FLAGS, true, 0.0, true, 1.0);
    g_hCvar_AttackDelay    = CreateConVar("l4d_tank_hp_sprite_attack_delay", "0.0", "Show the sprite to the survivor attacker, by this amount of time in seconds, after hitting the Tank.\n0 = OFF.", CVAR_FLAGS, true, 0.0);
    g_hCvar_AliveShow      = CreateConVar("l4d_tank_hp_sprite_alive_show", "1", "Show the alive sprite while tank is alive.\n0 = OFF, 1 = ON.", CVAR_FLAGS, true, 0.0, true, 1.0);
    g_hCvar_AliveModel     = CreateConVar("l4d_tank_hp_sprite_alive_model", "materials/vgui/healthbar_white.vmt", "Model of alive tank sprite.");
    g_hCvar_AliveAlpha     = CreateConVar("l4d_tank_hp_sprite_alive_alpha", "200", "Alpha of alive tank sprite.\n0 = Invisible, 255 = Fully Visible", CVAR_FLAGS, true, 0.0, true, 255.0);
    g_hCvar_AliveScale     = CreateConVar("l4d_tank_hp_sprite_alive_scale", "0.25", "Scale of alive tank sprite (increases both height and width).\nNote: Some range values maintain the same size. (e.g. from 0.0 to 0.38 the size doesn't change).", CVAR_FLAGS, true, 0.0);
    g_hCvar_DeadShow       = CreateConVar("l4d_tank_hp_sprite_dead_show", "1", "Show the dead sprite when a tank dies.\n0 = OFF, 1 = ON.", CVAR_FLAGS, true, 0.0, true, 1.0);
    g_hCvar_DeadModel      = CreateConVar("l4d_tank_hp_sprite_dead_model", "materials/sprites/death_icon.vmt", "Model of dead tank sprite.");
    g_hCvar_DeadAlpha      = CreateConVar("l4d_tank_hp_sprite_dead_alpha", "200", "Alpha of dead tank sprite.\n0 = Invisible, 255 = Fully Visible", CVAR_FLAGS, true, 0.0, true, 255.0);
    g_hCvar_DeadScale      = CreateConVar("l4d_tank_hp_sprite_dead_scale", "0.25", "Scale of dead tank sprite (increases both height and width).\nSome range values maintain the size the same.", CVAR_FLAGS, true, 0.0);
    g_hCvar_DeadColor      = CreateConVar("l4d_tank_hp_sprite_dead_color", "225 0 0", "Color of dead tank sprite.\nUse three values between 0-255 separated by spaces (\"<0-255> <0-255> <0-255>\").", CVAR_FLAGS);
    g_hCvar_Team           = CreateConVar("l4d_tank_hp_sprite_team", "3", "Which teams should the sprite be visible.\n0 = NONE, 1 = SURVIVOR, 2 = INFECTED, 4 = SPECTATOR, 8 = HOLDOUT.\nAdd numbers greater than 0 for multiple options.\nExample: \"3\", enables for SURVIVOR and INFECTED.", CVAR_FLAGS, true, 0.0, true, 15.0);
    g_hCvar_CustomModel    = CreateConVar("l4d_tank_hp_sprite_custom_model", "0", "Use a custom sprite for the alive model\nNote: This requires the client downloading the custom model (.vmt and .vtf) to work.\nSearch for FastDL for more info.\n0 = OFF, 1 = ON.", CVAR_FLAGS, true, 0.0, true, 1.0);
    g_hCvar_CustomModelVMT = CreateConVar("l4d_tank_hp_sprite_custom_model_vmt", "materials/mart/mart_custombar.vmt", "Custom sprite VMT path.");
    g_hCvar_CustomModelVTF = CreateConVar("l4d_tank_hp_sprite_custom_model_vtf", "materials/mart/mart_custombar.vtf", "Custom sprite VTF path.");
    g_hCvar_AllSpecials  = CreateConVar("l4d_tank_hp_sprite_all_specials", "1", "Should all specials have healthbar or only tanks\n0 = Tanks Only, 1 = All Specials", CVAR_FLAGS, true, 0.0, true, 1.0);

    g_hCvar_Enabled.AddChangeHook(Event_ConVarChanged);
    g_hCvar_ZAxis.AddChangeHook(Event_ConVarChanged);
    g_hCvar_FadeDistance.AddChangeHook(Event_ConVarChanged);
    g_hCvar_Sight.AddChangeHook(Event_ConVarChanged);
    g_hCvar_AttackDelay.AddChangeHook(Event_ConVarChanged);
    g_hCvar_AliveShow.AddChangeHook(Event_ConVarChanged);
    g_hCvar_AliveModel.AddChangeHook(Event_ConVarChanged);
    g_hCvar_AliveAlpha.AddChangeHook(Event_ConVarChanged);
    g_hCvar_AliveScale.AddChangeHook(Event_ConVarChanged);
    g_hCvar_DeadShow.AddChangeHook(Event_ConVarChanged);
    g_hCvar_DeadModel.AddChangeHook(Event_ConVarChanged);
    g_hCvar_DeadAlpha.AddChangeHook(Event_ConVarChanged);
    g_hCvar_DeadScale.AddChangeHook(Event_ConVarChanged);
    g_hCvar_DeadColor.AddChangeHook(Event_ConVarChanged);
    g_hCvar_Team.AddChangeHook(Event_ConVarChanged);
    g_hCvar_CustomModel.AddChangeHook(Event_ConVarChanged);
    g_hCvar_CustomModelVMT.AddChangeHook(Event_ConVarChanged);
    g_hCvar_CustomModelVTF.AddChangeHook(Event_ConVarChanged);

    // Load plugin configs from .cfg
    AutoExecConfig(true, CONFIG_FILENAME);

    // Admin Commands
    RegAdminCmd("sm_print_cvars_l4d_tank_hp_sprite", CmdPrintCvars, ADMFLAG_ROOT, "Print the plugin related cvars and their respective values to the console.");

    CreateTimer(0.1, TimerKill, _, TIMER_REPEAT);
    CreateTimer(0.1, TimerVisible, _, TIMER_REPEAT);
    CreateTimer(0.1, TimerRender, _, TIMER_REPEAT);
}

/****************************************************************************************************/

public void OnPluginEnd()
{
    int entity;
    char targetname[19];

    entity = INVALID_ENT_REFERENCE;
    while ((entity = FindEntityByClassname(entity, CLASSNAME_ENV_SPRITE)) != INVALID_ENT_REFERENCE)
    {
        if (GetEntProp(entity, Prop_Data, "m_iHammerID") == -1)
        {
            GetEntPropString(entity, Prop_Data, "m_iName", targetname, sizeof(targetname));
            if (StrEqual(targetname, "l4d_tank_hp_sprite"))
                AcceptEntityInput(entity, "Kill");
        }
    }
}

/****************************************************************************************************/

public void OnConfigsExecuted()
{
    GetCvars();

    g_bConfigLoaded = true;

    LateLoad();

    HookEvents(g_bCvar_Enabled);
}

/****************************************************************************************************/

public void Event_ConVarChanged(Handle convar, const char[] sOldValue, const char[] sNewValue)
{
    GetCvars();

    HookEvents(g_bCvar_Enabled);
}

/****************************************************************************************************/

public void GetCvars()
{
    g_bCvar_Enabled = g_hCvar_Enabled.BoolValue;
    g_fCvar_ZAxis = g_hCvar_ZAxis.FloatValue;
    g_fVPos[2] = g_fCvar_ZAxis;
    g_iCvar_FadeDistance = g_hCvar_FadeDistance.IntValue;
    FormatEx(g_sCvar_FadeDistance, sizeof(g_sCvar_FadeDistance), "%i", g_iCvar_FadeDistance);
    g_bCvar_Sight = g_hCvar_Sight.BoolValue;
    g_fCvar_AttackDelay = g_hCvar_AttackDelay.FloatValue;
    g_bCvar_AttackDelay = (g_fCvar_AttackDelay > 0.0);
    g_bCvar_AliveShow = g_hCvar_AliveShow.BoolValue;
    g_hCvar_AliveModel.GetString(g_sCvar_AliveModel, sizeof(g_sCvar_AliveModel));
    TrimString(g_sCvar_AliveModel);
    g_iCvar_AliveAlpha = g_hCvar_AliveAlpha.IntValue;
    FormatEx(g_sCvar_AliveAlpha, sizeof(g_sCvar_AliveAlpha), "%i", g_iCvar_AliveAlpha);
    g_fCvar_AliveScale = g_hCvar_AliveScale.FloatValue;
    FormatEx(g_sCvar_AliveScale, sizeof(g_sCvar_AliveScale), "%.2f", g_fCvar_AliveScale);
    g_bCvar_DeadShow = g_hCvar_DeadShow.BoolValue;
    g_hCvar_DeadModel.GetString(g_sCvar_DeadModel, sizeof(g_sCvar_DeadModel));
    TrimString(g_sCvar_DeadModel);
    g_iCvar_DeadAlpha = g_hCvar_DeadAlpha.IntValue;
    FormatEx(g_sCvar_DeadAlpha, sizeof(g_sCvar_DeadAlpha), "%i", g_iCvar_DeadAlpha);
    g_fCvar_DeadScale = g_hCvar_DeadScale.FloatValue;
    FormatEx(g_sCvar_DeadScale, sizeof(g_sCvar_DeadScale), "%.2f", g_fCvar_DeadScale);
    g_hCvar_DeadColor.GetString(g_sCvar_DeadColor, sizeof(g_sCvar_DeadColor));
    TrimString(g_sCvar_DeadColor);
    g_iCvar_Team = g_hCvar_Team.IntValue;
    g_bCvar_CustomModel = g_hCvar_CustomModel.BoolValue;
    g_hCvar_CustomModelVMT.GetString(g_sCvar_CustomModelVMT, sizeof(g_sCvar_CustomModelVMT));
    TrimString(g_sCvar_CustomModelVMT);
    g_hCvar_CustomModelVTF.GetString(g_sCvar_CustomModelVTF, sizeof(g_sCvar_CustomModelVTF));
    TrimString(g_sCvar_CustomModelVTF);

    if (g_bCvar_AliveShow)
    {
        if (g_bCvar_CustomModel)
        {
            AddFileToDownloadsTable(g_sCvar_CustomModelVMT);
            AddFileToDownloadsTable(g_sCvar_CustomModelVTF);
            PrecacheModel(g_sCvar_CustomModelVMT, true);
        }
        else
        {
            PrecacheModel(g_sCvar_AliveModel, true);
        }
    }

    if (g_bCvar_DeadShow)
        PrecacheModel(g_sCvar_DeadModel, true);
}

/****************************************************************************************************/

public void LateLoad()
{
    for (int client = 1; client <= MaxClients; client++)
    {
        if (!IsPlayerTank(client))
            continue;

        TankSprite(client);
    }
}

/****************************************************************************************************/

public void OnClientDisconnect(int client)
{
    if (!g_bConfigLoaded)
        return;

    gc_iTankSpriteRef[client] = INVALID_ENT_REFERENCE;
    gc_iTankSpriteFrameRef[client] = INVALID_ENT_REFERENCE;

    for (int target = 1; target <= MaxClients; target++)
    {
        gc_bVisible[target][client] = false;
        gc_fLastAttack[target][client] = 0.0;
    }
}

/****************************************************************************************************/

public void OnEntityDestroyed(int entity)
{
    if (!g_bConfigLoaded)
        return;

    if (!IsValidEntityIndex(entity))
        return;

    ge_bInvalidTrace[entity] = false;
    ge_iOwner[entity] = 0;
}

/****************************************************************************************************/

public void OnEntityCreated(int entity, const char[] classname)
{
    if (!g_bConfigLoaded)
        return;

    if (!IsValidEntityIndex(entity))
        return;

    switch (classname[0])
    {
        case 't':
        {
            if (StrEqual(classname, CLASSNAME_TANK_ROCK))
                ge_bInvalidTrace[entity] = true;
        }
    }
}

/****************************************************************************************************/

public void HookEvents(bool hook)
{
    if (hook && !g_bEventsHooked)
    {
        g_bEventsHooked = true;

        HookEvent("tank_spawn", Event_TankSpawn);
        HookEvent("player_hurt", Event_PlayerHurt);

        return;
    }

    if (!hook && g_bEventsHooked)
    {
        g_bEventsHooked = false;

        UnhookEvent("tank_spawn", Event_TankSpawn);
        UnhookEvent("player_hurt", Event_PlayerHurt);

        return;
    }
}

/****************************************************************************************************/

public void Event_TankSpawn(Event event, const char[] name, bool dontBroadcast)
{
    int client = GetClientOfUserId(event.GetInt("userid"));

    if (!IsValidClient(client))
        return;

    TankSprite(client);
}

/****************************************************************************************************/

public void Event_PlayerHurt(Event event, const char[] name, bool dontBroadcast)
{
    if (!g_bCvar_AttackDelay)
        return;

    int target = GetClientOfUserId(event.GetInt("userid"));

    if (!IsPlayerTank(target))
        return;

    int attacker = GetClientOfUserId(event.GetInt("attacker"));

    if (!IsValidClient(attacker))
        return;

    if (GetClientTeam(attacker) != TEAM_SURVIVOR)
        return;

    gc_fLastAttack[target][attacker] = GetGameTime();
}

/****************************************************************************************************/

public Action TimerKill(Handle timer)
{
    if (!g_bConfigLoaded)
        return Plugin_Continue;

    for (int target = 1; target <= MaxClients; target++)
    {
        if (gc_iTankSpriteRef[target] == INVALID_ENT_REFERENCE)
            continue;

        if (g_bCvar_Enabled && IsPlayerTank(target))
            continue;

        int entity = EntRefToEntIndex(gc_iTankSpriteRef[target]);

        if (entity != INVALID_ENT_REFERENCE)
            AcceptEntityInput(entity, "Kill");

        gc_iTankSpriteRef[target] = INVALID_ENT_REFERENCE;
        gc_iTankSpriteFrameRef[target] = INVALID_ENT_REFERENCE;

        for (int client = 1; client <= MaxClients; client++)
        {
            gc_bVisible[target][client] = false;
            gc_fLastAttack[target][client] = 0.0;
        }
    }

    return Plugin_Continue;
}

/****************************************************************************************************/

public Action TimerVisible(Handle timer)
{
    if (!g_bConfigLoaded)
        return Plugin_Continue;

    if (!g_bCvar_Enabled)
        return Plugin_Continue;

    for (int target = 1; target <= MaxClients; target++)
    {
        if (gc_iTankSpriteRef[target] == INVALID_ENT_REFERENCE)
            continue;

        if (!IsClientInGame(target))
            continue;

        for (int client = 1; client <= MaxClients; client++)
        {
            gc_bVisible[target][client] = false;

            if (!IsClientInGame(client))
                continue;

            if (IsFakeClient(client))
                continue;

            if (!(GetTeamFlag(GetClientTeam(client)) & g_iCvar_Team))
                continue;

            if (g_bCvar_AttackDelay || g_bCvar_Sight)
            {
                if (GetClientTeam(client) == TEAM_SURVIVOR || GetClientTeam(client) == TEAM_HOLDOUT)
                {
                    if (g_bCvar_AttackDelay && (GetGameTime() - gc_fLastAttack[target][client] > g_fCvar_AttackDelay))
                        continue;

                    if (g_bCvar_Sight && !IsVisibleTo(client, target))
                        continue;
                }
            }

            gc_bVisible[target][client] = true;
        }
    }

    return Plugin_Continue;
}

/****************************************************************************************************/

public Action TimerRender(Handle timer)
{
    if (!g_bConfigLoaded)
        return Plugin_Continue;

    if (!g_bCvar_Enabled)
        return Plugin_Continue;

    for (int target = 1; target <= MaxClients; target++)
    {
        if (!IsPlayerTank(target))
            continue;

        TankSprite(target);
    }

    return Plugin_Continue;
}

/****************************************************************************************************/

public void TankSprite(int client)
{
    int entity = INVALID_ENT_REFERENCE;

    if (gc_iTankSpriteRef[client] != INVALID_ENT_REFERENCE)
        entity = EntRefToEntIndex(gc_iTankSpriteRef[client]);

    if (entity == INVALID_ENT_REFERENCE)
    {
        char targetname[22];
        FormatEx(targetname, sizeof(targetname), "%s-%i", "l4d_tank_hp_sprite", client);

        entity = CreateEntityByName(CLASSNAME_ENV_SPRITE);
        ge_iOwner[entity] = client;
        gc_iTankSpriteRef[client] = EntIndexToEntRef(entity);
        DispatchKeyValue(entity, "targetname", targetname);
        DispatchKeyValue(entity, "spawnflags", "1");
        DispatchKeyValue(entity, "fademindist", g_sCvar_FadeDistance);
        SetEntProp(entity, Prop_Data, "m_iHammerID", -1);
        SDKHook(entity, SDKHook_SetTransmit, OnSetTransmit);
    }

    if (g_bCvar_CustomModel)
    {
        int entityFrame = INVALID_ENT_REFERENCE;

        if (gc_iTankSpriteFrameRef[client] != INVALID_ENT_REFERENCE)
            entityFrame = EntRefToEntIndex(gc_iTankSpriteFrameRef[client]);

        if (entityFrame == INVALID_ENT_REFERENCE)
        {
            char targetname[22];
            FormatEx(targetname, sizeof(targetname), "%s-%i", "l4d_tank_hp_sprite", client);

            entityFrame = CreateEntityByName(CLASSNAME_ENV_TEXTURETOGGLE);
            gc_iTankSpriteFrameRef[client] = EntIndexToEntRef(entityFrame);
            DispatchKeyValue(entityFrame, "targetname", "l4d_tank_hp_sprite");
            DispatchKeyValue(entityFrame, "target", targetname);

            TeleportEntity(entityFrame, g_fVPos, NULL_VECTOR, NULL_VECTOR);
            DispatchSpawn(entityFrame);
            ActivateEntity(entityFrame);

            SetVariantString("!activator");
            AcceptEntityInput(entityFrame, "SetParent", entity);
        }
    }

    if (IsPlayerIncapacitated(client))
    {
        if (g_bCvar_DeadShow)
        {
            DispatchKeyValue(entity, "model", g_sCvar_DeadModel);
            DispatchKeyValue(entity, "rendercolor", g_sCvar_DeadColor);
            DispatchKeyValue(entity, "renderamt", g_sCvar_DeadAlpha); // If renderamt goes before rendercolor, it doesn't render
            DispatchKeyValue(entity, "scale", g_sCvar_DeadScale);

            TeleportEntity(entity, g_fVPos, NULL_VECTOR, NULL_VECTOR);
            DispatchSpawn(entity);
            ActivateEntity(entity);

            SetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity", client);
            SetVariantString("!activator");
            AcceptEntityInput(entity, "SetParent", client);
            AcceptEntityInput(entity, "ShowSprite");
        }

        return;
    }

    if (!g_bCvar_AliveShow)
    {
        AcceptEntityInput(entity, "HideSprite");
        return;
    }

    int maxHealth = GetEntProp(client, Prop_Data, "m_iMaxHealth");
    int currentHealth = GetClientHealth(client);

    float percentageHealth;
    if (maxHealth > 0)
        percentageHealth = (float(currentHealth) / float(maxHealth));

    bool halfHealth = (percentageHealth <= 0.5);

    char sRenderColor[12];
    Format(sRenderColor, sizeof(sRenderColor), "%i %i 0", halfHealth ? 255 : RoundFloat(255.0 * ((1.0 - percentageHealth) * 2)), halfHealth ? RoundFloat(255.0 * (percentageHealth) * 2) : 255);

    DispatchKeyValue(entity, "model", g_bCvar_CustomModel ? g_sCvar_CustomModelVMT : g_sCvar_AliveModel);
    DispatchKeyValue(entity, "rendercolor", sRenderColor);
    DispatchKeyValue(entity, "renderamt", g_sCvar_AliveAlpha); // If renderamt goes before rendercolor, it doesn't render
    DispatchKeyValue(entity, "scale", g_sCvar_AliveScale);

    TeleportEntity(entity, g_fVPos, NULL_VECTOR, NULL_VECTOR);
    DispatchSpawn(entity);
    ActivateEntity(entity);

    SetEntPropEnt(entity, Prop_Send, "m_hOwnerEntity", client);
    SetVariantString("!activator");
    AcceptEntityInput(entity, "SetParent", client);
    AcceptEntityInput(entity, "ShowSprite");

    if (!g_bCvar_CustomModel)
        return;

    int entityFrame = EntRefToEntIndex(gc_iTankSpriteFrameRef[client]);

    if (entityFrame == INVALID_ENT_REFERENCE)
        return;

    int frame = RoundFloat(percentageHealth * 100);

    char input[38];
    FormatEx(input, sizeof(input), "OnUser1 !self:SetTextureIndex:%i:0:1", frame);
    SetVariantString(input);
    AcceptEntityInput(entityFrame, "AddOutput");
    AcceptEntityInput(entityFrame, "FireUser1");
}

/****************************************************************************************************/

public Action OnSetTransmit(int entity, int client)
{
    int owner = ge_iOwner[entity];

    if (owner == client)
        return Plugin_Handled;

    if (gc_bVisible[owner][client])
        return Plugin_Continue;

    return Plugin_Handled;
}

/****************************************************************************************************/

bool IsVisibleTo(int client, int target)
{
    float vClientPos[3];
    float vEntityPos[3];
    float vLookAt[3];
    float vAng[3];

    GetClientEyePosition(client, vClientPos);
    GetClientEyePosition(target, vEntityPos);
    MakeVectorFromPoints(vClientPos, vEntityPos, vLookAt);
    GetVectorAngles(vLookAt, vAng);

    Handle trace = TR_TraceRayFilterEx(vClientPos, vAng, MASK_PLAYERSOLID, RayType_Infinite, TraceFilter, target);

    bool isVisible;

    if (TR_DidHit(trace))
    {
        isVisible = (TR_GetEntityIndex(trace) == target);

        if (!isVisible)
        {
            vEntityPos[2] -= 62.0; // results the same as GetClientAbsOrigin

            delete trace;
            trace = TR_TraceHullFilterEx(vClientPos, vEntityPos, g_fVPlayerMins, g_fVPlayerMaxs, MASK_PLAYERSOLID, TraceFilter, target);

            if (TR_DidHit(trace))
                isVisible = (TR_GetEntityIndex(trace) == target);
        }
    }

    delete trace;

    return isVisible;
}

/****************************************************************************************************/

public bool TraceFilter(int entity, int contentsMask, int client)
{
    if (entity == client)
        return true;

    if (IsValidClientIndex(entity))
        return false;

    return ge_bInvalidTrace[entity] ? false : true;
}

/****************************************************************************************************/

public Action CmdPrintCvars(int client, int args)
{
    PrintToConsole(client, "");
    PrintToConsole(client, "======================================================================");
    PrintToConsole(client, "");
    PrintToConsole(client, "----------------- Plugin Cvars (l4d_tank_hp_sprite) ------------------");
    PrintToConsole(client, "");
    PrintToConsole(client, "l4d_tank_hp_sprite_version : %s", PLUGIN_VERSION);
    PrintToConsole(client, "l4d_tank_hp_sprite_enable : %b (%s)", g_bCvar_Enabled, g_bCvar_Enabled ? "true" : "false");
    PrintToConsole(client, "l4d_tank_hp_sprite_z_axis : %.2f", g_fCvar_ZAxis);
    PrintToConsole(client, "l4d_tank_hp_sprite_fade_distance : %i", g_iCvar_FadeDistance);
    PrintToConsole(client, "l4d_tank_hp_sprite_sight : %b (%s)", g_bCvar_Sight, g_bCvar_Sight ? "true" : "false");
    PrintToConsole(client, "l4d_tank_hp_sprite_attack_delay : %.2f (%s)", g_fCvar_AttackDelay, g_bCvar_AttackDelay ? "true" : "false");
    PrintToConsole(client, "l4d_tank_hp_sprite_alive_show : %b (%s)", g_bCvar_AliveShow, g_bCvar_AliveShow ? "true" : "false");
    PrintToConsole(client, "l4d_tank_hp_sprite_alive_model : \"%s\"", g_sCvar_AliveModel);
    PrintToConsole(client, "l4d_tank_hp_sprite_alive_alpha : %i", g_iCvar_AliveAlpha);
    PrintToConsole(client, "l4d_tank_hp_sprite_alive_scale : %.2f", g_fCvar_AliveScale);
    PrintToConsole(client, "l4d_tank_hp_sprite_dead_show : %b (%s)", g_bCvar_DeadShow, g_bCvar_DeadShow ? "true" : "false");
    PrintToConsole(client, "l4d_tank_hp_sprite_dead_model : \"%s\"", g_sCvar_DeadModel);
    PrintToConsole(client, "l4d_tank_hp_sprite_dead_alpha : %i", g_iCvar_DeadAlpha);
    PrintToConsole(client, "l4d_tank_hp_sprite_dead_scale : %.2f", g_fCvar_DeadScale);
    PrintToConsole(client, "l4d_tank_hp_sprite_dead_color : \"%s\"", g_sCvar_DeadColor);
    PrintToConsole(client, "l4d_tank_hp_sprite_team : %i", g_iCvar_Team);
    PrintToConsole(client, "l4d_tank_hp_sprite_custom_model : %b (%s)", g_bCvar_CustomModel, g_bCvar_CustomModel ? "true" : "false");
    PrintToConsole(client, "l4d_tank_hp_sprite_custom_model_vmt : \"%s\"", g_sCvar_CustomModelVMT);
    PrintToConsole(client, "l4d_tank_hp_sprite_custom_model_vtf : \"%s\"", g_sCvar_CustomModelVTF);
    PrintToConsole(client, "");
    PrintToConsole(client, "======================================================================");
    PrintToConsole(client, "");

    return Plugin_Handled;
}

// ====================================================================================================
// Helpers
// ====================================================================================================
/**
 * Validates if is a valid client index.
 *
 * @param client        Client index.
 * @return              True if client index is valid, false otherwise.
 */
bool IsValidClientIndex(int client)
{
    return (1 <= client <= MaxClients);
}

/****************************************************************************************************/

/**
 * Validates if is a valid client.
 *
 * @param client        Client index.
 * @return              True if client index is valid and client is in game, false otherwise.
 */
bool IsValidClient(int client)
{
    return (IsValidClientIndex(client) && IsClientInGame(client));
}

/****************************************************************************************************/

/**
 * Validates if is a valid entity index (between MaxClients+1 and 2048).
 *
 * @param entity        Entity index.
 * @return              True if entity index is valid, false otherwise.
 */
bool IsValidEntityIndex(int entity)
{
    return (MaxClients+1 <= entity <= GetMaxEntities());
}

/****************************************************************************************************/

/**
 * Gets the client L4D1/L4D2 zombie class id.
 *
 * @param client        Client index.
 * @return L4D1         1=SMOKER, 2=BOOMER, 3=HUNTER, 4=WITCH, 5=TANK, 6=NOT INFECTED
 * @return L4D2         1=SMOKER, 2=BOOMER, 3=HUNTER, 4=SPITTER, 5=JOCKEY, 6=CHARGER, 7=WITCH, 8=TANK, 9=NOT INFECTED
 */
int GetZombieClass(int client)
{
    return (GetEntProp(client, Prop_Send, "m_zombieClass"));
}

/****************************************************************************************************/

/**
 * Returns is a player is in ghost state.
 *
 * @param client        Client index.
 * @return              True if client is in ghost state, false otherwise.
 */
bool IsPlayerGhost(int client)
{
    return (GetEntProp(client, Prop_Send, "m_isGhost") == 1);
}

/****************************************************************************************************/

/**
 * Validates if the client is incapacitated.
 *
 * @param client        Client index.
 * @return              True if the client is incapacitated, false otherwise.
 */
bool IsPlayerIncapacitated(int client)
{
    return (GetEntProp(client, Prop_Send, "m_isIncapacitated") == 1);
}

/****************************************************************************************************/

/**
 * Returns if the client is a valid tank.
 *
 * @param client        Client index.
 * @return              True if client is a tank, false otherwise.
 */
bool IsPlayerTank(int client)
{
    bool isValid = IsValidClient(client) && GetClientTeam(client) == TEAM_INFECTED && IsPlayerAlive(client) && !IsPlayerGhost(client);
    if(!g_hCvar_AllSpecials.BoolValue && GetZombieClass(client) != g_iTankClass)
        return false;
    else
        return isValid;
}

/****************************************************************************************************/

/**
 * Returns the team flag from a team.
 *
 * @param team          Team index.
 * @return              Team flag.
 */
int GetTeamFlag(int team)
{
    switch (team)
    {
        case TEAM_SURVIVOR:
            return FLAG_TEAM_SURVIVOR;
        case TEAM_INFECTED:
            return FLAG_TEAM_INFECTED;
        case TEAM_SPECTATOR:
            return FLAG_TEAM_SPECTATOR;
        case TEAM_HOLDOUT:
            return FLAG_TEAM_HOLDOUT;
        default:
            return FLAG_TEAM_NONE;
    }
}