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



/*
 * Corpse Gore 2.0
 * Refactored from Corpse Gore by RedSMURF
 *
 * AMX Mod X / Counter-Strike 1.6
 *
 * Cvars:
 *   corpsegore_enable             1
 *   corpsegore_gib_knife          1
 *   corpsegore_gib_explosive      1
 *   corpsegore_break_velocity     12
 *   corpsegore_break_count        12
 *   corpsegore_break_life         50
 *   corpsegore_blood_color        247
 *   corpsegore_blood_scale        10
 *
 * Notes:
 *   - Preserves the original ClCorpse interception.
 *   - Uses a bounded collision box calculated from model bones.
 *   - Limits the wait for a clear corpse position.
 *   - Cleans up corpse entities and their tasks.
 */

#include <amxmodx>
#include <fakemeta>
#include <hamsandwich>

#define PLUGIN_NAME             "Corpse Gore"
#define PLUGIN_VERSION          "2.0"
#define PLUGIN_AUTHOR           "RedSMURF / Refactor"

#define CORPSE_CLASS            "corpse_gore"
#define CORPSE_KEY              981222

#define CORPSE_PENDING          pev_iuser1
#define CORPSE_CREATED_AT       pev_fuser1

#define CORPSE_BONES            36

#define CORPSE_BBOX_PAD         4.0
#define CORPSE_BBOX_MIN_X       20.0
#define CORPSE_BBOX_MIN_Y       12.0
#define CORPSE_BBOX_MAX_X       40.0
#define CORPSE_BBOX_MAX_Y       28.0

#define CORPSE_BBOX_HEIGHT      4.0

#define CORPSE_WAIT_INTERVAL    0.15
#define CORPSE_WAIT_TIMEOUT     2.0

#define CORPSE_DMG_EXPLOSIVE    ((1 << 6) | (1 << 24))
#define CORPSE_BREAK_FLESH      0x04

#define MAX_CLIENTS             32

new g_pEnable;
new g_pGibKnife;
new g_pGibExplosive;
new g_pBreakVelocity;
new g_pBreakCount;
new g_pBreakLife;
new g_pBloodColor;
new g_pBloodScale;

new bool:g_bEnabled;
new bool:g_bGibKnife;
new bool:g_bGibExplosive;

new g_iBreakVelocity;
new g_iBreakCount;
new g_iBreakLife;
new g_iBloodColor;
new g_iBloodScale;

new g_iBloodSprite;
new g_iBloodSpraySprite;
new g_iGibsModel;

new g_iMaxPlayers;

new Float:g_flLastKnifeHit[MAX_CLIENTS + 1];

new const g_szKnifeImpactSound[] = "common/bodysplat.wav";
new const g_szKnifeWallSound[] = "weapons/knife_hitwall1.wav";

new const g_iBloodDecals[] = {204, 205};


// --------------------------------------------------------------------------
// Plugin lifecycle
// --------------------------------------------------------------------------

public plugin_precache()
{
    g_iBloodSprite = precache_model("sprites/blood.spr");
    g_iBloodSpraySprite = precache_model("sprites/bloodspray.spr");
    g_iGibsModel = precache_model("models/hgibs.mdl");

    precache_sound(g_szKnifeImpactSound);
}

public plugin_init()
{
    register_plugin(PLUGIN_NAME, PLUGIN_VERSION, PLUGIN_AUTHOR);

    register_cvar(
        "RedSMURF_CorpseGore",
        PLUGIN_VERSION,
        FCVAR_SERVER | FCVAR_SPONLY
    );

    g_pEnable = register_cvar("corpsegore_enable", "1");
    g_pGibKnife = register_cvar("corpsegore_gib_knife", "1");
    g_pGibExplosive = register_cvar("corpsegore_gib_explosive", "1");

    g_pBreakVelocity = register_cvar("corpsegore_break_velocity", "12");
    g_pBreakCount = register_cvar("corpsegore_break_count", "12");
    g_pBreakLife = register_cvar("corpsegore_break_life", "50");

    g_pBloodColor = register_cvar("corpsegore_blood_color", "247");
    g_pBloodScale = register_cvar("corpsegore_blood_scale", "10");

    cacheCvars();

    hook_cvar_change(g_pEnable, "onCvarChanged");
    hook_cvar_change(g_pGibKnife, "onCvarChanged");
    hook_cvar_change(g_pGibExplosive, "onCvarChanged");
    hook_cvar_change(g_pBreakVelocity, "onCvarChanged");
    hook_cvar_change(g_pBreakCount, "onCvarChanged");
    hook_cvar_change(g_pBreakLife, "onCvarChanged");
    hook_cvar_change(g_pBloodColor, "onCvarChanged");
    hook_cvar_change(g_pBloodScale, "onCvarChanged");

    register_message(get_user_msgid("ClCorpse"), "onClCorpse");

    register_forward(FM_EmitSound, "onEmitSound");

    RegisterHam(Ham_TraceAttack, "info_target", "onTraceAttack");
    RegisterHam(Ham_TakeDamage, "info_target", "onTakeDamage");

    register_logevent("onRoundStart", 2, "1=Round_Start");

    g_iMaxPlayers = get_maxplayers();
}

