[TUT] Recording Damage/Hits Per-Round and Displaying It

For scripting examples for learning how to use AMXX.
User avatar
Archive-Poster
Posts: 1307
Gender:
United States of America

[TUT] Recording Damage/Hits Per-Round and Displaying It

Post by Archive-Poster »

This tutorial has been written under the assumption that you are a professional Fortran programmer beginner to intermediate level AMXX scripter, but is written to be extremely newbie-friendly.

Alright, let's get straight to the point. The following will teach you how to create your own damage recording system that will reset every round. If you've ever played in ESEA,
this is extremely similar (if not exactly) like the .dmg command.

Let's begin with the top.

Code: Select all

#include <amxmodx>
#include <hamsandwich>

#define MAX_PLAYERS    32

#define IsPlayer(%1)    (1<=%1<=g_iMaxPlayers)

// Time after round start the stats reset
#define RESET_DELAY    5.0

/* To store the damage, we need 2
indexes to be saved in each variable */
new iDamageTaken[MAX_PLAYERS+1]/*took x damage from*/[MAX_PLAYERS+1]
new iDamageGiven[MAX_PLAYERS+1]/*did x damage to*/[MAX_PLAYERS+1]

new iHitsTaken[MAX_PLAYERS+1]/*took x hits from*/[MAX_PLAYERS+1]
new iHitsGiven[MAX_PLAYERS+1]/*did x hits to*/[MAX_PLAYERS+1]

new b_CanTypeWhileAlive

new g_iMaxPlayers 
Now, you'll see that we have our IsPlayer macro which will be explained a little later on.

We also have RESET_DELAY. This is the time after round start that the damage count will be reset.

Most importantly, we have our two variables that eventually will be holding the damage amount that tells the plugin how much damage was received and taken for each player. The two array sizes (MAX_PLAYERS+1) are necessary to hold player-specific data.
We also have the HitsGiven variable, pretty self-explanatory.

You will see b_CanTypeWhileAlive in use later on.
Finally, there's g_iMaxPlayers which will be used in accordance with IsPlayer.

Let's move on, shall we?

Code: Select all

public plugin_init()
{
    register_plugin( "Damage Saving Example", "1.0", "Wrecked" ) // :avast:
    
    RegisterHam( Ham_TakeDamage, "player", "HAM_TakeDamage_Post", 1 )
    
    register_clcmd( "say .zomggaben", "CMD_ShowDamage" )
    
    register_logevent( "LEV_RoundStart", 2, "1=Round_Start" )
    
    g_iMaxPlayers = get_maxplayers()
} 
This one's pretty self-explanatory. We register the plugin, clcmd to check how much damage has been given, the TakeDamage event (which is used to store damage between the two players), and the round start.

g_iMaxPlayers obtains the maximum players, which will be used with IsPlayer, as said earlier.

Next!

Code: Select all

public HAM_TakeDamage_Post( victim, inflictor, attacker, float:damage, damagebits )
{
    if( !IsPlayer( attacker ) ) // make sure it's not generic damage
        return HAM_IGNORED;
        
    iDamageTaken[victim][attacker] += pev( victim, pev_dmg_take )
    iDamageGiven[attacker][victim] += pev( victim, pev_dmg_take )
    
    iHitsTaken[victim][attacker]++ // adds 1
    iHitsGiven[attacker][victim]++ // ...
    
    return HAM_IGNORED;
} 
Finally! Some real code!

