SPAS-12 semi-auto/pump-action for HL2
January 2024
You can help by adding links to this article from other relevant articles.
January 2024
Hello, I was asked to make a tutorial in which I show you how to create SPAS-12 with ability to change firing modes (semi-automatic and pump-action). I made it in Visual Studio 2013 and used source-sdk-2013 source code for singleplayer. I also used tools like Crowbar and Blender to make some animations that I needed. BTW it's my first tutorial so I apologise if I made any mistakes.
Contents
First Step
First of all you will need Half-Life 2 Source Code. You'll also need Visual Studio to compile it, I recommend Visual Studio 2013.
Editing Shotgun Code
When you have your Visual Studio and Source Code ready, search for "weapon_shotgun.cpp" file and open it. I'll show and explain everything I added for that code, but if you're not interested in it just copy all the code I put in the last paragraph.
Around line 37 we three bools. Let's add two more.
bool m_Autoloading; // For semi-auto fire mode <--
bool m_bNeedChamber; // When emptied completely <--
bool m_bNeedPump; // Now just pump in pump-action mode.
bool m_bDelayedFire1; // Fire primary when finished reloading
bool m_bDelayedFire2; // Fire secondary when finished reloading
Around line 82 we have some voids, we'll need two more.
void SwitchFiremode( void );
void ShotgunFire(int PelletsNum, int PelletDamage, bool UsePump);
And now around line 100 we have DATADESC for shotgun, we need to add two lines for that bools we added.
DEFINE_FIELD( m_Autoloading, FIELD_BOOLEAN),
DEFINE_FIELD( m_bNeedChamber, FIELD_BOOLEAN),
Around line 160 we have Precache for shotgun, we need to precache two sounds. These are sounds we're going to add in "game_sounds.txt" file in hl2/scripts folder.
PrecacheScriptSound("Weapon_Shotgun.Chamber");
PrecacheScriptSound("Weapon_Shotgun.Pump");
Around line 300 we have "bool CWeaponShotgun::StartReload(void)" and now we need to remove or comment out that:
/*
if (m_iClip1 <= 0)
{
m_bNeedPump = true; // We don't need that anymore
}
*/
We want different animations for "pump" in semi-auto and pump-action modes and add that sounds we precached. We need to go to "void CWeaponShotgun::Pump( void )" around line 415 and well... just remove everything and add that
CBaseCombatCharacter *pOwner = GetOwner();
if ( pOwner == NULL )
return;
m_bNeedPump = false;
if (m_Autoloading)
{
EmitSound("Weapon_Shotgun.Chamber");
SendWeaponAnim(ACT_SHOTGUN_CHAMBER); // I added new activity for new animation, you must add it "activitylist.cpp" and in "ai_activity.cpp" and "ai_activity.h"
// just search for ACT_SHOTGUN_PUMP in entire solution and add ACT_SHOTGUN_CHAMBER under it in these files
// or just change it to ACT_SHOTGUN_PUMP if you don't want different animation.
// Here is example for "activitylist.cpp"
// REGISTER_SHARED_ACTIVITY( ACT_SHOTGUN_PUMP);
// REGISTER_SHARED_ACTIVITY( ACT_SHOTGUN_CHAMBER);
}
else
{
EmitSound("Weapon_Shotgun.Pump");
SendWeaponAnim(ACT_SHOTGUN_PUMP);
}
pOwner->m_flNextAttack = gpGlobals->curtime + SequenceDuration();
m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration();
Around line 460 we have "void CWeaponShotgun::PrimaryAttack(void)" and now just remove everything in it and add this:
// Only the player fires this way so we can cast
CBasePlayer *pPlayer = ToBasePlayer( GetOwner() );
if (!pPlayer)
{
return;
}
if (GetOwner()->GetWaterLevel() == 3) //don't fire under water
{
WeaponSound(EMPTY);
m_flNextPrimaryAttack = gpGlobals->curtime + 0.2;
return;
}
if (!m_bInReload)
{
m_bNeedChamber = false;
}
if (m_Autoloading)
{
ShotgunFire(11, 6, false); //Here we can set (number of pellets, damage per pellet, pump)
}
else
{
ShotgunFire(11, 8, true); //The same as above
}
Under this we have "void CWeaponShotgun::SecondaryAttack( void )" and just like above we remove everything and add this:
CBasePlayer *pPlayer = ToBasePlayer(GetOwner());
if (!pPlayer)
{
return;
}
pPlayer->m_nButtons &= ~IN_ATTACK2;
m_flNextPrimaryAttack = gpGlobals->curtime + 0.3;
m_flNextSecondaryAttack = gpGlobals->curtime + 0.3;
SwitchFiremode();
Now we need to add void we referenced earlier. It's basically our primary attack for shotgun. And here we can for example change fire rate.
void CWeaponShotgun::ShotgunFire(int PelletsNum, int PelletDamage, bool UsePump)
{
CBasePlayer *pPlayer = ToBasePlayer(GetOwner());
if (!pPlayer)
{
return;
}
WeaponSound(SINGLE);
pPlayer->DoMuzzleFlash();
pPlayer->SetAnimation(PLAYER_ATTACK1);
if (m_Autoloading)
{
SendWeaponAnim(ACT_VM_SECONDARYATTACK); // autoloading needs different animation because it ejects shell
m_flNextPrimaryAttack = gpGlobals->curtime + 0.28f; // Rate of fire in autoloading mode (default 214 RPM)
}
else
{
SendWeaponAnim(ACT_VM_PRIMARYATTACK);
m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration(); //When there is SequenceDuration() this has to be under SendWeaponAnim !
}
m_iClip1 -= 1;
Vector vecSrc = pPlayer->Weapon_ShootPosition();
Vector vecAiming = pPlayer->GetAutoaimVector(AUTOAIM_SCALE_DEFAULT);
pPlayer->SetMuzzleFlashTime(gpGlobals->curtime + 1.0);
pPlayer->FireBullets(PelletsNum, vecSrc, vecAiming, GetBulletSpread(), MAX_TRACE_LENGTH, m_iPrimaryAmmoType, 0, -1, -1, PelletDamage, NULL, true, true); // weapon stats
pPlayer->ViewPunch(QAngle(random->RandomFloat(-2, -1), random->RandomFloat(-2, 2), 0));
CSoundEnt::InsertSound(SOUND_COMBAT, GetAbsOrigin(), SOUNDENT_VOLUME_SHOTGUN, 0.2, GetOwner());
if (!m_iClip1 && pPlayer->GetAmmoCount(m_iPrimaryAmmoType) <= 0)
{
// HEV suit - indicate out of ammo condition
pPlayer->SetSuitUpdate("!HEV_AMO0", FALSE, 0);
}
if (m_iClip1)
{
// pump so long as some rounds are left.
m_bNeedPump = UsePump; // we want false in autoloading mode
}
m_iPrimaryAttacks++;
gamestats->Event_WeaponFired(pPlayer, true, GetClassname());
}
Just like above we add another void for switching firing mode.
void CWeaponShotgun::SwitchFiremode(void)
{
CBasePlayer *pPlayer = ToBasePlayer(GetOwner());
if (!pPlayer)
{
return;
}
if (m_Autoloading)
{
WeaponSound(EMPTY);
SendWeaponAnim(ACT_VM_FIDGET); //We can add animation for switch
ClientPrint(pPlayer, HUD_PRINTCENTER, "Switched to pump-action");
m_Autoloading = false;
}
else
{
WeaponSound(EMPTY);
SendWeaponAnim(ACT_VM_FIDGET);
ClientPrint(pPlayer, HUD_PRINTCENTER, "Switched to semi-automatic");
m_Autoloading = true;
}
}
There are a few things we need to change in "void CWeaponShotgun::ItemPostFrame( void )" it should be around 588 line now First thing is here:
// If I'm primary firing and have one round stop reloading and fire
if ((pOwner->m_nButtons & IN_ATTACK) && (m_iClip1 >= 1) && !m_bNeedChamber)// <-- we need to add && !m_bNeedChamber because we can't fire when there is no shell in chamber
Now remove or comment out that because we don't use secondary attack anymore
/*
// If I'm secondary firing and have one round stop reloading and fire
else if ((pOwner->m_nButtons & IN_ATTACK2 ) && (m_iClip1 >=2))//
{
m_bInReload = false;
m_bNeedPump = false;
m_bDelayedFire2 = true;
}
*/
Now we have "else if (m_flNextPrimaryAttack <= gpGlobals->curtime)". We have to add two new conditions.
if (m_bNeedChamber)
{
Pump();
m_bNeedChamber = false;
return;
}
if (m_iClip1 <= 0)
{
Reload();
m_bNeedChamber = true;
return;
}
Now search for that: In my code it's 668 line. Just change PrimaryAttack(); to SecondaryAttack(); just like that:
// If only one shell is left, do a single shot instead
if ( m_iClip1 == 1 )
{
SecondaryAttack();
}
Finally we have the last thing to edit "CWeaponShotgun::CWeaponShotgun( void )" in my code it's line 776. We add : "m_bNeedChamber = false;" and "m_bFiresUnderwater = true;" It looks like that:
CWeaponShotgun::CWeaponShotgun( void )
{
m_bReloadsSingly = true;
m_bNeedPump = false;
m_bNeedChamber = false;
m_bDelayedFire1 = false;
m_bDelayedFire2 = false;
m_fMinRange1 = 0.0;
m_fMaxRange1 = 500;
m_fMinRange2 = 0.0;
m_fMaxRange2 = 200;
m_bFiresUnderwater = true; // We want secondary attack to work under water
}
That's all, coding is done. Now just save all files and rebuild the solution. When compiling is done copy client and server dlls to bin folder.
Adjusting Shotgun Model
Now we need to use Crowbar to decompile and open shotgun model. Open "v_shotgun.qc" file and in $sequence "altfire" change animation path to "v_shotgun_anims\fire01.smd" and add { event 6001 3 "2" } because we want shell eject in automatic mode.
You should also add new sequence for alternate "pump" animation that we added in source code. Mine looks like that:
$sequence "chamber" {
"v_shotgun_anims\chamber.smd"
activity "ACT_SHOTGUN_CHAMBER" 1
fadein 0.2
fadeout 0.2
node "0"
fps 22
}
Here I used that animation I made in Blender. Anyway save that file compile your model and put it into models folder.
Adjusting Shotgun Sounds
Just open "game_sounds.txt" file and add this:
"Weapon_Shotgun.Chamber"
{
"channel" "CHAN_ITEM"
"volume" "0.7"
"soundlevel" "SNDLVL_NORM"
"wave" "weapons/shotgun/shotgun_cock.wav"
}
"Weapon_Shotgun.Pump"
{
"channel" "CHAN_ITEM"
"volume" "0.7"
"soundlevel" "SNDLVL_NORM"
"wave" "weapons/shotgun/shotgun_cock.wav"
}
Entire Shotgun Code
If you don't want to make changes in code manually, just copy and paste that code.
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: A shotgun.
//
// Primary attack: single barrel shot or semi-auto.
// Secondary attack: change firing mode.
//
//=============================================================================//
#include "cbase.h"
#include "npcevent.h"
#include "basehlcombatweapon_shared.h"
#include "basecombatcharacter.h"
#include "ai_basenpc.h"
#include "player.h"
#include "gamerules.h" // For g_pGameRules
#include "in_buttons.h"
#include "soundent.h"
#include "vstdlib/random.h"
#include "gamestats.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
extern ConVar sk_auto_reload_time;
extern ConVar sk_plr_num_shotgun_pellets;
class CWeaponShotgun : public CBaseHLCombatWeapon
{
DECLARE_DATADESC();
public:
DECLARE_CLASS( CWeaponShotgun, CBaseHLCombatWeapon );
DECLARE_SERVERCLASS();
private:
bool m_Autoloading; // For semi-auto fire mode
bool m_bNeedChamber; // When emptied completely
bool m_bNeedPump; // Now just pump in pump-action mode.
bool m_bDelayedFire1; // Fire primary when finished reloading
bool m_bDelayedFire2; // Fire secondary when finished reloading
public:
void Precache( void );
int CapabilitiesGet( void ) { return bits_CAP_WEAPON_RANGE_ATTACK1; }
virtual const Vector& GetBulletSpread( void )
{
static Vector vitalAllyCone = VECTOR_CONE_3DEGREES;
static Vector cone = VECTOR_CONE_10DEGREES;
if( GetOwner() && (GetOwner()->Classify() == CLASS_PLAYER_ALLY_VITAL) )
{
// Give Alyx's shotgun blasts more a more directed punch. She needs
// to be at least as deadly as she would be with her pistol to stay interesting (sjb)
return vitalAllyCone;
}
return cone;
}
virtual int GetMinBurst() { return 1; }
virtual int GetMaxBurst() { return 3; }
virtual float GetMinRestTime();
virtual float GetMaxRestTime();
virtual float GetFireRate( void );
bool StartReload( void );
bool Reload( void );
void FillClip( void );
void FinishReload( void );
void CheckHolsterReload( void );
void Pump( void );
// void WeaponIdle( void );
void ItemHolsterFrame( void );
void ItemPostFrame( void );
void PrimaryAttack( void );
void SecondaryAttack( void );
void DryFire( void );
void SwitchFiremode( void );
void ShotgunFire(int PelletsNum, int PelletDamage, bool UsePump);
void FireNPCPrimaryAttack( CBaseCombatCharacter *pOperator, bool bUseWeaponAngles );
void Operator_ForceNPCFire( CBaseCombatCharacter *pOperator, bool bSecondary );
void Operator_HandleAnimEvent( animevent_t *pEvent, CBaseCombatCharacter *pOperator );
DECLARE_ACTTABLE();
CWeaponShotgun(void);
};
IMPLEMENT_SERVERCLASS_ST(CWeaponShotgun, DT_WeaponShotgun)
END_SEND_TABLE()
LINK_ENTITY_TO_CLASS( weapon_shotgun, CWeaponShotgun );
PRECACHE_WEAPON_REGISTER(weapon_shotgun);
BEGIN_DATADESC( CWeaponShotgun )
DEFINE_FIELD( m_Autoloading, FIELD_BOOLEAN),
DEFINE_FIELD( m_bNeedChamber, FIELD_BOOLEAN),
DEFINE_FIELD( m_bNeedPump, FIELD_BOOLEAN ),
DEFINE_FIELD( m_bDelayedFire1, FIELD_BOOLEAN ),
DEFINE_FIELD( m_bDelayedFire2, FIELD_BOOLEAN ),
END_DATADESC()
acttable_t CWeaponShotgun::m_acttable[] =
{
{ ACT_IDLE, ACT_IDLE_SMG1, true }, // FIXME: hook to shotgun unique
{ ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_SHOTGUN, true },
{ ACT_RELOAD, ACT_RELOAD_SHOTGUN, false },
{ ACT_WALK, ACT_WALK_RIFLE, true },
{ ACT_IDLE_ANGRY, ACT_IDLE_ANGRY_SHOTGUN, true },
// Readiness activities (not aiming)
{ ACT_IDLE_RELAXED, ACT_IDLE_SHOTGUN_RELAXED, false },//never aims
{ ACT_IDLE_STIMULATED, ACT_IDLE_SHOTGUN_STIMULATED, false },
{ ACT_IDLE_AGITATED, ACT_IDLE_SHOTGUN_AGITATED, false },//always aims
{ ACT_WALK_RELAXED, ACT_WALK_RIFLE_RELAXED, false },//never aims
{ ACT_WALK_STIMULATED, ACT_WALK_RIFLE_STIMULATED, false },
{ ACT_WALK_AGITATED, ACT_WALK_AIM_RIFLE, false },//always aims
{ ACT_RUN_RELAXED, ACT_RUN_RIFLE_RELAXED, false },//never aims
{ ACT_RUN_STIMULATED, ACT_RUN_RIFLE_STIMULATED, false },
{ ACT_RUN_AGITATED, ACT_RUN_AIM_RIFLE, false },//always aims
// Readiness activities (aiming)
{ ACT_IDLE_AIM_RELAXED, ACT_IDLE_SMG1_RELAXED, false },//never aims
{ ACT_IDLE_AIM_STIMULATED, ACT_IDLE_AIM_RIFLE_STIMULATED, false },
{ ACT_IDLE_AIM_AGITATED, ACT_IDLE_ANGRY_SMG1, false },//always aims
{ ACT_WALK_AIM_RELAXED, ACT_WALK_RIFLE_RELAXED, false },//never aims
{ ACT_WALK_AIM_STIMULATED, ACT_WALK_AIM_RIFLE_STIMULATED, false },
{ ACT_WALK_AIM_AGITATED, ACT_WALK_AIM_RIFLE, false },//always aims
{ ACT_RUN_AIM_RELAXED, ACT_RUN_RIFLE_RELAXED, false },//never aims
{ ACT_RUN_AIM_STIMULATED, ACT_RUN_AIM_RIFLE_STIMULATED, false },
{ ACT_RUN_AIM_AGITATED, ACT_RUN_AIM_RIFLE, false },//always aims
//End readiness activities
{ ACT_WALK_AIM, ACT_WALK_AIM_SHOTGUN, true },
{ ACT_WALK_CROUCH, ACT_WALK_CROUCH_RIFLE, true },
{ ACT_WALK_CROUCH_AIM, ACT_WALK_CROUCH_AIM_RIFLE, true },
{ ACT_RUN, ACT_RUN_RIFLE, true },
{ ACT_RUN_AIM, ACT_RUN_AIM_SHOTGUN, true },
{ ACT_RUN_CROUCH, ACT_RUN_CROUCH_RIFLE, true },
{ ACT_RUN_CROUCH_AIM, ACT_RUN_CROUCH_AIM_RIFLE, true },
{ ACT_GESTURE_RANGE_ATTACK1, ACT_GESTURE_RANGE_ATTACK_SHOTGUN, true },
{ ACT_RANGE_ATTACK1_LOW, ACT_RANGE_ATTACK_SHOTGUN_LOW, true },
{ ACT_RELOAD_LOW, ACT_RELOAD_SHOTGUN_LOW, false },
{ ACT_GESTURE_RELOAD, ACT_GESTURE_RELOAD_SHOTGUN, false },
};
IMPLEMENT_ACTTABLE(CWeaponShotgun);
void CWeaponShotgun::Precache( void )
{
CBaseCombatWeapon::Precache();
PrecacheScriptSound("Weapon_Shotgun.Chamber");
PrecacheScriptSound("Weapon_Shotgun.Pump");
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : *pOperator -
//-----------------------------------------------------------------------------
void CWeaponShotgun::FireNPCPrimaryAttack( CBaseCombatCharacter *pOperator, bool bUseWeaponAngles )
{
Vector vecShootOrigin, vecShootDir;
CAI_BaseNPC *npc = pOperator->MyNPCPointer();
ASSERT( npc != NULL );
WeaponSound( SINGLE_NPC );
pOperator->DoMuzzleFlash();
m_iClip1 = m_iClip1 - 1;
if ( bUseWeaponAngles )
{
QAngle angShootDir;
GetAttachment( LookupAttachment( "muzzle" ), vecShootOrigin, angShootDir );
AngleVectors( angShootDir, &vecShootDir );
}
else
{
vecShootOrigin = pOperator->Weapon_ShootPosition();
vecShootDir = npc->GetActualShootTrajectory( vecShootOrigin );
}
pOperator->FireBullets( 8, vecShootOrigin, vecShootDir, GetBulletSpread(), MAX_TRACE_LENGTH, m_iPrimaryAmmoType, 0 );
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CWeaponShotgun::Operator_ForceNPCFire( CBaseCombatCharacter *pOperator, bool bSecondary )
{
// Ensure we have enough rounds in the clip
m_iClip1++;
FireNPCPrimaryAttack( pOperator, true );
}
//-----------------------------------------------------------------------------
// Purpose:
// Input :
// Output :
//-----------------------------------------------------------------------------
void CWeaponShotgun::Operator_HandleAnimEvent( animevent_t *pEvent, CBaseCombatCharacter *pOperator )
{
switch( pEvent->event )
{
case EVENT_WEAPON_SHOTGUN_FIRE:
{
FireNPCPrimaryAttack( pOperator, false );
}
break;
default:
CBaseCombatWeapon::Operator_HandleAnimEvent( pEvent, pOperator );
break;
}
}
//-----------------------------------------------------------------------------
// Purpose: When we shipped HL2, the shotgun weapon did not override the
// BaseCombatWeapon default rest time of 0.3 to 0.6 seconds. When
// NPC's fight from a stationary position, their animation events
// govern when they fire so the rate of fire is specified by the
// animation. When NPC's move-and-shoot, the rate of fire is
// specifically controlled by the shot regulator, so it's imporant
// that GetMinRestTime and GetMaxRestTime are implemented and provide
// reasonable defaults for the weapon. To address difficulty concerns,
// we are going to fix the combine's rate of shotgun fire in episodic.
// This change will not affect Alyx using a shotgun in EP1. (sjb)
//-----------------------------------------------------------------------------
float CWeaponShotgun::GetMinRestTime()
{
if( hl2_episodic.GetBool() && GetOwner() && GetOwner()->Classify() == CLASS_COMBINE )
{
return 1.2f;
}
return BaseClass::GetMinRestTime();
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
float CWeaponShotgun::GetMaxRestTime()
{
if( hl2_episodic.GetBool() && GetOwner() && GetOwner()->Classify() == CLASS_COMBINE )
{
return 1.5f;
}
return BaseClass::GetMaxRestTime();
}
//-----------------------------------------------------------------------------
// Purpose: Time between successive shots in a burst. Also returned for EP2
// with an eye to not messing up Alyx in EP1.
//-----------------------------------------------------------------------------
float CWeaponShotgun::GetFireRate()
{
if( hl2_episodic.GetBool() && GetOwner() && GetOwner()->Classify() == CLASS_COMBINE )
{
return 0.8f;
}
return 0.7;
}
//-----------------------------------------------------------------------------
// Purpose: Override so only reload one shell at a time
// Input :
// Output :
//-----------------------------------------------------------------------------
bool CWeaponShotgun::StartReload( void )
{
CBaseCombatCharacter *pOwner = GetOwner();
if ( pOwner == NULL )
return false;
if (pOwner->GetAmmoCount(m_iPrimaryAmmoType) <= 0)
return false;
if (m_iClip1 >= GetMaxClip1())
return false;
// If shotgun totally emptied then a pump animation is needed // K but pump after you load the first shell
//NOTENOTE: This is kinda lame because the player doesn't get strong feedback on when the reload has finished,
// without the pump. Technically, it's incorrect, but it's good for feedback...
//
/*
if (m_iClip1 <= 0)
{
m_bNeedPump = true; // We don't need that anymore
}
*/
int j = MIN(1, pOwner->GetAmmoCount(m_iPrimaryAmmoType));
if (j <= 0)
return false;
SendWeaponAnim( ACT_SHOTGUN_RELOAD_START );
// Make shotgun shell visible
SetBodygroup(1,0);
pOwner->m_flNextAttack = gpGlobals->curtime;
m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration();
m_bInReload = true;
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Override so only reload one shell at a time
// Input :
// Output :
//-----------------------------------------------------------------------------
bool CWeaponShotgun::Reload( void )
{
// Check that StartReload was called first
if (!m_bInReload)
{
Warning("ERROR: Shotgun Reload called incorrectly!\n");
}
CBaseCombatCharacter *pOwner = GetOwner();
if ( pOwner == NULL )
return false;
if (pOwner->GetAmmoCount(m_iPrimaryAmmoType) <= 0)
return false;
if (m_iClip1 >= GetMaxClip1())
return false;
int j = MIN(1, pOwner->GetAmmoCount(m_iPrimaryAmmoType));
if (j <= 0)
return false;
FillClip();
WeaponSound(RELOAD);
SendWeaponAnim(ACT_VM_RELOAD);
pOwner->m_flNextAttack = gpGlobals->curtime;
m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration();
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Play finish reload anim and fill clip
// Input :
// Output :
//-----------------------------------------------------------------------------
void CWeaponShotgun::FinishReload( void )
{
// Make shotgun shell invisible
SetBodygroup(1,1);
CBaseCombatCharacter *pOwner = GetOwner();
if ( pOwner == NULL )
return;
m_bInReload = false;
// Finish reload animation
SendWeaponAnim( ACT_SHOTGUN_RELOAD_FINISH );
pOwner->m_flNextAttack = gpGlobals->curtime;
m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration();
}
//-----------------------------------------------------------------------------
// Purpose: Play finish reload anim and fill clip
// Input :
// Output :
//-----------------------------------------------------------------------------
void CWeaponShotgun::FillClip( void )
{
CBaseCombatCharacter *pOwner = GetOwner();
if ( pOwner == NULL )
return;
// Add them to the clip
if ( pOwner->GetAmmoCount( m_iPrimaryAmmoType ) > 0 )
{
if ( Clip1() < GetMaxClip1() )
{
m_iClip1++;
pOwner->RemoveAmmo( 1, m_iPrimaryAmmoType );
}
}
}
//-----------------------------------------------------------------------------
// Purpose: Play weapon pump anim
// Input :
// Output :
//-----------------------------------------------------------------------------
void CWeaponShotgun::Pump( void )
{
CBaseCombatCharacter *pOwner = GetOwner();
if ( pOwner == NULL )
return;
m_bNeedPump = false;
if (m_Autoloading)
{
EmitSound("Weapon_Shotgun.Chamber");
SendWeaponAnim(ACT_SHOTGUN_CHAMBER); //Hello there, change me to ACT_SHOTGUN_PUMP if you don't want different animation or give me references.
}
else
{
EmitSound("Weapon_Shotgun.Pump");
SendWeaponAnim(ACT_SHOTGUN_PUMP);
}
pOwner->m_flNextAttack = gpGlobals->curtime + SequenceDuration();
m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration();
}
//-----------------------------------------------------------------------------
// Purpose:
//
//
//-----------------------------------------------------------------------------
void CWeaponShotgun::DryFire( void )
{
WeaponSound(EMPTY);
SendWeaponAnim( ACT_VM_DRYFIRE );
m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration();
}
//-----------------------------------------------------------------------------
// Purpose:
//
//
//-----------------------------------------------------------------------------
void CWeaponShotgun::PrimaryAttack( void )
{
// Only the player fires this way so we can cast
CBasePlayer *pPlayer = ToBasePlayer( GetOwner() );
if (!pPlayer)
{
return;
}
if (GetOwner()->GetWaterLevel() == 3) //don't fire under water
{
WeaponSound(EMPTY);
m_flNextPrimaryAttack = gpGlobals->curtime + 0.2;
return;
}
if (!m_bInReload)
{
m_bNeedChamber = false;
}
if (m_Autoloading)
{
ShotgunFire(11, 6, false); //Here we can set (number of pellets, damage per pellet, pump)
}
else
{
ShotgunFire(11, 8, true); //The same as above
}
}
//-----------------------------------------------------------------------------
// Purpose:
//
//
//-----------------------------------------------------------------------------
void CWeaponShotgun::SecondaryAttack( void )
{
CBasePlayer *pPlayer = ToBasePlayer(GetOwner());
if (!pPlayer)
{
return;
}
pPlayer->m_nButtons &= ~IN_ATTACK2;
m_flNextPrimaryAttack = gpGlobals->curtime + 0.3;
m_flNextSecondaryAttack = gpGlobals->curtime + 0.3;
SwitchFiremode();
}
void CWeaponShotgun::ShotgunFire(int PelletsNum, int PelletDamage, bool UsePump)
{
CBasePlayer *pPlayer = ToBasePlayer(GetOwner());
if (!pPlayer)
{
return;
}
WeaponSound(SINGLE);
pPlayer->DoMuzzleFlash();
pPlayer->SetAnimation(PLAYER_ATTACK1);
if (m_Autoloading)
{
SendWeaponAnim(ACT_VM_SECONDARYATTACK); // autoloading needs different animation because it ejects shell
m_flNextPrimaryAttack = gpGlobals->curtime + 0.28f; // Rate of fire in autoloading mode (default 214 RPM)
}
else
{
SendWeaponAnim(ACT_VM_PRIMARYATTACK);
m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration(); //When there is SequenceDuration() this has to be under SendWeaponAnim !
}
m_iClip1 -= 1;
Vector vecSrc = pPlayer->Weapon_ShootPosition();
Vector vecAiming = pPlayer->GetAutoaimVector(AUTOAIM_SCALE_DEFAULT);
pPlayer->SetMuzzleFlashTime(gpGlobals->curtime + 1.0);
pPlayer->FireBullets(PelletsNum, vecSrc, vecAiming, GetBulletSpread(), MAX_TRACE_LENGTH, m_iPrimaryAmmoType, 0, -1, -1, PelletDamage, NULL, true, true); // weapon stats
pPlayer->ViewPunch(QAngle(random->RandomFloat(-2, -1), random->RandomFloat(-2, 2), 0));
CSoundEnt::InsertSound(SOUND_COMBAT, GetAbsOrigin(), SOUNDENT_VOLUME_SHOTGUN, 0.2, GetOwner());
if (!m_iClip1 && pPlayer->GetAmmoCount(m_iPrimaryAmmoType) <= 0)
{
// HEV suit - indicate out of ammo condition
pPlayer->SetSuitUpdate("!HEV_AMO0", FALSE, 0);
}
if (m_iClip1)
{
// pump so long as some rounds are left.
m_bNeedPump = UsePump; // we want false in autoloading mode
}
m_iPrimaryAttacks++;
gamestats->Event_WeaponFired(pPlayer, true, GetClassname());
}
void CWeaponShotgun::SwitchFiremode(void)
{
CBasePlayer *pPlayer = ToBasePlayer(GetOwner());
if (!pPlayer)
{
return;
}
if (m_Autoloading)
{
WeaponSound(EMPTY);
SendWeaponAnim(ACT_VM_FIDGET);
ClientPrint(pPlayer, HUD_PRINTCENTER, "Switched to pump-action");
m_Autoloading = false;
}
else
{
WeaponSound(EMPTY);
SendWeaponAnim(ACT_VM_FIDGET);
ClientPrint(pPlayer, HUD_PRINTCENTER, "Switched to semi-automatic");
m_Autoloading = true;
}
}
//-----------------------------------------------------------------------------
// Purpose: Override so shotgun can do mulitple reloads in a row
//-----------------------------------------------------------------------------
void CWeaponShotgun::ItemPostFrame( void )
{
CBasePlayer *pOwner = ToBasePlayer( GetOwner() );
if (!pOwner)
{
return;
}
if (m_bInReload)
{
// If I'm primary firing and have one round stop reloading and fire
if ((pOwner->m_nButtons & IN_ATTACK) && (m_iClip1 >= 1) && !m_bNeedChamber)
{
m_bInReload = false;
m_bNeedPump = false;
m_bDelayedFire1 = true;
}
/*
// If I'm secondary firing and have one round stop reloading and fire
else if ((pOwner->m_nButtons & IN_ATTACK2 ) && (m_iClip1 >=2))//
{
m_bInReload = false;
m_bNeedPump = false;
m_bDelayedFire2 = true;
}
*/
else if (m_flNextPrimaryAttack <= gpGlobals->curtime)
{
if (m_bNeedChamber)
{
Pump();
m_bNeedChamber = false;
return;
}
if (m_iClip1 <= 0)
{
Reload();
m_bNeedChamber = true;
return;
}
// If out of ammo end reload
if (pOwner->GetAmmoCount(m_iPrimaryAmmoType) <=0)
{
FinishReload();
return;
}
// If clip not full reload again
if (m_iClip1 < GetMaxClip1())
{
Reload();
return;
}
// Clip full, stop reloading
else
{
FinishReload();
return;
}
}
}
else
{
// Make shotgun shell invisible
SetBodygroup(1,1);
}
if ((m_bNeedPump) && (m_flNextPrimaryAttack <= gpGlobals->curtime))
{
Pump();
return;
}
// Shotgun uses same timing and ammo for secondary attack
if ((m_bDelayedFire2 || pOwner->m_nButtons & IN_ATTACK2)&&(m_flNextPrimaryAttack <= gpGlobals->curtime))
{
m_bDelayedFire2 = false;
if ( (m_iClip1 <= 1 && UsesClipsForAmmo1()))
{
// If only one shell is left, do a single shot instead
if ( m_iClip1 == 1 )
{
SecondaryAttack();
}
else if (!pOwner->GetAmmoCount(m_iPrimaryAmmoType))
{
DryFire();
}
else
{
StartReload();
}
}
// Fire underwater?
else if (GetOwner()->GetWaterLevel() == 3 && m_bFiresUnderwater == false)
{
WeaponSound(EMPTY);
m_flNextPrimaryAttack = gpGlobals->curtime + 0.2;
return;
}
else
{
// If the firing button was just pressed, reset the firing time
if ( pOwner->m_afButtonPressed & IN_ATTACK )
{
m_flNextPrimaryAttack = gpGlobals->curtime;
}
SecondaryAttack();
}
}
else if ( (m_bDelayedFire1 || pOwner->m_nButtons & IN_ATTACK) && m_flNextPrimaryAttack <= gpGlobals->curtime)
{
m_bDelayedFire1 = false;
if ( (m_iClip1 <= 0 && UsesClipsForAmmo1()) || ( !UsesClipsForAmmo1() && !pOwner->GetAmmoCount(m_iPrimaryAmmoType) ) )
{
if (!pOwner->GetAmmoCount(m_iPrimaryAmmoType))
{
DryFire();
}
else
{
StartReload();
}
}
// Fire underwater?
else if (pOwner->GetWaterLevel() == 3 && m_bFiresUnderwater == false)
{
WeaponSound(EMPTY);
m_flNextPrimaryAttack = gpGlobals->curtime + 0.2;
return;
}
else
{
// If the firing button was just pressed, reset the firing time
CBasePlayer *pPlayer = ToBasePlayer( GetOwner() );
if ( pPlayer && pPlayer->m_afButtonPressed & IN_ATTACK )
{
m_flNextPrimaryAttack = gpGlobals->curtime;
}
PrimaryAttack();
}
}
if ( pOwner->m_nButtons & IN_RELOAD && UsesClipsForAmmo1() && !m_bInReload )
{
// reload when reload is pressed, or if no buttons are down and weapon is empty.
StartReload();
}
else
{
// no fire buttons down
m_bFireOnEmpty = false;
if ( !HasAnyAmmo() && m_flNextPrimaryAttack < gpGlobals->curtime )
{
// weapon isn't useable, switch.
if ( !(GetWeaponFlags() & ITEM_FLAG_NOAUTOSWITCHEMPTY) && pOwner->SwitchToNextBestWeapon( this ) )
{
m_flNextPrimaryAttack = gpGlobals->curtime + 0.3;
return;
}
}
else
{
// weapon is useable. Reload if empty and weapon has waited as long as it has to after firing
if ( m_iClip1 <= 0 && !(GetWeaponFlags() & ITEM_FLAG_NOAUTORELOAD) && m_flNextPrimaryAttack < gpGlobals->curtime )
{
if (StartReload())
{
// if we've successfully started to reload, we're done
return;
}
}
}
WeaponIdle( );
return;
}
}
//-----------------------------------------------------------------------------
// Purpose: Constructor
//-----------------------------------------------------------------------------
CWeaponShotgun::CWeaponShotgun( void )
{
m_bReloadsSingly = true;
m_bNeedPump = false;
m_bNeedChamber = false;
m_bDelayedFire1 = false;
m_bDelayedFire2 = false;
m_fMinRange1 = 0.0;
m_fMaxRange1 = 500;
m_fMinRange2 = 0.0;
m_fMaxRange2 = 200;
m_bFiresUnderwater = true;// We want secondary attack to work under water
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CWeaponShotgun::ItemHolsterFrame( void )
{
// Must be player held
if ( GetOwner() && GetOwner()->IsPlayer() == false )
return;
// We can't be active
if ( GetOwner()->GetActiveWeapon() == this )
return;
// If it's been longer than three seconds, reload
if ( ( gpGlobals->curtime - m_flHolsterTime ) > sk_auto_reload_time.GetFloat() )
{
// Reset the timer
m_flHolsterTime = gpGlobals->curtime;
if ( GetOwner() == NULL )
return;
if ( m_iClip1 == GetMaxClip1() )
return;
// Just load the clip with no animations
int ammoFill = MIN( (GetMaxClip1() - m_iClip1), GetOwner()->GetAmmoCount( GetPrimaryAmmoType() ) );
GetOwner()->RemoveAmmo( ammoFill, GetPrimaryAmmoType() );
m_iClip1 += ammoFill;
}
}
//==================================================
// Purpose:
//==================================================
/*
void CWeaponShotgun::WeaponIdle( void )
{
//Only the player fires this way so we can cast
CBasePlayer *pPlayer = GetOwner()
if ( pPlayer == NULL )
return;
//If we're on a target, play the new anim
if ( pPlayer->IsOnTarget() )
{
SendWeaponAnim( ACT_VM_IDLE_ACTIVE );
}
}
*/