public plugin_end()
{
    removeAllCorpses();
}

public onCvarChanged(pCvar, const szOldValue[], const szNewValue[])
{
    cacheCvars();
}

stock cacheCvars()
{
    g_bEnabled = get_pcvar_num(g_pEnable) != 0;
    g_bGibKnife = get_pcvar_num(g_pGibKnife) != 0;
    g_bGibExplosive = get_pcvar_num(g_pGibExplosive) != 0;

    g_iBreakVelocity = clamp(get_pcvar_num(g_pBreakVelocity), 0, 255);
    g_iBreakCount = clamp(get_pcvar_num(g_pBreakCount), 0, 255);
    g_iBreakLife = clamp(get_pcvar_num(g_pBreakLife), 0, 255);

    g_iBloodColor = clamp(get_pcvar_num(g_pBloodColor), 0, 255);
    g_iBloodScale = clamp(get_pcvar_num(g_pBloodScale), 0, 255);
}


// --------------------------------------------------------------------------
// Corpse creation
// --------------------------------------------------------------------------

public onClCorpse()
{
    if (!g_bEnabled)
        return PLUGIN_CONTINUE;

    new szModel[32];
    get_msg_arg_string(1, szModel, charsmax(szModel));

    new Float:flOrigin[3];
    new Float:flAngles[3];

    // ClCorpse origin is transmitted in 1/128-unit coordinates.
    for (new i; i < 3; i++)
    {
        flOrigin[i] = float(get_msg_arg_int(i + 2)) / 128.0;
        flAngles[i] = get_msg_arg_float(i + 5);
    }

    new iSequence = get_msg_arg_int(9);
    new iBody = get_msg_arg_int(10);

    createCorpse(szModel, flOrigin, flAngles, iSequence, iBody);

    // Suppress the original client corpse.
    return PLUGIN_HANDLED;
}

stock createCorpse(
    const szModel[],
    const Float:flOrigin[3],
    const Float:flAngles[3],
    iSequence,
    iBody
)
{
    new iEnt = create_entity("info_target");

    if (!pev_valid(iEnt))
        return 0;

    new szModelPath[96];

    formatex(
        szModelPath,
        charsmax(szModelPath),
        "models/player/%s/%s.mdl",
        szModel,
        szModel
    );

    set_pev(iEnt, pev_classname, CORPSE_CLASS);
    set_pev(iEnt, pev_impulse, CORPSE_KEY);

    // Initialize entity before setting model and collision.
    dllfunc(DLLFunc_Spawn, iEnt);

    engfunc(EngFunc_SetModel, iEnt, szModelPath);
    engfunc(EngFunc_SetOrigin, iEnt, flOrigin);

    set_pev(iEnt, pev_angles, flAngles);

    set_pev(iEnt, pev_body, iBody);
    set_pev(iEnt, pev_sequence, iSequence);
    set_pev(iEnt, pev_frame, 255.0);
    set_pev(iEnt, pev_framerate, 0.0);
    set_pev(iEnt, pev_animtime, get_gametime());

    set_pev(iEnt, pev_movetype, MOVETYPE_NONE);
    set_pev(iEnt, pev_solid, SOLID_NOT);
    set_pev(iEnt, pev_takedamage, DAMAGE_YES);

    set_pev(iEnt, CORPSE_PENDING, 1);
    set_pev(iEnt, CORPSE_CREATED_AT, get_gametime());

    corpseFitBox(iEnt);

    if (corpseIsBlocked(iEnt))
    {
        set_task(
            CORPSE_WAIT_INTERVAL,
            "taskActivateCorpse",
            iEnt,
            "",
            0,
            "b"
        );
    }
    else
    {
        activateCorpse(iEnt);
    }

    return iEnt;
}


// --------------------------------------------------------------------------
// Collision box
// --------------------------------------------------------------------------

