//====== Copyright © 1996-2003, Valve Corporation, All rights reserved. ======= // // Purpose: // //============================================================================= #include "cbase.h" #include "tf_weapon_minigun.h" #include "decals.h" #include "in_buttons.h" #include "tf_fx_shared.h" // Client specific. #ifdef CLIENT_DLL #include "c_tf_player.h" #include "soundenvelope.h" // Server specific. #else #include "tf_player.h" #endif #define MAX_BARREL_SPIN_VELOCITY 20 //============================================================================= // // Weapon Minigun tables. // IMPLEMENT_NETWORKCLASS_ALIASED( TFMinigun, DT_WeaponMinigun ) BEGIN_NETWORK_TABLE( CTFMinigun, DT_WeaponMinigun ) // Client specific. #ifdef CLIENT_DLL RecvPropInt( RECVINFO( m_iWeaponState ) ), RecvPropBool( RECVINFO( m_bCritShot ) ) // Server specific. #else SendPropInt( SENDINFO( m_iWeaponState ), 4, SPROP_UNSIGNED | SPROP_CHANGES_OFTEN ), SendPropBool( SENDINFO( m_bCritShot ) ) #endif END_NETWORK_TABLE() #ifdef CLIENT_DLL BEGIN_PREDICTION_DATA( CTFMinigun ) DEFINE_FIELD( m_iWeaponState, FIELD_INTEGER ), END_PREDICTION_DATA() #endif LINK_ENTITY_TO_CLASS( tf_weapon_minigun, CTFMinigun ); PRECACHE_WEAPON_REGISTER( tf_weapon_minigun ); // Server specific. #ifndef CLIENT_DLL BEGIN_DATADESC( CTFMinigun ) END_DATADESC() #endif //============================================================================= // // Weapon Minigun functions. // //----------------------------------------------------------------------------- // Purpose: Constructor. //----------------------------------------------------------------------------- CTFMinigun::CTFMinigun() { #ifdef CLIENT_DLL m_pSoundCur = NULL; #endif #ifdef CLIENT_DLL m_pEjectBrassEffect = NULL; m_iEjectBrassAttachment = -1; m_pMuzzleEffect = NULL; m_iMuzzleAttachment = -1; #endif WeaponReset(); } //----------------------------------------------------------------------------- // Purpose: Destructor. //----------------------------------------------------------------------------- CTFMinigun::~CTFMinigun() { WeaponReset(); } void CTFMinigun::WeaponReset( void ) { BaseClass::WeaponReset(); m_iWeaponState = AC_STATE_IDLE; m_iWeaponMode = TF_WEAPON_PRIMARY_MODE; m_bCritShot = false; m_flStartedFiringAt = -1; m_flNextFiringSpeech = 0; m_flBarrelAngle = 0; m_flBarrelCurrentVelocity = 0; m_flBarrelTargetVelocity = 0; #ifdef CLIENT_DLL if ( m_pSoundCur ) { CSoundEnvelopeController::GetController().SoundDestroy( m_pSoundCur ); m_pSoundCur = NULL; } m_iMinigunSoundCur = -1; StopMuzzleEffect(); StopBrassEffect(); #endif } #ifdef GAME_DLL int CTFMinigun::UpdateTransmitState( void ) { // ALWAYS transmit to all clients. return SetTransmitState( FL_EDICT_ALWAYS ); } #endif //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::Precache( void ) { BaseClass::Precache(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::PrimaryAttack() { SharedAttack(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::SharedAttack() { CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( !pPlayer ) return; if ( !CanAttack() ) { WeaponIdle(); return; } if ( pPlayer->m_nButtons & IN_ATTACK ) { m_iWeaponMode = TF_WEAPON_PRIMARY_MODE; } else if ( pPlayer->m_nButtons & IN_ATTACK2 ) { m_iWeaponMode = TF_WEAPON_SECONDARY_MODE; } switch ( m_iWeaponState ) { default: case AC_STATE_IDLE: { // Removed the need for cells to powerup the AC WindUp(); m_flNextPrimaryAttack = gpGlobals->curtime + 1.0; m_flNextSecondaryAttack = gpGlobals->curtime + 1.0; m_flTimeWeaponIdle = gpGlobals->curtime + 1.0; m_flStartedFiringAt = -1; pPlayer->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_PRE ); break; } case AC_STATE_STARTFIRING: { // Start playing the looping fire sound if ( m_flNextPrimaryAttack <= gpGlobals->curtime ) { if ( m_iWeaponMode == TF_WEAPON_SECONDARY_MODE ) { m_iWeaponState = AC_STATE_SPINNING; #ifdef GAME_DLL pPlayer->SpeakWeaponFire( MP_CONCEPT_WINDMINIGUN ); #endif } else { m_iWeaponState = AC_STATE_FIRING; #ifdef GAME_DLL pPlayer->SpeakWeaponFire( MP_CONCEPT_FIREMINIGUN ); #endif } m_flNextSecondaryAttack = m_flNextPrimaryAttack = m_flTimeWeaponIdle = gpGlobals->curtime + 0.1; } break; } case AC_STATE_FIRING: { if ( m_iWeaponMode == TF_WEAPON_SECONDARY_MODE ) { #ifdef GAME_DLL pPlayer->ClearWeaponFireScene(); pPlayer->SpeakWeaponFire( MP_CONCEPT_WINDMINIGUN ); #endif m_iWeaponState = AC_STATE_SPINNING; m_flNextSecondaryAttack = m_flNextPrimaryAttack = m_flTimeWeaponIdle = gpGlobals->curtime + 0.1; } else if ( pPlayer->GetAmmoCount(m_iPrimaryAmmoType) <= 0 ) { m_iWeaponState = AC_STATE_DRYFIRE; } else { if ( m_flStartedFiringAt < 0 ) { m_flStartedFiringAt = gpGlobals->curtime; } #ifdef GAME_DLL if ( m_flNextFiringSpeech < gpGlobals->curtime ) { m_flNextFiringSpeech = gpGlobals->curtime + 5.0; pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_MINIGUN_FIREWEAPON ); } #endif // Only fire if we're actually shooting BaseClass::PrimaryAttack(); // fire and do timers CalcIsAttackCritical(); m_bCritShot = IsCurrentAttackACrit(); pPlayer->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_PRIMARY ); m_flTimeWeaponIdle = gpGlobals->curtime + 0.2; } break; } case AC_STATE_DRYFIRE: { m_flStartedFiringAt = -1; if ( pPlayer->GetAmmoCount(m_iPrimaryAmmoType) > 0 ) { m_iWeaponState = AC_STATE_FIRING; } else if ( m_iWeaponMode == TF_WEAPON_SECONDARY_MODE ) { m_iWeaponState = AC_STATE_SPINNING; } SendWeaponAnim( ACT_VM_SECONDARYATTACK ); break; } case AC_STATE_SPINNING: { m_flStartedFiringAt = -1; if ( m_iWeaponMode == TF_WEAPON_PRIMARY_MODE ) { if ( pPlayer->GetAmmoCount(m_iPrimaryAmmoType) > 0 ) { #ifdef GAME_DLL pPlayer->ClearWeaponFireScene(); pPlayer->SpeakWeaponFire( MP_CONCEPT_FIREMINIGUN ); #endif m_iWeaponState = AC_STATE_FIRING; } else { m_iWeaponState = AC_STATE_DRYFIRE; } } SendWeaponAnim( ACT_VM_SECONDARYATTACK ); break; } } } //----------------------------------------------------------------------------- // Purpose: Fall through to Primary Attack //----------------------------------------------------------------------------- void CTFMinigun::SecondaryAttack( void ) { CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( !pPlayer ) return; SharedAttack(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::WindUp( void ) { // Get the player owning the weapon. CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( !pPlayer ) return; // Play wind-up animation and sound (SPECIAL1). SendWeaponAnim( ACT_MP_ATTACK_STAND_PREFIRE ); // Set the appropriate firing state. m_iWeaponState = AC_STATE_STARTFIRING; pPlayer->m_Shared.AddCond( TF_COND_AIMING ); #ifndef CLIENT_DLL pPlayer->StopRandomExpressions(); #endif #ifdef CLIENT_DLL WeaponSoundUpdate(); #endif // Update player's speed pPlayer->TeamFortress_SetSpeed(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFMinigun::CanHolster( void ) { if ( m_iWeaponState > AC_STATE_IDLE ) return false; if ( GetActivity() == ACT_MP_ATTACK_STAND_POSTFIRE ) { if ( !IsViewModelSequenceFinished() ) return false; } return BaseClass::CanHolster(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFMinigun::Holster( CBaseCombatWeapon *pSwitchingTo ) { if ( m_iWeaponState > AC_STATE_IDLE ) { WindDown(); } return BaseClass::Holster( pSwitchingTo ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFMinigun::Lower( void ) { if ( m_iWeaponState > AC_STATE_IDLE ) { WindDown(); } return BaseClass::Lower(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::WindDown( void ) { // Get the player owning the weapon. CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( !pPlayer ) return; SendWeaponAnim( ACT_MP_ATTACK_STAND_POSTFIRE ); // Set the appropriate firing state. m_iWeaponState = AC_STATE_IDLE; pPlayer->m_Shared.RemoveCond( TF_COND_AIMING ); #ifdef CLIENT_DLL WeaponSoundUpdate(); #else pPlayer->ClearWeaponFireScene(); #endif // Time to weapon idle. m_flTimeWeaponIdle = gpGlobals->curtime + 2.0; // Update player's speed pPlayer->TeamFortress_SetSpeed(); #ifdef CLIENT_DLL m_flBarrelTargetVelocity = 0; #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::WeaponIdle() { if ( gpGlobals->curtime < m_flTimeWeaponIdle ) return; // Always wind down if we've hit here, because it only happens when the player has stopped firing/spinning if ( m_iWeaponState != AC_STATE_IDLE ) { CTFPlayer *pPlayer = ToTFPlayer( GetPlayerOwner() ); if ( pPlayer ) { pPlayer->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_POST ); } WindDown(); return; } BaseClass::WeaponIdle(); m_flTimeWeaponIdle = gpGlobals->curtime + 12.5;// how long till we do this again. } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFMinigun::SendWeaponAnim( int iActivity ) { #ifdef CLIENT_DLL // Client procedurally animates the barrel bone if ( iActivity == ACT_MP_ATTACK_STAND_PRIMARYFIRE || iActivity == ACT_MP_ATTACK_STAND_PREFIRE ) { m_flBarrelTargetVelocity = MAX_BARREL_SPIN_VELOCITY; } else if ( iActivity == ACT_MP_ATTACK_STAND_POSTFIRE ) { m_flBarrelTargetVelocity = 0; } #endif // When we start firing, play the startup firing anim first if ( iActivity == ACT_VM_PRIMARYATTACK ) { // If we're already playing the fire anim, let it continue. It loops. if ( GetActivity() == ACT_VM_PRIMARYATTACK ) return true; // Otherwise, play the start it return BaseClass::SendWeaponAnim( ACT_VM_PRIMARYATTACK ); } return BaseClass::SendWeaponAnim( iActivity ); } //----------------------------------------------------------------------------- // Purpose: This will force the minigun to turn off the firing sound and play the spinning sound //----------------------------------------------------------------------------- void CTFMinigun::HandleFireOnEmpty( void ) { if ( m_iWeaponState == AC_STATE_FIRING || m_iWeaponState == AC_STATE_SPINNING ) { m_iWeaponState = AC_STATE_DRYFIRE; SendWeaponAnim( ACT_VM_SECONDARYATTACK ); if ( m_iWeaponMode == TF_WEAPON_SECONDARY_MODE ) { m_iWeaponState = AC_STATE_SPINNING; } } } #ifdef CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CStudioHdr *CTFMinigun::OnNewModel( void ) { CStudioHdr *hdr = BaseClass::OnNewModel(); m_iBarrelBone = LookupBone( "barrel" ); // skip resetting this while recording in the tool // we change the weapon to the worldmodel and back to the viewmodel when recording // which causes the minigun to not spin while recording if ( !IsToolRecording() ) { m_flBarrelAngle = 0; m_flBarrelCurrentVelocity = 0; m_flBarrelTargetVelocity = 0; } return hdr; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::StandardBlendingRules( CStudioHdr *hdr, Vector pos[], Quaternion q[], float currentTime, int boneMask ) { BaseClass::StandardBlendingRules( hdr, pos, q, currentTime, boneMask ); if (m_iBarrelBone != -1) { UpdateBarrelMovement(); // Weapon happens to be aligned to (0,0,0) // If that changes, use this code block instead to // modify the angles /* RadianEuler a; QuaternionAngles( q[iBarrelBone], a ); a.x = m_flBarrelAngle; AngleQuaternion( a, q[iBarrelBone] ); */ AngleQuaternion( RadianEuler( 0, 0, m_flBarrelAngle ), q[m_iBarrelBone] ); } } //----------------------------------------------------------------------------- // Purpose: Updates the velocity and position of the rotating barrel //----------------------------------------------------------------------------- void CTFMinigun::UpdateBarrelMovement() { if ( m_flBarrelCurrentVelocity != m_flBarrelTargetVelocity ) { // update barrel velocity to bring it up to speed or to rest m_flBarrelCurrentVelocity = Approach( m_flBarrelTargetVelocity, m_flBarrelCurrentVelocity, 0.1 ); if ( 0 == m_flBarrelCurrentVelocity ) { // if we've stopped rotating, turn off the wind-down sound WeaponSoundUpdate(); } } // update the barrel rotation based on current velocity m_flBarrelAngle += m_flBarrelCurrentVelocity * gpGlobals->frametime; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::OnDataChanged( DataUpdateType_t updateType ) { // Brass ejection and muzzle flash. HandleBrassEffect(); HandleMuzzleEffect(); BaseClass::OnDataChanged( updateType ); WeaponSoundUpdate(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::UpdateOnRemove( void ) { if ( m_pSoundCur ) { CSoundEnvelopeController::GetController().SoundDestroy( m_pSoundCur ); m_pSoundCur = NULL; } // Force the particle system off. StopMuzzleEffect(); StopBrassEffect(); BaseClass::UpdateOnRemove(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::SetDormant( bool bDormant ) { // If I'm going from active to dormant and I'm carried by another player, stop our firing sound. if ( !IsCarriedByLocalPlayer() ) { // Am I firing? Stop the firing sound. if ( !IsDormant() && bDormant && m_iWeaponState >= AC_STATE_FIRING ) { WeaponSoundUpdate(); } // If firing and going dormant - stop the brass effect. if ( !IsDormant() && bDormant && m_iWeaponState != AC_STATE_IDLE ) { StopMuzzleEffect(); StopBrassEffect(); } } // Deliberately skip base combat weapon C_BaseEntity::SetDormant( bDormant ); } //----------------------------------------------------------------------------- // Purpose: // won't be called for w_ version of the model, so this isn't getting updated twice //----------------------------------------------------------------------------- void CTFMinigun::ItemPreFrame( void ) { UpdateBarrelMovement(); BaseClass::ItemPreFrame(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::StartBrassEffect() { C_BaseEntity *pEffectOwner = GetWeaponForEffect(); if ( !pEffectOwner ) return; // Try and setup the attachment point if it doesn't already exist. // This caching will mess up if we go third person from first - we only do this in taunts and don't fire so we should // be okay for now. if ( m_iEjectBrassAttachment == -1 ) { m_iEjectBrassAttachment = pEffectOwner->LookupAttachment( "eject_brass" ); } // Start the brass ejection, if a system hasn't already been started. if ( m_iEjectBrassAttachment != -1 && m_pEjectBrassEffect == NULL ) { m_pEjectBrassEffect = pEffectOwner->ParticleProp()->Create( "eject_minigunbrass", PATTACH_POINT_FOLLOW, m_iEjectBrassAttachment ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::StartMuzzleEffect() { C_BaseEntity *pEffectOwner = GetWeaponForEffect(); if ( !pEffectOwner ) return; // Try and setup the attachment point if it doesn't already exist. // This caching will mess up if we go third person from first - we only do this in taunts and don't fire so we should // be okay for now. if ( m_iMuzzleAttachment == -1 ) { m_iMuzzleAttachment = pEffectOwner->LookupAttachment( "muzzle" ); } // Start the muzzle flash, if a system hasn't already been started. if ( m_iMuzzleAttachment != -1 && m_pMuzzleEffect == NULL ) { m_pMuzzleEffect = pEffectOwner->ParticleProp()->Create( "muzzle_minigun_constant", PATTACH_POINT_FOLLOW, m_iMuzzleAttachment ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::StopBrassEffect() { C_BaseEntity *pEffectOwner = GetWeaponForEffect(); if ( !pEffectOwner ) return; // Stop the brass ejection. if ( m_pEjectBrassEffect ) { pEffectOwner->ParticleProp()->StopEmission( m_pEjectBrassEffect ); m_pEjectBrassEffect = NULL; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::StopMuzzleEffect() { C_BaseEntity *pEffectOwner = GetWeaponForEffect(); if ( !pEffectOwner ) return; // Stop the muzzle flash. if ( m_pMuzzleEffect ) { pEffectOwner->ParticleProp()->StopEmission( m_pMuzzleEffect ); m_pMuzzleEffect = NULL; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::HandleBrassEffect() { if ( m_iWeaponState == AC_STATE_FIRING && m_pEjectBrassEffect == NULL ) { StartBrassEffect(); } else if ( m_iWeaponState != AC_STATE_FIRING && m_pEjectBrassEffect ) { StopBrassEffect(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::HandleMuzzleEffect() { if ( m_iWeaponState == AC_STATE_FIRING && m_pMuzzleEffect == NULL ) { StartMuzzleEffect(); } else if ( m_iWeaponState != AC_STATE_FIRING && m_pMuzzleEffect ) { StopMuzzleEffect(); } } //----------------------------------------------------------------------------- // Purpose: View model barrel rotation angle. Calculated here, implemented in // tf_viewmodel.cpp //----------------------------------------------------------------------------- float CTFMinigun::GetBarrelRotation( void ) { return m_flBarrelAngle; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFMinigun::CreateMove( float flInputSampleTime, CUserCmd *pCmd, const QAngle &vecOldViewAngles ) { // Prevent jumping while firing if ( m_iWeaponState != AC_STATE_IDLE ) { pCmd->buttons &= ~IN_JUMP; } BaseClass::CreateMove( flInputSampleTime, pCmd, vecOldViewAngles ); } //----------------------------------------------------------------------------- // Purpose: Ensures the correct sound (including silence) is playing for // current weapon state. //----------------------------------------------------------------------------- void CTFMinigun::WeaponSoundUpdate() { // determine the desired sound for our current state int iSound = -1; switch ( m_iWeaponState ) { case AC_STATE_IDLE: if ( m_flBarrelCurrentVelocity > 0 ) { iSound = SPECIAL2; // wind down sound #ifdef CLIENT_DLL if ( m_flBarrelTargetVelocity > 0 ) { m_flBarrelTargetVelocity = 0; } #endif } else iSound = -1; break; case AC_STATE_STARTFIRING: iSound = SPECIAL1; // wind up sound break; case AC_STATE_FIRING: { if ( m_bCritShot == true ) { iSound = BURST; // Crit sound } else { iSound = WPN_DOUBLE; // firing sound } } break; case AC_STATE_SPINNING: iSound = SPECIAL3; // spinning sound break; case AC_STATE_DRYFIRE: iSound = EMPTY; // out of ammo, still trying to fire break; default: Assert( false ); break; } // if we're already playing the desired sound, nothing to do if ( m_iMinigunSoundCur == iSound ) return; // if we're playing some other sound, stop it if ( m_pSoundCur ) { // Stop the previous sound immediately CSoundEnvelopeController::GetController().SoundDestroy( m_pSoundCur ); m_pSoundCur = NULL; } m_iMinigunSoundCur = iSound; // if there's no sound to play for current state, we're done if ( -1 == iSound ) return; // play the appropriate sound CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); const char *shootsound = GetShootSound( iSound ); CLocalPlayerFilter filter; m_pSoundCur = controller.SoundCreate( filter, entindex(), shootsound ); controller.Play( m_pSoundCur, 1.0, 100 ); controller.SoundChangeVolume( m_pSoundCur, 1.0, 0.1 ); } #endif