First off, you'll notice the IsPlayer being put to use. This macro makes sure that the specified attacker is a player, and not the map or other entity. Without this check, there's a strong possibility that an excessive amount of babies problems would be created.
The macro up top makes sure the player is within the player range, since each player is given an imaginary number on connect (correct me if I'm wrong, anybody), and we check to see if they're between 0 and 32 (legitimate player range).

Next, we have our two variables. These two will be holding the amount of damage given and recieved. The two indexes hold the players involved in the damage.
Also, pev( victim, pev_dmg_take ) is getting the ACTUAL amount of damage taken after taken into consideration the armor and other factors of the victim (thanks to Connor, Arkshine, and Bugsy).

Finally, there's iHitsTaken and iHitsGiven to record the amount of hits given and taken, just to make your life more difficult.

Alright, the following two blocks of code are going to be a bit more difficult to comprehend, but I'll try my best to explain them in simple terms.

Code: Select all

public CMD_ShowDamage( id )
{
    if( is_user_alive( id ) && !b_CanTypeWhileAlive )
    {
        client_print( id, print_chat, "Hey! You can't do that right now!" )
        
        return PLUGIN_CONTINUE;
    }
    
    new iPlayers[32]
    new iNum
    new plrid
    
    get_players( iPlayers, iNum )
    
    static name[32]
    static message[108]
    
    new CsTeams:idteam = cs_get_user_team( id )
    
    for( new i = 0; i < iNum; i++ )
    {
        plrid = iPlayers[i]
        
        if( id == plrid || cs_get_user_team( plrid ) == idteam )
        {
            continue;
        }
        
        get_user_name( plrid, name, 31 )
        
        formatex(
        message, 107, "Given: ( %i Damage / %i Hits ) Taken: ( %i Damage / %i Hits )",
        iDamageGiven[id][plrid],
        iHitsGiven[id][plrid],
        iDamageTaken[id][plrid],
        iHitsTaken[id][plrid]
        )
        
        client_print( id, print_chat, "[%s] %s", name, message )
    }
    
    return PLUGIN_CONTINUE;
} 
Alright, this is the function that's called when a user types .damage in chat. At the top, you can see that I check is_user_alive. If he IS alive, I tell him he can't do this right now, and then return PLUGIN_CONTINUE. What this does is stop the function from executing any further code in that function, but continue with normal operation. This means the chat message will be sent. If I wanted to block it, I'd return PLUGIN_HANDLED.

Now, the rest of the code may come off as a bit confusing if you're new. What I'm doing at first is getting the amount of players in-game and then declaring a new variable that will hold the name and message that will be sent.

I then initiate the for loop, declaring a new variable, making the conditional, and then the increment. This declares the new variable, then checks the conditional. If the conditional is true, it executes the code inside the block. After that, it executes the third param of the loop, and repeats from the 2nd param. When the conditional is false, it ends the for loop.
Inside the for loop, I check if the current player it's looping through is the player we're display it to, and if the player's team is the same as the player we're looping through. If it is, we continue to the next iteration (go back to the conditional) of the loop.

Otherwise, we get their name, and format a message that we'll be displaying to the player. It will look something like

Code: Select all

[Wrecked] Given: ( 49 Damage / 2 Hits ) Taken: ( 100 Damage / 4 Hits )
If you're unfamiliar, %i tells the plugin to replace it with an integer, which is pointed out in the parameters after the message.
We then display the message to the user (%s means replace it with a string), and return PLUGIN_CONTINUE to let the say command run through and let everyone else see what the original client command was.

Phewf! Next!

Code: Select all

public LEV_RoundStart()
{
    #if defined RESET_DELAY
        set_task( RESET_DELAY, "TASK_ResetStats" )
    #else
        TASK_ResetStats()
    #endif
    
    b_CanTypeWhileAlive = true
}

public TASK_ResetStats()
{
    new iPlayers[32]
    new iNum
    new plrid
    new otherid
    
    get_players( iPlayers, iNum )
    
    for( new i = 0; i < iNum; i++ )
    {
        plrid = iPlayers[i]
        
        for( new j = 0; j < iNum; j++ )
        {
            otherid = iPlayers[j]
            
            if( otherid == plrid )
            {
                continue;
            }
            
            iDamageTaken[plrid][otherid] = 0
            iDamageGiven[plrid][otherid] = 0
            
            iHitsTaken[plrid][otherid] = 0
            iHitsGiven[plrid][otherid] = 0
        }
    }
    
    b_CanTypeWhileAlive = false
} 
I'm going to assume you read the above example, and skip the explanation of for loops, since there's two of them in here and it'd be wasting both of our time.

The RoundStart function sets the delay to reset the player data if it's defined. If it's not, the function automatically calls TASK_ResetStats to reset the player damage / hit data.
The bool lets players type .damage until the player stats are reset in the following function.

So, as you can see, it's a quite extensive function, as it has 2 for loops, but it really isn't much code at all.
What I'm doing is;
  • Looping through each player
  • Looping through each player every time we loop through a different player (yes, very memory extensive, let me know if you have a better solution)
  • Resetting the damage and hit data in relation to each player with every bi-player combo possible.
So, we reset the player data in relation to the original player index with the new player index in relation to the secondary player index.

If you have any personal questions on this function, let me know and I'll explain it more in-depth.


So, our final plugin should look something like this:

Code: Select all

#include <amxmodx>
#include <hamsandwich>
#include <cstrike>

#define MAX_PLAYERS    32

#define IsPlayer(%1)    (1<=%1<=g_iMaxPlayers)

// Time after round start the stats reset
#define RESET_DELAY    5.0

/* To store the damage, we need 2
indexes to be saved in each variable */
new iDamageTaken[MAX_PLAYERS+1]/*took x damage from*/[MAX_PLAYERS+1]
new iDamageGiven[MAX_PLAYERS+1]/*did x damage to*/[MAX_PLAYERS+1]

new iHitsTaken[MAX_PLAYERS+1]/*took x hits from*/[MAX_PLAYERS+1]
new iHitsGiven[MAX_PLAYERS+1]/*did x hits to*/[MAX_PLAYERS+1]

new bool:b_CanTypeWhileAlive

new g_iMaxPlayers

public plugin_init()
{
    register_plugin( "Damage Saving Example", "1.0", "Wrecked" ) // :avast:
    
    RegisterHam( Ham_TakeDamage, "player", "HAM_TakeDamage_Post", 1 )
    
    register_clcmd( "say .damage", "CMD_ShowDamage" )
    
    register_logevent( "LEV_RoundStart", 2, "1=Round_Start" )
    
    g_iMaxPlayers = get_maxplayers()
}

public LEV_RoundStart()
{
    #if defined RESET_DELAY
        set_task( RESET_DELAY, "TASK_ResetStats" )
    #else
        TASK_ResetStats()
    #endif
    
    b_CanTypeWhileAlive = true
}
    

public TASK_ResetStats()
{
    new iPlayers[32]
    new iNum
    new plrid
    new otherid
    
    get_players( iPlayers, iNum )
    
    for( new i = 0; i < iNum; i++ )
    {
        plrid = iPlayers[i]
        
        for( new j = 0; j < iNum; j++ )
        {
            otherid = iPlayers[j]
            
            if( otherid == plrid )
            {
                continue;
            }
            
            iDamageTaken[plrid][otherid] = 0
            iDamageGiven[plrid][otherid] = 0
            
            iHitsTaken[plrid][otherid] = 0
            iHitsGiven[plrid][otherid] = 0
        }
    }
    
    b_CanTypeWhileAlive = false
}

public HAM_TakeDamage_Post( victim, inflictor, attacker, float:damage, damagebits )
{
    if( !IsPlayer( attacker ) ) // make sure it's not generic damage
        return HAM_IGNORED;
        
    iDamageTaken[victim][attacker] += pev( victim, pev_dmg_take ) // record the damage
    iDamageGiven[attacker][victim] += pev( victim, pev_dmg_take ) // ...
    
    iHitsTaken[victim][attacker]++ // adds 1
    iHitsGiven[attacker][victim]++ // ...
    
    return HAM_IGNORED;
}

public CMD_ShowDamage( id )
{
    if( is_user_alive( id ) && !b_CanTypeWhileAlive )
    {
        client_print( id, print_chat, "Hey! You can't do that right now!" )
        
        return PLUGIN_CONTINUE;
    }
    
    new iPlayers[32]
    new iNum
    new plrid
    
    get_players( iPlayers, iNum )
    
    static name[32]
    static message[108]
    
    new CsTeams:idteam = cs_get_user_team( id )
    
    for( new i = 0; i < iNum; i++ )
    {
        plrid = iPlayers[i]
        
        if( id == plrid || cs_get_user_team( plrid ) == idteam )
        {
            continue;
        }
        
        get_user_name( plrid, name, 31 )
        
        formatex(
        message, 107, "Given: ( %i Damage / %i Hits ) Taken: ( %i Damage / %i Hits )",
        iDamageGiven[id][plrid],
        iHitsGiven[id][plrid],
        iDamageTaken[id][plrid],
        iHitsTaken[id][plrid]
        )
        
        client_print( id, print_chat, "[%s] %s", name, message )
    }
    
    return PLUGIN_CONTINUE;
} 
Notes
The plugin has been compiled and tested, it works.
I'm aware that creating a triearray for the values would be very efficient, but I felt triearrays would be a little controversial for new scripters and a little confusing.
User avatar
Archive-Poster
Posts: 1307
Gender:
United States of America

Re: [TUT] Recording Damage/Hits Per-Round and Displaying It

Post by Archive-Poster »

Just to let you know that damage param passed in TakeDamage is not the real damage taken/given, to retrieve the real damage done, you have to check pev_dmg_take value in TakeDamage post registered.
You could also hook Damage event as well.

It work only with few damage types as DMG_FALL, because original damage is equal to final one, but for bullet dmg it's always different.
Also, csx should provide some natives that can give you all information.

pev_dmg_take is normally a float, but you can retrieve its integer value using it as if it was an integer, pev(victim, pev_dmg_take) will work fine.
Also, if you look at HLSDK, TakeDamage returns the damage done, but it's not the case on counter-strike.
You could extend your snippet to specific weapon damage save ;)

But first, double check that you can't achieve this with csx module, because you would make it for nothing
Post Reply

Who is online

Users browsing this forum: No registered users and 1 guest