stock corpseFitBox(iEnt)
{
    if (!isCorpse(iEnt))
        return;

    new Float:flOrigin[3];
    new Float:flAngles[3];

    pev(iEnt, pev_origin, flOrigin);
    pev(iEnt, pev_angles, flAngles);

    new Float:flMins[3];
    new Float:flMaxs[3];

    flMins[0] = 9999.0;
    flMins[1] = 9999.0;

    flMaxs[0] = -9999.0;
    flMaxs[1] = -9999.0;

    new iValidBones;

    // GoldSrc yaw is around the Z axis.
    new Float:flYaw = flAngles[1];

    new Float:flCos = floatcos(flYaw, degrees);
    new Float:flSin = floatsin(flYaw, degrees);

    for (new iBone; iBone < CORPSE_BONES; iBone++)
    {
        new Float:flBone[3];
        new Float:flBoneAngles[3];

        engfunc(
            EngFunc_GetBonePosition,
            iEnt,
            iBone,
            flBone,
            flBoneAngles
        );

        new Float:flDX = flBone[0] - flOrigin[0];
        new Float:flDY = flBone[1] - flOrigin[1];

        // Transform world-space bone offsets into yaw-local space.
        new Float:flLocalX = flDX * flCos + flDY * flSin;
        new Float:flLocalY = -flDX * flSin + flDY * flCos;

        // Reject invalid or extreme bone positions.
        if (floatabs(flLocalX) > 96.0 || floatabs(flLocalY) > 96.0)
            continue;

        flMins[0] = floatmin(flMins[0], flLocalX);
        flMins[1] = floatmin(flMins[1], flLocalY);

        flMaxs[0] = floatmax(flMaxs[0], flLocalX);
        flMaxs[1] = floatmax(flMaxs[1], flLocalY);

        iValidBones++;
    }

    // Fallback for models with unusable bone information.
    if (!iValidBones)
    {
        flMins[0] = -32.0;
        flMins[1] = -16.0;

        flMaxs[0] = 32.0;
        flMaxs[1] = 16.0;
    }
    else
    {
        flMins[0] -= CORPSE_BBOX_PAD;
        flMins[1] -= CORPSE_BBOX_PAD;

        flMaxs[0] += CORPSE_BBOX_PAD;
        flMaxs[1] += CORPSE_BBOX_PAD;

        // Clamp the extents so one bad bone cannot produce
        // a huge collision box.
        flMins[0] = floatmax(flMins[0], -CORPSE_BBOX_MAX_X);
        flMins[1] = floatmax(flMins[1], -CORPSE_BBOX_MAX_Y);

        flMaxs[0] = floatmin(flMaxs[0], CORPSE_BBOX_MAX_X);
        flMaxs[1] = floatmin(flMaxs[1], CORPSE_BBOX_MAX_Y);

        // Ensure a minimum width and depth.
        if (flMaxs[0] - flMins[0] < CORPSE_BBOX_MIN_X)
        {
            flMins[0] = -CORPSE_BBOX_MIN_X * 0.5;
            flMaxs[0] = CORPSE_BBOX_MIN_X * 0.5;
        }

        if (flMaxs[1] - flMins[1] < CORPSE_BBOX_MIN_Y)
        {
            flMins[1] = -CORPSE_BBOX_MIN_Y * 0.5;
            flMaxs[1] = CORPSE_BBOX_MIN_Y * 0.5;
        }
    }

    // Trace to the ground to position the thin collision slab.
    new Float:flTraceStart[3];
    new Float:flTraceEnd[3];

    flTraceStart[0] = flOrigin[0];
    flTraceStart[1] = flOrigin[1];
    flTraceStart[2] = flOrigin[2] + 16.0;

    flTraceEnd[0] = flOrigin[0];
    flTraceEnd[1] = flOrigin[1];
    flTraceEnd[2] = flOrigin[2] - 64.0;

    engfunc(
        EngFunc_TraceLine,
        flTraceStart,
        flTraceEnd,
        IGNORE_MONSTERS,
        iEnt,
        0
    );

    new Float:flFraction;
    get_tr2(0, TR_flFraction, flFraction);

    new Float:flFloorOffset;

    if (flFraction < 1.0)
    {
        new Float:flHit[3];
        get_tr2(0, TR_vecEndPos, flHit);

        flFloorOffset = flHit[2] - flOrigin[2];
    }
    else
    {
        // No floor found: keep the box close to the entity origin.
        flFloorOffset = -8.0;
    }

    flMins[2] = flFloorOffset;
    flMaxs[2] = flFloorOffset + CORPSE_BBOX_HEIGHT;

    engfunc(EngFunc_SetSize, iEnt, flMins, flMaxs);
    engfunc(EngFunc_SetOrigin, iEnt, flOrigin);
}


// --------------------------------------------------------------------------
// Deferred activation
// --------------------------------------------------------------------------

public taskActivateCorpse(iEnt)
{
    if (!isCorpse(iEnt) || !pev(iEnt, CORPSE_PENDING))
    {
        remove_task(iEnt);
        return;
    }

    new Float:flCreatedAt;
    pev(iEnt, CORPSE_CREATED_AT, flCreatedAt);

    // Do not wait indefinitely if a player occupies the corpse.
    if (get_gametime() - flCreatedAt >= CORPSE_WAIT_TIMEOUT)
    {
        activateCorpse(iEnt);
        return;
    }

    if (!corpseIsBlocked(iEnt))
        activateCorpse(iEnt);
}

stock activateCorpse(iEnt)
{
    if (!isCorpse(iEnt))
        return;

    new Float:flOrigin[3];
    pev(iEnt, pev_origin, flOrigin);

    set_pev(iEnt, CORPSE_PENDING, 0);
    set_pev(iEnt, pev_solid, SOLID_BBOX);

    engfunc(EngFunc_SetOrigin, iEnt, flOrigin);

    remove_task(iEnt);
}

stock bool:corpseIsBlocked(iEnt)
{
    if (!isCorpse(iEnt))
        return false;

    new Float:flCorpseMin[3];
    new Float:flCorpseMax[3];

    pev(iEnt, pev_absmin, flCorpseMin);
    pev(iEnt, pev_absmax, flCorpseMax);

    for (new id = 1; id <= g_iMaxPlayers; id++)
    {
        if (!is_user_alive(id))
            continue;

        new Float:flPlayerMin[3];
        new Float:flPlayerMax[3];

        pev(id, pev_absmin, flPlayerMin);
        pev(id, pev_absmax, flPlayerMax);

        new bool:bOverlap = true;

        for (new axis; axis < 3; axis++)
        {
            if (
                flPlayerMin[axis] >= flCorpseMax[axis]
                || flPlayerMax[axis] <= flCorpseMin[axis]
            )
            {
                bOverlap = false;
                break;
            }
        }

        if (bOverlap)
            return true;
    }

    return false;
}


// --------------------------------------------------------------------------
// Damage and knife handling
// --------------------------------------------------------------------------

public onTraceAttack(iEnt, iAttacker, Float:flDamage, Float:flDirection[3], iTrace, iDamageBits)
{
    if (!g_bEnabled || !isCorpse(iEnt) || pev(iEnt, CORPSE_PENDING))
        return HAM_IGNORED;

    if (
        g_bGibKnife
        && is_user_alive(iAttacker)
        && get_user_weapon(iAttacker) == CSW_KNIFE
    )
    {
        g_flLastKnifeHit[iAttacker] = get_gametime();

        gibCorpse(iEnt);

        // Prevent normal damage processing on the removed corpse.
        return HAM_SUPERCEDE;
    }

    new Float:flHitPosition[3];
    get_tr2(iTrace, TR_vecEndPos, flHitPosition);

    emitBlood(flHitPosition);

    // Corpses are effect entities; suppress ordinary damage.
    return HAM_SUPERCEDE;
}

public onTakeDamage(iEnt, iInflictor, iAttacker, Float:flDamage, iDamageBits)
{
    if (!g_bEnabled || !isCorpse(iEnt))
        return HAM_IGNORED;

    if (!(iDamageBits & CORPSE_DMG_EXPLOSIVE))
        return HAM_IGNORED;

    if (g_bGibExplosive)
        gibCorpse(iEnt);

    return HAM_SUPERCEDE;
}

public onEmitSound(id, iChannel, const szSample[], Float:flVolume, Float:flAttn, iFlags, iPitch)
{
    if (
        !g_bEnabled
        || id < 1
        || id > g_iMaxPlayers
        || get_gametime() - g_flLastKnifeHit[id] > 0.1
        || !equal(szSample, g_szKnifeWallSound)
    )
    {
        return FMRES_IGNORED;
    }

    engfunc(
        EngFunc_EmitSound,
        id,
        iChannel,
        g_szKnifeImpactSound,
        flVolume,
        flAttn,
        iFlags,
        iPitch
    );

    return FMRES_SUPERCEDE;
}


// --------------------------------------------------------------------------
// Gore effects
// --------------------------------------------------------------------------

stock gibCorpse(iEnt)
{
    if (!isCorpse(iEnt))
        return;

    new Float:flMin[3];
    new Float:flMax[3];

    pev(iEnt, pev_absmin, flMin);
    pev(iEnt, pev_absmax, flMax);

    new Float:flCenter[3];
    new Float:flSize[3];

    flCenter[0] = (flMin[0] + flMax[0]) * 0.5;
    flCenter[1] = (flMin[1] + flMax[1]) * 0.5;
    flCenter[2] = flMin[2] + 8.0;

    flSize[0] = (flMax[0] - flMin[0]) * 0.25;
    flSize[1] = (flMax[1] - flMin[1]) * 0.25;
    flSize[2] = 8.0;

    message_begin_f(MSG_PVS, SVC_TEMPENTITY, flCenter);

    write_byte(TE_BREAKMODEL);

    write_coord_f(flCenter[0]);
    write_coord_f(flCenter[1]);
    write_coord_f(flCenter[2]);

    write_coord_f(flSize[0]);
    write_coord_f(flSize[1]);
    write_coord_f(flSize[2]);

    write_coord_f(0.0);
    write_coord_f(0.0);
    write_coord_f(0.0);

    write_byte(randomCvarRange(g_iBreakVelocity, 2, 0, 255));
    write_short(g_iGibsModel);

    write_byte(randomCvarRange(g_iBreakCount, 3, 0, 255));
    write_byte(randomCvarRange(g_iBreakLife, 15, 0, 255));

    write_byte(CORPSE_BREAK_FLESH);

    message_end();

    emitBlood(flCenter);
    emitBloodDecal(flCenter);

    removeCorpse(iEnt);
}

stock emitBlood(const Float:flPosition[3])
{
    message_begin_f(MSG_PVS, SVC_TEMPENTITY, flPosition);

    write_byte(TE_BLOODSPRITE);

    write_coord_f(flPosition[0]);
    write_coord_f(flPosition[1]);
    write_coord_f(flPosition[2]);

    write_short(g_iBloodSpraySprite);
    write_short(g_iBloodSprite);

    write_byte(g_iBloodColor);
    write_byte(randomCvarRange(g_iBloodScale, 3, 0, 255));

    message_end();
}

stock emitBloodDecal(const Float:flPosition[3])
{
    new Float:flStart[3];
    new Float:flEnd[3];

    flStart[0] = flPosition[0];
    flStart[1] = flPosition[1];
    flStart[2] = flPosition[2];

    flEnd[0] = flStart[0];
    flEnd[1] = flStart[1];
    flEnd[2] = flStart[2] - 24.0;

    engfunc(
        EngFunc_TraceLine,
        flStart,
        flEnd,
        IGNORE_MONSTERS,
        0,
        0
    );

    new Float:flFraction;
    get_tr2(0, TR_flFraction, flFraction);

    if (flFraction >= 1.0)
        return;

    get_tr2(0, TR_vecEndPos, flEnd);

    message_begin_f(MSG_BROADCAST, SVC_TEMPENTITY, flEnd);

    write_byte(TE_WORLDDECAL);

    write_coord_f(flEnd[0]);
    write_coord_f(flEnd[1]);
    write_coord_f(flEnd[2]);

    write_byte(g_iBloodDecals[random_num(0, sizeof(g_iBloodDecals) - 1)]);

    message_end();
}


// --------------------------------------------------------------------------
// Entity utilities
// --------------------------------------------------------------------------

stock bool:isCorpse(iEnt)
{
    return (
        pev_valid(iEnt)
        && pev(iEnt, pev_impulse) == CORPSE_KEY
    );
}

stock removeCorpse(iEnt)
{
    if (!pev_valid(iEnt))
        return;

    remove_task(iEnt);

    engfunc(EngFunc_RemoveEntity, iEnt);
}

stock removeAllCorpses()
{
    new iEnt = -1;

    while (
        (iEnt = engfunc(
            EngFunc_FindEntityByString,
            iEnt,
            "classname",
            CORPSE_CLASS
        ))
    )
    {
        removeCorpse(iEnt);
    }
}

stock randomCvarRange(iValue, iSpread, iMin, iMax)
{
    new iLow = max(iMin, iValue - iSpread);
    new iHigh = min(iMax, iValue + iSpread);

    return random_num(iLow, iHigh);
}


// --------------------------------------------------------------------------
// Round cleanup
// --------------------------------------------------------------------------

public onRoundStart()
{
    removeAllCorpses();
}