diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/tf/tf_obj_sentrygun.cpp | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/server/tf/tf_obj_sentrygun.cpp')
| -rw-r--r-- | game/server/tf/tf_obj_sentrygun.cpp | 2441 |
1 files changed, 2441 insertions, 0 deletions
diff --git a/game/server/tf/tf_obj_sentrygun.cpp b/game/server/tf/tf_obj_sentrygun.cpp new file mode 100644 index 0000000..20a1786 --- /dev/null +++ b/game/server/tf/tf_obj_sentrygun.cpp @@ -0,0 +1,2441 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Engineer's Sentrygun OMG +// +// $NoKeywords: $ +//=============================================================================// +#include "cbase.h" + +#include "tf_obj_sentrygun.h" +#include "engine/IEngineSound.h" +#include "tf_player.h" +#include "tf_team.h" +#include "world.h" +#include "tf_projectile_rocket.h" +#include "te_effect_dispatch.h" +#include "tf_gamerules.h" +#include "ammodef.h" +#include "tf_weapon_wrench.h" +#include "tf_weapon_laser_pointer.h" +#include "tf_weapon_shotgun.h" +#include "bot/map_entities/tf_bot_hint_sentrygun.h" +#include "bot/tf_bot.h" +#include "nav_mesh/tf_nav_mesh.h" +#include "nav_pathfind.h" +#include "tf_weapon_knife.h" +#include "tf_logic_robot_destruction.h" +#include "tf_target_dummy.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +extern bool IsInCommentaryMode(); + +extern ConVar tf_nav_in_combat_range; + +// Ground placed version +#define SENTRY_MODEL_PLACEMENT "models/buildables/sentry1_blueprint.mdl" +#define SENTRY_MODEL_LEVEL_1 "models/buildables/sentry1.mdl" +#define SENTRY_MODEL_LEVEL_1_UPGRADE "models/buildables/sentry1_heavy.mdl" +#define SENTRY_MODEL_LEVEL_2 "models/buildables/sentry2.mdl" +#define SENTRY_MODEL_LEVEL_2_UPGRADE "models/buildables/sentry2_heavy.mdl" +#define SENTRY_MODEL_LEVEL_3 "models/buildables/sentry3.mdl" +#define SENTRY_MODEL_LEVEL_3_UPGRADE "models/buildables/sentry3_heavy.mdl" + +#define SENTRY_ROCKET_MODEL "models/buildables/sentry3_rockets.mdl" + +#define SENTRYGUN_MINS Vector(-20, -20, 0) +#define SENTRYGUN_MAXS Vector( 20, 20, 66) + +#define SENTRYGUN_ADD_SHELLS 40 +#define SENTRYGUN_ADD_ROCKETS 8 + +#define SENTRY_THINK_DELAY 0.05 + +#define SENTRYGUN_CONTEXT "SentrygunContext" + +#define SENTRYGUN_RECENTLY_ATTACKED_TIME 2.0 + +#define SENTRYGUN_MINIGUN_RESIST_LVL_1 0.0 +#define SENTRYGUN_MINIGUN_RESIST_LVL_2 0.15 +#define SENTRYGUN_MINIGUN_RESIST_LVL_3 0.20 + +#define SENTRYGUN_SAPPER_OWNER_DAMAGE_MODIFIER 0.66f + +#define SENTRYGUN_MAX_LEVEL_MINI 1 +#define MINI_SENTRY_SCALE 0.75f +#define DISPOSABLE_SCALE 0.65f +#define SMALL_SENTRY_SCALE 0.80f + +#define WRANGLER_DISABLE_TIME 3.0f + +enum +{ + SENTRYGUN_ATTACHMENT_MUZZLE = 0, + SENTRYGUN_ATTACHMENT_MUZZLE_ALT, + SENTRYGUN_ATTACHMENT_ROCKET, +}; + +enum target_ranges +{ + RANGE_MELEE, + RANGE_NEAR, + RANGE_MID, + RANGE_FAR, +}; + +#define VECTOR_CONE_TF_SENTRY Vector( 0.1, 0.1, 0 ) + +//----------------------------------------------------------------------------- +// Purpose: Only send the LocalWeaponData to the player carrying the weapon +//----------------------------------------------------------------------------- +void* SendProxy_SendLocalObjectDataTable( const SendProp *pProp, const void *pStruct, const void *pVarData, CSendProxyRecipients *pRecipients, int objectID ) +{ + // Get the weapon entity + CBaseObject *pObject = (CBaseObject*)pVarData; + if ( pObject ) + { + // Only send this chunk of data to the player carrying this weapon + CBasePlayer *pPlayer = ToBasePlayer( pObject->GetOwner() ); + if ( pPlayer ) + { + pRecipients->SetOnly( pPlayer->GetClientIndex() ); + return (void*)pVarData; + } + } + + return NULL; +} +REGISTER_SEND_PROXY_NON_MODIFIED_POINTER( SendProxy_SendLocalObjectDataTable ); + +BEGIN_NETWORK_TABLE_NOBASE( CObjectSentrygun, DT_SentrygunLocalData ) + SendPropInt( SENDINFO(m_iKills), -1, SPROP_VARINT | SPROP_CHANGES_OFTEN ), + SendPropInt( SENDINFO(m_iAssists), -1, SPROP_VARINT | SPROP_CHANGES_OFTEN ), +END_NETWORK_TABLE() + +IMPLEMENT_SERVERCLASS_ST( CObjectSentrygun, DT_ObjectSentrygun ) + SendPropInt( SENDINFO(m_iAmmoShells), -1, SPROP_VARINT | SPROP_CHANGES_OFTEN ), + SendPropInt( SENDINFO(m_iAmmoRockets), -1, SPROP_VARINT | SPROP_CHANGES_OFTEN ), + SendPropInt( SENDINFO(m_iState), Q_log2( SENTRY_NUM_STATES ) + 1, SPROP_UNSIGNED ), + SendPropBool( SENDINFO( m_bPlayerControlled ) ), + SendPropInt( SENDINFO( m_nShieldLevel ), 4, SPROP_UNSIGNED ), + SendPropEHandle( SENDINFO( m_hEnemy ) ), + SendPropEHandle( SENDINFO( m_hAutoAimTarget ) ), + SendPropDataTable( "SentrygunLocalData", 0, &REFERENCE_SEND_TABLE( DT_SentrygunLocalData ), SendProxy_SendLocalObjectDataTable ), +END_SEND_TABLE() + +BEGIN_DATADESC( CObjectSentrygun ) +END_DATADESC() + +LINK_ENTITY_TO_CLASS(obj_sentrygun, CObjectSentrygun); +PRECACHE_REGISTER(obj_sentrygun); + +ConVar tf_sentrygun_damage( "tf_sentrygun_damage", "16", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +ConVar tf_sentrygun_mini_damage( "tf_sentrygun_mini_damage", "8", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +ConVar tf_sentrygun_ammocheat( "tf_sentrygun_ammocheat", "0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +extern ConVar tf_obj_upgrade_per_hit; +ConVar tf_sentrygun_newtarget_dist( "tf_sentrygun_newtarget_dist", "200", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +ConVar tf_sentrygun_metal_per_shell( "tf_sentrygun_metal_per_shell", "1", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +ConVar tf_sentrygun_metal_per_rocket( "tf_sentrygun_metal_per_rocket", "2", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +ConVar tf_sentrygun_notarget( "tf_sentrygun_notarget", "0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +ConVar tf_sentrygun_max_absorbed_damage_while_controlled_for_achievement( "tf_sentrygun_max_absorbed_damage_while_controlled_for_achievement", "500", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +ConVar tf_sentrygun_kill_after_redeploy_time_achievement( "tf_sentrygun_kill_after_redeploy_time_achievement", "10", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); +extern ConVar tf_cheapobjects; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CObjectSentrygun::CObjectSentrygun() +{ + // Don't bother with health modifying attributes here, because we don't have an owner yet, and it'll be stomped in FirstSpawn() + int iHealth = GetMaxHealthForCurrentLevel(); + SetMaxHealth( iHealth ); + SetHealth( iHealth ); + SetType( OBJ_SENTRYGUN ); + + m_bFireNextFrame = false; + m_bFireRocketNextFrame = false; + m_flAutoAimStartTime = 0.f; + m_bPlayerControlled = false; + m_iLifetimeShieldedDamage = 0; + m_flFireRate = 1.f; + m_flSentryRange = SENTRY_MAX_RANGE; + m_nShieldLevel.Set( SHIELD_NONE ); + + m_lastTeammateWrenchHit = NULL; + m_lastTeammateWrenchHitTimer.Invalidate(); + + m_flScaledSentry = 1.0f; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::Spawn() +{ + m_iPitchPoseParameter = -1; + m_iYawPoseParameter = -1; + + SetModel( SENTRY_MODEL_PLACEMENT ); + + // Rotate Details + m_iRightBound = 45; + m_iLeftBound = 315; + m_iBaseTurnRate = 6; + m_flFieldOfView = VIEW_FIELD_NARROW; + + // Give the Gun some ammo + m_iAmmoShells = 0; + m_iAmmoRockets = 0; + + float flMaxAmmoMult = 1.f; + if ( GetOwner() ) + { + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetOwner(), flMaxAmmoMult, mvm_sentry_ammo ); + } + + m_iMaxAmmoShells = SENTRYGUN_MAX_SHELLS_1 * flMaxAmmoMult; + m_iMaxAmmoRockets = SENTRYGUN_MAX_ROCKETS * flMaxAmmoMult; + + m_iAmmoType = GetAmmoDef()->Index( "TF_AMMO_PRIMARY" ); + + // Start searching for enemies + m_hEnemy = NULL; + + m_flHeavyBulletResist = SENTRYGUN_MINIGUN_RESIST_LVL_1; + + m_lastTeammateWrenchHit = NULL; + m_lastTeammateWrenchHitTimer.Invalidate(); + + BaseClass::Spawn(); + + SetViewOffset( SENTRYGUN_EYE_OFFSET_LEVEL_1 ); + + SetBuildingSize(); + + m_iState.Set( SENTRY_STATE_INACTIVE ); + + SetContextThink( &CObjectSentrygun::SentryThink, gpGlobals->curtime + SENTRY_THINK_DELAY, SENTRYGUN_CONTEXT ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::FirstSpawn() +{ + m_flLastAttackedTime = 0; + + int iHealth = GetMaxHealthForCurrentLevel(); + + SetMaxHealth( iHealth ); + SetHealth( iHealth ); + + BaseClass::FirstSpawn(); +} + +Vector CObjectSentrygun::GetEnemyAimPosition( CBaseEntity* pEnemy ) const +{ + // Default to pointing to the origin + Vector vecPos = pEnemy->WorldSpaceCenter(); + + CTFPlayer* pTFEnemy = ToTFPlayer( pEnemy ); + + // This is expensive, so only do it if our target is in a state that requires it + if ( pTFEnemy ) + { + bool bShouldUseAccurateMethod = false; + + int playerFlags = pTFEnemy->GetFlags(); + // Crouch jumping makes your box weird + bShouldUseAccurateMethod |= !( playerFlags & FL_ONGROUND ) && ( playerFlags & FL_DUCKING ); + // Taunting can make your box weird + bShouldUseAccurateMethod |= pTFEnemy->m_Shared.InCond( TF_COND_TAUNTING ); + + if ( bShouldUseAccurateMethod ) + { + // Use this bone as the the aim target + int iSpineBone = pTFEnemy->LookupBone( "bip_spine_2" ); + if ( iSpineBone != -1 ) + { + QAngle angles; + pTFEnemy->GetBonePosition( iSpineBone, vecPos, angles ); + } + } + } + + return vecPos; +} + +void CObjectSentrygun::SentryThink( void ) +{ + m_flSentryRange = SENTRY_MAX_RANGE; + if ( !IsDisposableBuilding() ) + { + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetOwner(), m_flSentryRange, mult_sentry_range ); + } + + switch( m_iState ) + { + case SENTRY_STATE_INACTIVE: + case SENTRY_STATE_UPGRADING: // Base class handles this + break; + + case SENTRY_STATE_SEARCHING: + SentryRotate(); + break; + + case SENTRY_STATE_ATTACKING: + Attack(); + break; + + default: + Assert( 0 ); + break; + } + + SetContextThink( &CObjectSentrygun::SentryThink, gpGlobals->curtime + SENTRY_THINK_DELAY, SENTRYGUN_CONTEXT ); + + if ( m_nShieldLevel > 0 && (gpGlobals->curtime > m_flShieldFadeTime) ) + { + m_nShieldLevel.Set( SHIELD_NONE ); + m_vecGoalAngles.x = 0; + } + + // infinite ammo for enemy team in MvM mode + if ( TFGameRules()->IsMannVsMachineMode() && GetTeamNumber() == TF_TEAM_PVE_INVADERS ) + { + m_iAmmoRockets = SENTRYGUN_MAX_ROCKETS; + m_iMaxAmmoRockets = SENTRYGUN_MAX_ROCKETS; + m_iAmmoShells = SENTRYGUN_MAX_SHELLS_3; + m_iMaxAmmoShells = SENTRYGUN_MAX_SHELLS_3; + } +} + +void CObjectSentrygun::StartPlacement( CTFPlayer *pPlayer ) +{ + BaseClass::StartPlacement( pPlayer ); + + // Set my build size + m_vecBuildMins = SENTRYGUN_MINS; + m_vecBuildMaxs = SENTRYGUN_MAXS; + m_vecBuildMins -= Vector( 4,4,0 ); + m_vecBuildMaxs += Vector( 4,4,0 ); + + MakeMiniBuilding( pPlayer ); + MakeDisposableBuilding( pPlayer ); + MakeScaledBuilding( GetBuilder() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Start building the object +//----------------------------------------------------------------------------- +bool CObjectSentrygun::StartBuilding( CBaseEntity *pBuilder ) +{ + SetStartBuildingModel(); + + // Have to re-call this in case the player changed their weapon + // between StartPlacement and StartBuilding. + MakeMiniBuilding( GetBuilder() ); + MakeDisposableBuilding( GetBuilder() ); + MakeScaledBuilding( GetBuilder() ); + + if ( IsMiniBuilding() ) + { + SetBodygroup( FindBodygroupByName( "mini_sentry_light" ), 1 ); + } + + CreateBuildPoints(); + + SetPoseParameter( m_iPitchPoseParameter, 0.0 ); + SetPoseParameter( m_iYawPoseParameter, 0.0 ); + + SetObjectMode( IsDisposableBuilding() ? MODE_SENTRYGUN_DISPOSABLE : MODE_SENTRYGUN_NORMAL ); + + return BaseClass::StartBuilding( pBuilder ); +} + +void CObjectSentrygun::SetStartBuildingModel( void ) +{ + SetModel( SENTRY_MODEL_LEVEL_1_UPGRADE ); + m_iState.Set( SENTRY_STATE_INACTIVE ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::MakeMiniBuilding( CTFPlayer* pPlayer ) +{ + if ( !ShouldBeMiniBuilding( pPlayer ) || IsMiniBuilding() ) + return; + + BaseClass::MakeMiniBuilding( pPlayer ); + SetModelScale( MINI_SENTRY_SCALE ); + + int iHealth = GetMaxHealthForCurrentLevel(); + + SetMaxHealth( iHealth ); + SetHealth( iHealth / 2.0f ); + SetBuildingSize(); +} + +//----------------------------------------------------------------------------- +int CObjectSentrygun::GetMaxUpgradeLevel( ) +{ + if ( IsDisposableBuilding() || IsMiniBuilding() ) + return SENTRYGUN_MAX_LEVEL_MINI; + + return BaseClass::GetMaxUpgradeLevel(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::OnGoActive( void ) +{ + SetModel( SENTRY_MODEL_LEVEL_1 ); + + if ( IsMiniBuilding() ) + { + SetBodygroup( FindBodygroupByName( "mini_sentry_light" ), 1 ); + } + + m_iState.Set( SENTRY_STATE_SEARCHING ); + + // Orient it + QAngle angles = GetAbsAngles(); + + m_vecCurAngles.y = UTIL_AngleMod( angles.y ); + m_iRightBound = UTIL_AngleMod( (int)angles.y - 50 ); + m_iLeftBound = UTIL_AngleMod( (int)angles.y + 50 ); + if ( m_iRightBound > m_iLeftBound ) + { + m_iRightBound = m_iLeftBound; + m_iLeftBound = UTIL_AngleMod( (int)angles.y - 50); + } + + // Start it rotating + m_vecGoalAngles.y = m_iRightBound; + m_vecGoalAngles.x = m_vecCurAngles.x = 0; + m_bTurningRight = true; + + EmitSound( "Building_Sentrygun.Built" ); + + // if our eye pos is underwater, we're waterlevel 3, else 0 + bool bUnderwater = ( UTIL_PointContents( EyePosition() ) & MASK_WATER ) ? true : false; + SetWaterLevel( ( bUnderwater ) ? 3 : 0 ); + + if ( m_bCarryDeploy ) + { + m_iAmmoShells = m_iOldAmmoShells; + m_iAmmoRockets = m_iOldAmmoRockets; + } + else + { + m_iAmmoShells = m_iMaxAmmoShells; + m_iAmmoRockets = m_iMaxAmmoRockets; + } + + // Init attachments for level 1 sentry gun + m_iAttachments[SENTRYGUN_ATTACHMENT_MUZZLE] = LookupAttachment( "muzzle" ); + m_iAttachments[SENTRYGUN_ATTACHMENT_MUZZLE_ALT] = 0; + m_iAttachments[SENTRYGUN_ATTACHMENT_ROCKET] = 0; + + BaseClass::OnGoActive(); + + IGameEvent * event = gameeventmanager->CreateEvent( "sentry_on_go_active" ); + if ( event ) + { + event->SetInt( "index", entindex() ); // object entity index + + gameeventmanager->FireEvent( event, true ); // don't send to clients + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::Precache() +{ + BaseClass::Precache(); + + int iModelIndex; + + // Models + PrecacheModel( SENTRY_MODEL_PLACEMENT ); + + iModelIndex = PrecacheModel( SENTRY_MODEL_LEVEL_1 ); + PrecacheGibsForModel( iModelIndex ); + + iModelIndex = PrecacheModel( SENTRY_MODEL_LEVEL_1_UPGRADE ); + PrecacheGibsForModel( iModelIndex ); + + iModelIndex = PrecacheModel( SENTRY_MODEL_LEVEL_2 ); + PrecacheGibsForModel( iModelIndex ); + + iModelIndex = PrecacheModel( SENTRY_MODEL_LEVEL_2_UPGRADE ); + PrecacheGibsForModel( iModelIndex ); + + iModelIndex = PrecacheModel( SENTRY_MODEL_LEVEL_3 ); + PrecacheGibsForModel( iModelIndex ); + + iModelIndex = PrecacheModel( SENTRY_MODEL_LEVEL_3_UPGRADE ); + PrecacheGibsForModel( iModelIndex ); + + PrecacheModel( SENTRY_ROCKET_MODEL ); + PrecacheModel( "models/effects/sentry1_muzzle/sentry1_muzzle.mdl" ); + + PrecacheModel( "models/buildables/sentry_shield.mdl" ); + + // Sounds + PrecacheScriptSound( "Building_Sentrygun.Fire" ); + PrecacheScriptSound( "Building_Sentrygun.Fire2" ); // level 2 sentry + PrecacheScriptSound( "Building_Sentrygun.Fire3" ); // level 3 sentry + PrecacheScriptSound( "Building_Sentrygun.FireRocket" ); + PrecacheScriptSound( "Building_Sentrygun.Alert" ); + PrecacheScriptSound( "Building_Sentrygun.AlertTarget" ); + PrecacheScriptSound( "Building_Sentrygun.Idle" ); + PrecacheScriptSound( "Building_Sentrygun.Idle2" ); // level 2 sentry + PrecacheScriptSound( "Building_Sentrygun.Idle3" ); // level 3 sentry + PrecacheScriptSound( "Building_Sentrygun.Built" ); + PrecacheScriptSound( "Building_Sentrygun.Empty" ); + PrecacheScriptSound( "Building_Sentrygun.ShaftFire" ); + PrecacheScriptSound( "Building_Sentrygun.ShaftFire2" ); + PrecacheScriptSound( "Building_Sentrygun.ShaftFire3" ); + PrecacheScriptSound( "Building_Sentrygun.ShaftLaserPass" ); + PrecacheScriptSound( "Building_MiniSentrygun.Fire" ); + + PrecacheParticleSystem( "sentrydamage_1" ); + PrecacheParticleSystem( "sentrydamage_2" ); + PrecacheParticleSystem( "sentrydamage_3" ); + PrecacheParticleSystem( "sentrydamage_4" ); +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +bool CObjectSentrygun::CanBeUpgraded( CTFPlayer *pPlayer ) +{ + if ( m_bWasMapPlaced && !HasSpawnFlags(SF_SENTRY_UPGRADEABLE) ) + { + return false; + } + + return BaseClass::CanBeUpgraded( pPlayer ); +} + +//----------------------------------------------------------------------------- +// Purpose: Raises the Sentrygun one level +//----------------------------------------------------------------------------- +void CObjectSentrygun::StartUpgrading( void ) +{ + BaseClass::StartUpgrading(); + + float flMaxAmmoMult = 1.f; + if ( GetOwner() ) + { + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetOwner(), flMaxAmmoMult, mvm_sentry_ammo ); + } + + switch( m_iUpgradeLevel ) + { + case 2: + SetModel( SENTRY_MODEL_LEVEL_2_UPGRADE ); + m_flHeavyBulletResist = SENTRYGUN_MINIGUN_RESIST_LVL_2; + SetViewOffset( SENTRYGUN_EYE_OFFSET_LEVEL_2 ); + m_iMaxAmmoShells = SENTRYGUN_MAX_SHELLS_2 * flMaxAmmoMult; + break; + case 3: + SetModel( SENTRY_MODEL_LEVEL_3_UPGRADE ); + if ( !m_bCarryDeploy ) + { + m_iAmmoRockets = SENTRYGUN_MAX_ROCKETS; + } + m_flHeavyBulletResist = SENTRYGUN_MINIGUN_RESIST_LVL_3; + SetViewOffset( SENTRYGUN_EYE_OFFSET_LEVEL_3 ); + m_iMaxAmmoShells = SENTRYGUN_MAX_SHELLS_3 * flMaxAmmoMult; + break; + default: + Assert(0); + break; + } + + // more ammo capability + if ( !m_bCarryDeploy ) + { + m_iAmmoShells = m_iMaxAmmoShells; + } + + m_iState.Set( SENTRY_STATE_UPGRADING ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::FinishUpgrading( void ) +{ + BaseClass::FinishUpgrading(); + + m_iState.Set( SENTRY_STATE_SEARCHING ); + m_hEnemy = NULL; + + switch( m_iUpgradeLevel ) + { + case 1: + // This can happen when a saper downgrades a sentry + // No need to do anything here + break; + case 2: + SetModel( SENTRY_MODEL_LEVEL_2 ); + break; + case 3: + SetModel( SENTRY_MODEL_LEVEL_3 ); + break; + default: + Assert(0); + break; + } + + // Look up the new attachments + m_iAttachments[SENTRYGUN_ATTACHMENT_MUZZLE] = LookupAttachment( "muzzle_l" ); + m_iAttachments[SENTRYGUN_ATTACHMENT_MUZZLE_ALT] = LookupAttachment( "muzzle_r" ); + m_iAttachments[SENTRYGUN_ATTACHMENT_ROCKET] = LookupAttachment( "rocket_l" ); +} + +//----------------------------------------------------------------------------- +// Hit by a friendly engineer's wrench +//----------------------------------------------------------------------------- +bool CObjectSentrygun::OnWrenchHit( CTFPlayer *pPlayer, CTFWrench *pWrench, Vector hitLoc ) +{ + if ( IsDisposableBuilding() ) + return false; + + bool bDidWork = false; + + // If the player repairs it at all, we're done + if ( GetHealth() < GetMaxHealth() ) + { + // STAGING_ENGY + // Mod repair value by shield value + float flRepairValue = pWrench->GetRepairValue(); + if ( m_nShieldLevel == SHIELD_NORMAL ) + { + flRepairValue *= SHIELD_NORMAL_VALUE; + } + + if ( Command_Repair( pPlayer, flRepairValue ) ) + { + DoWrenchHitEffect( hitLoc, true, false ); + bDidWork = true; + } + } + + // Don't put in upgrade metal until the sentry is fully healed + if ( !bDidWork ) + { + if ( CheckUpgradeOnHit( pPlayer ) ) + { + DoWrenchHitEffect( hitLoc, false, true ); + bDidWork = true; + } + } + + if ( !IsUpgrading() ) + { + // player ammo into rockets + // 1 ammo = 1 shell + // 2 ammo = 1 rocket + // only fill rockets if we have extra shells + + int iPlayerMetal = pPlayer->GetAmmoCount( TF_AMMO_METAL ); + + // If the sentry has less that 100% ammo, put some ammo in it + if ( m_iAmmoShells < m_iMaxAmmoShells && iPlayerMetal > 0 ) + { + int iMaxShellsPlayerCanAfford = (int)( (float)iPlayerMetal / tf_sentrygun_metal_per_shell.GetFloat() ); + + // cap the amount we can add + int iAmountToAdd = MIN( SENTRYGUN_ADD_SHELLS, iMaxShellsPlayerCanAfford ); + iAmountToAdd = MIN( ( m_iMaxAmmoShells - m_iAmmoShells ), iAmountToAdd ); + + // STAGING_ENGY + // Mod Ammo if shielded + if ( m_nShieldLevel == SHIELD_NORMAL ) + { + iAmountToAdd *= SHIELD_NORMAL_VALUE; + } + + pPlayer->RemoveAmmo( iAmountToAdd * tf_sentrygun_metal_per_shell.GetInt(), TF_AMMO_METAL ); + m_iAmmoShells += iAmountToAdd; + + if ( iAmountToAdd > 0 ) + { + bDidWork = true; + } + } + + // One rocket per two ammo + iPlayerMetal = pPlayer->GetAmmoCount( TF_AMMO_METAL ); + + if ( m_iAmmoRockets < m_iMaxAmmoRockets && m_iUpgradeLevel == 3 && iPlayerMetal > 0 ) + { + int iMaxRocketsPlayerCanAfford = (int)( (float)iPlayerMetal / tf_sentrygun_metal_per_rocket.GetFloat() ); + + int iAmountToAdd = MIN( ( SENTRYGUN_ADD_ROCKETS ), iMaxRocketsPlayerCanAfford ); + iAmountToAdd = MIN( ( m_iMaxAmmoRockets - m_iAmmoRockets ), iAmountToAdd ); + + // STAGING_ENGY + // Mod Ammo if shielded + if ( m_nShieldLevel == SHIELD_NORMAL ) + { + iAmountToAdd *= SHIELD_NORMAL_VALUE; + } + + pPlayer->RemoveAmmo( iAmountToAdd * tf_sentrygun_metal_per_rocket.GetFloat(), TF_AMMO_METAL ); + m_iAmmoRockets += iAmountToAdd; + + if ( iAmountToAdd > 0 ) + { + bDidWork = true; + } + } + } + + if ( GetOwner() != pPlayer ) + { + if ( bDidWork && m_bPlayerControlled ) + { + pPlayer->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_HELP_MANUAL_SENTRY, 1 ); + } + + // keep track of who last hit us with a wrench for kill assists + m_lastTeammateWrenchHit = pPlayer; + m_lastTeammateWrenchHitTimer.Start(); + } + + return bDidWork; +} + +//----------------------------------------------------------------------------- +// Debug infos +//----------------------------------------------------------------------------- +int CObjectSentrygun::DrawDebugTextOverlays(void) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + char tempstr[512]; + + Q_snprintf( tempstr, sizeof( tempstr ), "Level: %d", m_iUpgradeLevel.Get() ); + EntityText(text_offset,tempstr,0); + text_offset++; + + Q_snprintf( tempstr, sizeof( tempstr ), "Shells: %d / %d", m_iAmmoShells.Get(), m_iMaxAmmoShells.Get() ); + EntityText(text_offset,tempstr,0); + text_offset++; + + if ( m_iUpgradeLevel == 3 ) + { + Q_snprintf( tempstr, sizeof( tempstr ), "Rockets: %d / %d", m_iAmmoRockets.Get(), m_iMaxAmmoRockets.Get() ); + EntityText(text_offset,tempstr,0); + text_offset++; + } + + Q_snprintf( tempstr, sizeof( tempstr ), "Upgrade metal %d", m_iUpgradeMetal.Get() ); + EntityText(text_offset,tempstr,0); + text_offset++; + + Vector vecSrc = EyePosition(); + Vector forward; + + // m_vecCurAngles + AngleVectors( m_vecCurAngles, &forward ); + NDebugOverlay::Line( vecSrc, vecSrc + forward * 200, 0, 255, 0, false, 0.1 ); + + // m_vecGoalAngles + AngleVectors( m_vecGoalAngles, &forward ); + NDebugOverlay::Line( vecSrc, vecSrc + forward * 200, 0, 0, 255, false, 0.1 ); + } + + return text_offset; +} + +//----------------------------------------------------------------------------- +// Returns the sentry targeting range the target is in +//----------------------------------------------------------------------------- +int CObjectSentrygun::Range( CBaseEntity *pTarget ) +{ + Vector vecOrg = EyePosition(); + Vector vecTargetOrg = pTarget->EyePosition(); + + int iDist = ( vecTargetOrg - vecOrg ).Length(); + + if (iDist < 132) + return RANGE_MELEE; + if (iDist < 550) + return RANGE_NEAR; + if (iDist < m_flSentryRange) + return RANGE_MID; + return RANGE_FAR; +} + +//----------------------------------------------------------------------------- +// Look for a target +//----------------------------------------------------------------------------- +bool CObjectSentrygun::FindTarget() +{ + if ( m_bPlayerControlled ) + { + m_flShieldFadeTime = gpGlobals->curtime + WRANGLER_DISABLE_TIME; + } + m_bPlayerControlled = false; + + // Disable the sentry guns for ifm. + if ( tf_sentrygun_notarget.GetBool() ) + return false; + + if ( IsInCommentaryMode() ) + return false; + + // Sapper, etc. + if ( IsDisabled() ) + return false; + + // Loop through players within SENTRY_MAX_RANGE units (sentry range). + Vector vecSentryOrigin = EyePosition(); + + // find the enemy team + int iEnemyTeam = ( GetTeamNumber() == TF_TEAM_BLUE ) ? TF_TEAM_RED : TF_TEAM_BLUE; + CTFTeam *pTeam = TFTeamMgr()->GetTeam( iEnemyTeam ); + if ( !pTeam ) + return false; + + // If we have an enemy get his minimum distance to check against. + Vector vecSegment; + Vector vecTargetCenter; + float flMinDist2 = m_flSentryRange * m_flSentryRange; + CBaseEntity *pTargetCurrent = NULL; + CBaseEntity *pTargetOld = m_hEnemy.Get(); + float flOldTargetDist2 = FLT_MAX; + bool bDummyTarget = false; + + // Sentry Decoy + // If we already have a sentry decoy target, keep shooting at it + // Otherwise look for a sentry decoy's first + if ( pTargetCurrent == NULL ) + { + CTFTargetDummy *pDummy = dynamic_cast<CTFTargetDummy*>( pTargetOld ); + if ( pDummy ) + { + pTargetCurrent = pDummy; + bDummyTarget = true; + } + else + { + // Search through all dummies and find one in range + for ( int i = 0; i < ITFTargetDummy::AutoList().Count(); ++i ) + { + pDummy = static_cast<CTFTargetDummy*>( ITFTargetDummy::AutoList()[i] ); + if ( InSameTeam( pDummy ) ) + continue; + + vecTargetCenter = pDummy->GetAbsOrigin(); + vecTargetCenter += pDummy->GetViewOffset(); + VectorSubtract( vecTargetCenter, vecSentryOrigin, vecSegment ); + float flDist2 = vecSegment.LengthSqr(); + + // Check to see if the target is closer than the already validated target. + if ( flDist2 > flMinDist2 ) + continue; + + // Ray trace!!! + if ( FVisible( pDummy, MASK_SHOT | CONTENTS_GRATE ) ) + { + pTargetCurrent = pDummy; + bDummyTarget = true; + } + } + } + } + + // If our builder has an active laser pointer we don't seek targets. + CTFPlayer* pBuilder = GetBuilder(); + if ( pBuilder ) + { + // CTFLaserPointer* pPointer = static_cast<CTFLaserPointer*>( pBuilder->Weapon_OwnsThisID( TF_WEAPON_LASER_POINTER ) ); + // FIX ME: Temp fix until we find out why the pointer thinks its deployed after spawn + CTFLaserPointer* pPointer = dynamic_cast<CTFLaserPointer*>( pBuilder->GetActiveWeapon() ); + if ( pPointer && pPointer->HasLaserDot() && !IsDisposableBuilding() ) + { + m_bPlayerControlled = true; + m_nShieldLevel.Set( SHIELD_NORMAL ); + m_flShieldFadeTime = gpGlobals->curtime + WRANGLER_DISABLE_TIME; + + // If not target dummy, use laserdot, otherwise targetdummy overrides + if ( !bDummyTarget || !pTargetCurrent ) + { + pTargetCurrent = pPointer->GetLaserDot(); + + // Are we in our brief auto aim period? + float flAutoAimTime = gpGlobals->curtime - m_flAutoAimStartTime; + if ( m_hAutoAimTarget && (flAutoAimTime < 0.2f) ) + { + // Only use the auto aim target if we can actually range to him. + Vector vecSrc; + QAngle vecAng; + GetAttachment( GetFireAttachment(), vecSrc, vecAng ); + Vector vecEnemy = GetEnemyAimPosition( m_hAutoAimTarget ); + trace_t trace; + CTraceFilterIgnoreTeammatesAndTeamObjects filter( pBuilder, COLLISION_GROUP_NONE, pBuilder->GetTeamNumber() ); + UTIL_TraceLine( vecSrc, vecEnemy, MASK_SOLID, &filter, &trace ); + if ( trace.m_pEnt == m_hAutoAimTarget ) + { + pTargetCurrent = m_hAutoAimTarget; + } + else + { + m_hAutoAimTarget = NULL; + } + } + else + { + m_hAutoAimTarget = NULL; + } + } + + if ( pTargetCurrent->GetAbsOrigin().DistTo( vecSentryOrigin ) > 30.f ) + { + if ( pTargetCurrent != pTargetOld ) + { + FoundTarget( pTargetCurrent, vecSentryOrigin, true ); + } + return true; + } + else + { + pTargetCurrent = NULL; + } + } + } + + // Don't auto track to targets while under the effects of the player shield. + // The shield fades 3 seconds after we disengage from player control. + if ( m_nShieldLevel == SHIELD_NORMAL ) + return false; + + // is there an active truce? + bool bTruceActive = TFGameRules() && TFGameRules()->IsTruceActive(); + + if ( ( pTargetCurrent == NULL ) && !bTruceActive ) + { + // Sentries will try to target players first, then objects. However, if the enemy held was an object it will continue + // to try and attack it first. + int nTeamCount = pTeam->GetNumPlayers(); + for ( int iPlayer = 0; iPlayer < nTeamCount; ++iPlayer ) + { + CTFPlayer *pTargetPlayer = static_cast<CTFPlayer*>( pTeam->GetPlayer( iPlayer ) ); + if ( pTargetPlayer == NULL ) + continue; + + // Make sure the player is alive. + if ( !pTargetPlayer->IsAlive() ) + continue; + + if ( pTargetPlayer->GetFlags() & FL_NOTARGET ) + continue; + + vecTargetCenter = pTargetPlayer->GetAbsOrigin(); + vecTargetCenter += pTargetPlayer->GetViewOffset(); + VectorSubtract( vecTargetCenter, vecSentryOrigin, vecSegment ); + float flDist2 = vecSegment.LengthSqr(); + + // Check to see if the target is closer than the already validated target. + if ( flDist2 > flMinDist2 ) + continue; + + // It is closer, check to see if the target is valid. + if ( ValidTargetPlayer( pTargetPlayer, vecSentryOrigin, vecTargetCenter ) ) + { + flMinDist2 = flDist2; + pTargetCurrent = pTargetPlayer; + + // Store the current target distance if we come across it + if ( pTargetPlayer == pTargetOld ) + { + flOldTargetDist2 = flDist2; + } + } + } + } + + // If we already have a target, don't check objects. + if ( pTargetCurrent == NULL ) + { + // target non-player bots + CUtlVector< INextBot * > botVector; + TheNextBots().CollectAllBots( &botVector ); + + float closeBotRangeSq = m_flSentryRange * m_flSentryRange; + + for( int b=0; b<botVector.Count(); ++b ) + { + CBaseCombatCharacter *bot = botVector[b]->GetEntity(); + + Vector vecBotTarget = GetEnemyAimPosition( bot ); + float rangeSq = ( vecBotTarget - vecSentryOrigin ).LengthSqr(); + + if ( rangeSq < closeBotRangeSq ) + { + if ( ValidTargetBot( bot, vecSentryOrigin, vecBotTarget ) ) + { + closeBotRangeSq = rangeSq; + pTargetCurrent = bot; + } + } + } + + if ( ( pTargetCurrent == NULL ) && !bTruceActive ) + { + // target objects + int nTeamObjectCount = pTeam->GetNumObjects(); + for ( int iObject = 0; iObject < nTeamObjectCount; ++iObject ) + { + CBaseObject *pTargetObject = pTeam->GetObject( iObject ); + if ( !pTargetObject ) + continue; + + vecTargetCenter = pTargetObject->GetAbsOrigin(); + vecTargetCenter += pTargetObject->GetViewOffset(); + VectorSubtract( vecTargetCenter, vecSentryOrigin, vecSegment ); + float flDist2 = vecSegment.LengthSqr(); + + // Store the current target distance if we come across it + if ( pTargetObject == pTargetOld ) + { + flOldTargetDist2 = flDist2; + } + + // Check to see if the target is closer than the already validated target. + if ( flDist2 > flMinDist2 ) + continue; + + // It is closer, check to see if the target is valid. + if ( ValidTargetObject( pTargetObject, vecSentryOrigin, vecTargetCenter ) ) + { + flMinDist2 = flDist2; + pTargetCurrent = pTargetObject; + } + } + } + } + + // We have a target. + if ( pTargetCurrent ) + { + if ( pTargetCurrent != pTargetOld ) + { + // Always target dummies + // flMinDist2 is the new target's distance + // flOldTargetDist2 is the old target's distance + // Don't switch unless the new target is closer by some percentage + if ( bDummyTarget || flMinDist2 < ( flOldTargetDist2 * 0.75f ) ) + { + FoundTarget( pTargetCurrent, vecSentryOrigin ); + } + } + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CObjectSentrygun::ValidTargetPlayer( CTFPlayer *pPlayer, const Vector &vecStart, const Vector &vecEnd ) +{ + // Keep shooting at spies that go invisible after we acquire them as a target. + if ( pPlayer->m_Shared.GetPercentInvisible() > 0.5 ) + return false; + + // Keep shooting at spies that disguise after we acquire them as at a target. + if ( pPlayer->m_Shared.InCond( TF_COND_DISGUISED ) && pPlayer->m_Shared.GetDisguiseTeam() == GetTeamNumber() && pPlayer != m_hEnemy ) + return false; + + // Don't shoot spys that are pretending to be a dispenser + if ( pPlayer->m_Shared.InCond( TF_COND_DISGUISED_AS_DISPENSER ) ) + return false; + + // Don't target spies after they OnKill disguise with 'Your Eternal Reward' + if ( ( pPlayer->m_Shared.InCond( TF_COND_DISGUISING ) || pPlayer->m_Shared.InCond( TF_COND_DISGUISED ) ) + && pPlayer->m_Shared.GetDisguiseTeam() == GetTeamNumber() ) + { + CTFKnife *pKnife = dynamic_cast<CTFKnife *>( pPlayer->GetActiveTFWeapon() ); + if ( pKnife && pKnife->GetKnifeType() == KNIFE_DISGUISE_ONKILL ) + return false; + } + + // Not across water boundary. + if ( ( GetWaterLevel() == 0 && pPlayer->GetWaterLevel() >= 3 ) || ( GetWaterLevel() == 3 && pPlayer->GetWaterLevel() <= 0 ) ) + return false; + + // Ray trace!!! + return FVisible( pPlayer, MASK_SHOT | CONTENTS_GRATE ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CObjectSentrygun::ValidTargetObject( CBaseObject *pObject, const Vector &vecStart, const Vector &vecEnd ) +{ + // Ignore objects being placed, they are not real objects yet. + if ( pObject->IsPlacing() ) + return false; + + // Ignore sappers. + if ( pObject->MustBeBuiltOnAttachmentPoint() ) + return false; + + // Not across water boundary. + if ( ( GetWaterLevel() == 0 && pObject->GetWaterLevel() >= 3 ) || ( GetWaterLevel() == 3 && pObject->GetWaterLevel() <= 0 ) ) + return false; + + if ( pObject->GetObjectFlags() & OF_DOESNT_HAVE_A_MODEL ) + return false; + + // Ray trace. + return FVisible( pObject, MASK_SHOT | CONTENTS_GRATE ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CObjectSentrygun::ValidTargetBot( CBaseCombatCharacter *pBot, const Vector &vecStart, const Vector &vecEnd ) +{ + // Already collected all of the players in FindTarget() + if ( pBot->IsPlayer() ) + return false; + + // Don't want to shoot bots that are dead, on the same team, or aren't solid (they won't take damage anyway) + if ( !pBot->IsAlive() || pBot->InSameTeam( this ) || pBot->IsSolidFlagSet( FSOLID_NOT_SOLID ) ) + return false; + + // Not across water boundary. + if ( ( GetWaterLevel() == 0 && pBot->GetWaterLevel() >= 3 ) || ( GetWaterLevel() == 3 && pBot->GetWaterLevel() <= 0 ) ) + return false; + + if ( TFGameRules() && TFGameRules()->IsPlayingRobotDestructionMode() ) + { + CTFRobotDestruction_Robot *pRobot = dynamic_cast< CTFRobotDestruction_Robot* >( pBot ); + if ( pRobot && pRobot->GetShieldedState() ) + return false; + } + + // Ray trace. + CBaseEntity *pBlocker; + bool bVisible = FVisible( pBot, MASK_SHOT | CONTENTS_GRATE, &pBlocker ); + + if ( bVisible ) + return true; + + // Also valid if it's parented to the blocker + if ( pBlocker == pBot->GetParent() ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Found a Target +//----------------------------------------------------------------------------- +void CObjectSentrygun::FoundTarget( CBaseEntity *pTarget, const Vector &vecSoundCenter, bool bNoSound ) +{ + m_hEnemy = pTarget; + + if ( ( m_iAmmoShells > 0 ) || ( m_iAmmoRockets > 0 && m_iUpgradeLevel == 3 ) ) + { + // Play one sound to everyone but the target. + CPASFilter filter( vecSoundCenter ); + + if ( pTarget->IsPlayer() ) + { + CTFPlayer *pPlayer = ToTFPlayer( pTarget ); + + // Play a specific sound just to the target and remove it from the general recipient list. + if ( !bNoSound ) + { + CSingleUserRecipientFilter singleFilter( pPlayer ); + EmitSentrySound( singleFilter, entindex(), "Building_Sentrygun.AlertTarget" ); + filter.RemoveRecipient( pPlayer ); + + // if the target is a bot, alert it + CTFBot *bot = ToTFBot( pPlayer ); + if ( bot ) + { + bot->GetVisionInterface()->AddKnownEntity( this ); + bot->RememberEnemySentry( this, bot->GetAbsOrigin() ); + } + } + } + + if ( !bNoSound ) + { + EmitSentrySound( filter, entindex(), "Building_Sentrygun.Alert" ); + } + } + + // Update timers, we are attacking now! + m_iState.Set( SENTRY_STATE_ATTACKING ); + m_flNextAttack = gpGlobals->curtime + SENTRY_THINK_DELAY; + if ( m_flNextRocketAttack < gpGlobals->curtime ) + { + m_flNextRocketAttack = gpGlobals->curtime;// + 0.5; + } +} + +//----------------------------------------------------------------------------- +// FInViewCone - returns true is the passed ent is in +// the caller's forward view cone. The dot product is performed +// in 2d, making the view cone infinitely tall. +//----------------------------------------------------------------------------- +bool CObjectSentrygun::FInViewCone ( CBaseEntity *pEntity ) +{ + Vector forward; + AngleVectors( m_vecCurAngles, &forward ); + + Vector2D vec2LOS = ( pEntity->GetAbsOrigin() - GetAbsOrigin() ).AsVector2D(); + vec2LOS.NormalizeInPlace(); + + float flDot = vec2LOS.Dot( forward.AsVector2D() ); + + if ( flDot > m_flFieldOfView ) + { + return true; + } + else + { + return false; + } +} + +//----------------------------------------------------------------------------- +// Make sure our target is still valid, and if so, fire at it +//----------------------------------------------------------------------------- +void CObjectSentrygun::Attack() +{ + StudioFrameAdvance( ); + + if ( IsUsingReverseBuild() || !FindTarget() ) + { + m_iState.Set( SENTRY_STATE_SEARCHING ); + m_hEnemy = NULL; + return; + } + + // Track enemy + Vector vecMid = EyePosition(); + Vector vecMidEnemy = GetEnemyAimPosition( m_hEnemy ); + Vector vecDirToEnemy = vecMidEnemy - vecMid; + + QAngle angToTarget; + VectorAngles( vecDirToEnemy, angToTarget ); + + angToTarget.y = UTIL_AngleMod( angToTarget.y ); + if (angToTarget.x < -180) + angToTarget.x += 360; + if (angToTarget.x > 180) + angToTarget.x -= 360; + + // now all numbers should be in [1...360] + // pin to turret limitations to [-50...50] + if (angToTarget.x > 50) + angToTarget.x = 50; + else if (angToTarget.x < -50) + angToTarget.x = -50; + m_vecGoalAngles.y = angToTarget.y; + m_vecGoalAngles.x = angToTarget.x; + + MoveTurret(); + + // Fire on the target if it's within 10 units of being aimed right at it + if ( m_flNextAttack <= gpGlobals->curtime && (m_vecGoalAngles - m_vecCurAngles).Length() <= 10 ) + { + if ( !m_bPlayerControlled || m_bFireNextFrame ) + { + m_bFireNextFrame = false; + Fire(); + } + + m_flFireRate = 1.f; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetOwner(), m_flFireRate, mult_sentry_firerate ); + + if ( m_bPlayerControlled ) + { + m_flFireRate *= 0.5f; + } + + if ( IsMiniBuilding() && !IsDisposableBuilding() ) + { + m_flFireRate *= 0.75f; + } + + if ( GetBuilder() && GetBuilder()->m_Shared.InCond( TF_COND_CRITBOOSTED_USER_BUFF ) ) + { + m_flFireRate *= 0.4f; + } + + if ( m_iUpgradeLevel == 1 ) + { + // Level 1 sentries fire slower + m_flNextAttack = gpGlobals->curtime + (0.2*m_flFireRate); + } + else + { + m_flNextAttack = gpGlobals->curtime + (0.1*m_flFireRate); + } + } + else + { + // SetSentryAnim( TFTURRET_ANIM_SPIN ); + } + + if ( m_bPlayerControlled && m_bFireRocketNextFrame ) + { + m_bFireRocketNextFrame = false; + FireRocket(); + } +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +bool CObjectSentrygun::FireRocket() +{ + if ( m_flNextRocketAttack >= gpGlobals->curtime || m_iAmmoRockets <= 0 ) + return false; + + if ( m_hEnemy.Get() == NULL ) + return false; + + Vector vecAimDir; + + Vector vecSrc; + QAngle vecAng; + + GetAttachment( m_iAttachments[SENTRYGUN_ATTACHMENT_ROCKET], vecSrc, vecAng ); + + Vector vecEnemyPos = GetEnemyAimPosition( m_hEnemy ); + vecAimDir = vecEnemyPos - vecSrc; + vecAimDir.NormalizeInPlace(); + + // If we cannot see their WorldSpaceCenter ( possible, as we do our target finding based + // on the eye position of the target ) then fire at the eye position + trace_t tr; + + CTraceFilterSimple traceFilter( this, COLLISION_GROUP_NONE ); + ITraceFilter *pFilterChain = NULL; + + CTraceFilterIgnoreFriendlyCombatItems traceFilterCombatItem( this, COLLISION_GROUP_NONE, GetTeamNumber() ); + if ( TFGameRules() && TFGameRules()->GameModeUsesUpgrades() ) + { + // Ignore teammates and their (physical) upgrade items in MvM + pFilterChain = &traceFilterCombatItem; + } + + CTraceFilterChain traceFilterChain( &traceFilter, pFilterChain ); + UTIL_TraceLine( vecSrc, vecEnemyPos, MASK_SOLID, &traceFilterChain, &tr); + + if ( m_bPlayerControlled || (tr.m_pEnt && !tr.m_pEnt->IsWorld()) ) + { + // NOTE: vecAng is not actually set by GetAttachment!!! + QAngle angDir; + VectorAngles( vecAimDir, angDir ); + + EmitSentrySound( "Building_Sentrygun.FireRocket" ); + + QAngle angAimDir; + VectorAngles( vecAimDir, angAimDir ); + CTFProjectile_SentryRocket *pProjectile = CTFProjectile_SentryRocket::Create( vecSrc, angAimDir, this, GetBuilder() ); + if ( pProjectile ) + { + int iDamage = 100; + CALL_ATTRIB_HOOK_INT_ON_OTHER( GetOwner(), iDamage, mult_engy_sentry_damage ); + pProjectile->SetDamage( iDamage ); + } + + // Setup next rocket shot + if ( m_bPlayerControlled ) + { + m_flNextRocketAttack = gpGlobals->curtime + 2.25; + } + else + { + AddGesture( ACT_RANGE_ATTACK2 ); + m_flNextRocketAttack = gpGlobals->curtime + 3; + } + + if ( !tf_sentrygun_ammocheat.GetBool() && !HasSpawnFlags( SF_SENTRY_INFINITE_AMMO ) ) + { + m_iAmmoRockets--; + } + } + + m_timeSinceLastFired.Start(); + + return true; +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +int CObjectSentrygun::GetFireAttachment() +{ + int iAttachment; + + if ( m_iUpgradeLevel > 1 && m_iLastMuzzleAttachmentFired == m_iAttachments[SENTRYGUN_ATTACHMENT_MUZZLE] ) + { + // level 2 and 3 turrets alternate muzzles each time they fizzy fizzy fire. + iAttachment = m_iAttachments[SENTRYGUN_ATTACHMENT_MUZZLE_ALT]; + } + else + { + iAttachment = m_iAttachments[SENTRYGUN_ATTACHMENT_MUZZLE]; + } + m_iLastMuzzleAttachmentFired = iAttachment; + + return iAttachment; +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +void CObjectSentrygun::OnKilledEnemy(CBasePlayer* pVictim) +{ + if ( !pVictim ) + return; + + CTFPlayer *pOwner = GetOwner(); + if ( !pOwner ) + return; + + if ( m_bPlayerControlled && pVictim->GetAbsOrigin().DistToSqr( GetAbsOrigin() ) > ( m_flSentryRange * m_flSentryRange ) ) + { + pOwner->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_MANUAL_SENTRY_KILLS_BEYOND_RANGE ); + } + + CTFPlayer *pCTFVictim = static_cast<CTFPlayer *>( pVictim ); + if ( pCTFVictim->GetControlPointStandingOn() != NULL ) + { + pOwner->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_SENTRY_KILL_CAPS, 1 ); + } + + if ( (gpGlobals->curtime - GetCarryDeployTime() < tf_sentrygun_kill_after_redeploy_time_achievement.GetInt()) && + GetUpgradeLevel() == 3 ) + { + pOwner->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_MOVE_SENTRY_GET_KILL ); + } +} + +//----------------------------------------------------------------------------- +// Fire on our target +//----------------------------------------------------------------------------- +bool CObjectSentrygun::Fire() +{ + //NDebugOverlay::Cross3D( m_hEnemy->WorldSpaceCenter(), 10, 255, 0, 0, false, 0.1 ); + + Vector vecAimDir; + + // Level 3 Turrets fire rockets every 3 seconds + if ( m_iUpgradeLevel == 3 && + m_iAmmoRockets > 0 && + m_flNextRocketAttack < gpGlobals->curtime && + !m_bPlayerControlled ) + { + FireRocket(); + } + + // All turrets fire shells + if ( m_iAmmoShells > 0 ) + { + if ( !IsPlayingGesture( ACT_RANGE_ATTACK1 ) ) + { + RemoveGesture( ACT_RANGE_ATTACK1_LOW ); + AddGesture( ACT_RANGE_ATTACK1 ); + } + + if ( m_hEnemy.Get() == NULL ) + return false; + + Vector vecSrc; + QAngle vecAng; + + int iAttachment = GetFireAttachment(); + GetAttachment( iAttachment, vecSrc, vecAng ); + + Vector vecMidEnemy = GetEnemyAimPosition( m_hEnemy ); + + // If we cannot see their WorldSpaceCenter ( possible, as we do our target finding based + // on the eye position of the target ) then fire at the eye position + trace_t tr; + CTraceFilterSimple traceFilter( this, COLLISION_GROUP_NONE ); + ITraceFilter *pFilterChain = NULL; + + CTraceFilterIgnoreFriendlyCombatItems traceFilterCombatItem( this, COLLISION_GROUP_NONE, GetTeamNumber() ); + if ( TFGameRules() && TFGameRules()->GameModeUsesUpgrades() ) + { + // Ignore teammates and their (physical) upgrade items in MvM + pFilterChain = &traceFilterCombatItem; + } + + CTraceFilterChain traceFilterChain( &traceFilter, pFilterChain ); + UTIL_TraceLine( vecSrc, vecMidEnemy, MASK_SOLID, &traceFilterChain, &tr); + + if ( !tr.m_pEnt || tr.m_pEnt->IsWorld() ) + { + // Hack it lower a little bit.. + // The eye position is not always within the hitboxes for a standing TF Player + vecMidEnemy = m_hEnemy->EyePosition() + Vector(0,0,-5); + } + + vecAimDir = vecMidEnemy - vecSrc; + + float flDistToTarget = vecAimDir.Length(); + + vecAimDir.NormalizeInPlace(); + + //NDebugOverlay::Cross3D( vecSrc, 10, 255, 0, 0, false, 0.1 ); + + FireBulletsInfo_t info; + + info.m_vecSrc = vecSrc; + info.m_vecDirShooting = vecAimDir; + info.m_iTracerFreq = 1; + info.m_iShots = 1; + info.m_pAttacker = GetBuilder(); + if ( info.m_pAttacker == NULL ) + { + info.m_pAttacker = this; + } + if ( m_bPlayerControlled ) + { + info.m_vecSpread = VECTOR_CONE_3DEGREES; + } + else + { + info.m_vecSpread = vec3_origin; + } + info.m_flDistance = flDistToTarget + 100; + info.m_iAmmoType = m_iAmmoType; + + if ( IsMiniBuilding() ) + { + info.m_flDamage = tf_sentrygun_mini_damage.GetFloat(); + info.m_flDamageForceScale = 0.0f; + } + else + { + info.m_flDamage = tf_sentrygun_damage.GetFloat(); + } + + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetOwner(), info.m_flDamage, mult_engy_sentry_damage ); + + FireBullets( info ); + + // sentry gun fire 'heats up' the nav mesh around it + UpdateNavMeshCombatStatus(); + + + //NDebugOverlay::Line( vecSrc, vecSrc + vecAimDir * 1000, 255, 0, 0, false, 0.1 ); + + CEffectData data; + data.m_nEntIndex = entindex(); + data.m_nAttachmentIndex = iAttachment; + data.m_fFlags = m_iUpgradeLevel; + data.m_vOrigin = vecSrc; + DispatchEffect( "TF_3rdPersonMuzzleFlash_SentryGun", data ); + + if ( IsMiniBuilding() ) + { + EmitSound_t params; + params.m_pSoundName = "Building_MiniSentrygun.Fire"; + params.m_flSoundTime = 0; + params.m_pflSoundDuration = 0; + params.m_bWarnOnDirectWaveReference = true; + CPASAttenuationFilter filter( this, "Building_MiniSentrygun.Fire" ); + EmitSound( filter, entindex(), params ); + } + else + { + if ( !m_bPlayerControlled ) + { + switch( m_iUpgradeLevel ) + { + case 1: + default: + EmitSentrySound( "Building_Sentrygun.Fire" ); + break; + case 2: + EmitSentrySound( "Building_Sentrygun.Fire2" ); + break; + case 3: + EmitSentrySound( "Building_Sentrygun.Fire3" ); + break; + } + } + else + { + switch ( m_iUpgradeLevel ) + { + case 1: + EmitSentrySound( "Building_Sentrygun.ShaftFire" ); + break; + case 2: + EmitSentrySound( "Building_Sentrygun.ShaftFire2" ); + break; + case 3: + EmitSentrySound( "Building_Sentrygun.ShaftFire3" ); + break; + } + } + } + + if ( !tf_sentrygun_ammocheat.GetBool() && !HasSpawnFlags( SF_SENTRY_INFINITE_AMMO ) ) + { + m_iAmmoShells--; + } + } + else + { + if ( m_iUpgradeLevel > 1 ) + { + if ( !IsPlayingGesture( ACT_RANGE_ATTACK1_LOW ) ) + { + RemoveGesture( ACT_RANGE_ATTACK1 ); + AddGesture( ACT_RANGE_ATTACK1_LOW ); + } + } + + // Out of ammo, play a click + EmitSound( "Building_Sentrygun.Empty" ); + + // Disposable sentries blow up when their ammo runs out + if ( IsDisposableBuilding() ) + { + DetonateObject(); + } + + m_flNextAttack = gpGlobals->curtime + 0.2; + } + + // note when we last fired at en enemy (or tried to) + m_timeSinceLastFired.Start(); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::ModifyFireBulletsDamage( CTakeDamageInfo* dmgInfo ) +{ + if ( m_bPlayerControlled && dmgInfo ) + { + dmgInfo->SetDamageCustom( TF_DMG_CUSTOM_PLAYER_SENTRY ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +float CObjectSentrygun::GetPushMultiplier() +{ + if ( IsMiniBuilding() ) + return 8.f; + else + return 16.f; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::MakeTracer( const Vector &vecTracerSrc, const trace_t &tr, int iTracerType ) +{ + trace_t tmptrace; + tmptrace.endpos = tr.endpos + RandomVector(-10,10); + + // Sentryguns are perfectly accurate, but this doesn't look good for tracers. + // Add a little noise to them, but not enough so that it looks like they're missing. + BaseClass::MakeTracer( vecTracerSrc, tmptrace, iTracerType ); +} + +//----------------------------------------------------------------------------- +// Purpose: MakeTracer asks back for the attachment index +//----------------------------------------------------------------------------- +int CObjectSentrygun::GetTracerAttachment( void ) +{ + return m_iAttachments[SENTRYGUN_ATTACHMENT_MUZZLE]; +} + +//----------------------------------------------------------------------------- +// Rotate and scan for targets +//----------------------------------------------------------------------------- +void CObjectSentrygun::SentryRotate( void ) +{ + if ( GetReversesBuildingConstructionSpeed() ) + { + m_iState.Set( SENTRY_STATE_INACTIVE ); + return; + } + + // if we're playing a fire gesture, stop it + if ( IsPlayingGesture( ACT_RANGE_ATTACK1 ) ) + { + RemoveGesture( ACT_RANGE_ATTACK1 ); + } + + if ( IsPlayingGesture( ACT_RANGE_ATTACK1_LOW ) ) + { + RemoveGesture( ACT_RANGE_ATTACK1_LOW ); + } + + // animate + StudioFrameAdvance(); + + // Look for a target + if ( FindTarget() ) + return; + + // Rotate + if ( !MoveTurret() ) + { + // Change direction + + if ( IsDisabled() || m_nShieldLevel == SHIELD_NORMAL ) + { + EmitSound( "Building_Sentrygun.Disabled" ); + m_vecGoalAngles.x = 30; + } + else + { + switch( m_iUpgradeLevel ) + { + case 1: + default: + EmitSentrySound( "Building_Sentrygun.Idle" ); + break; + case 2: + EmitSound( "Building_Sentrygun.Idle2" ); + break; + case 3: + EmitSound( "Building_Sentrygun.Idle3" ); + break; + } + + // Switch rotation direction + if ( m_bTurningRight ) + { + m_bTurningRight = false; + m_vecGoalAngles.y = m_iLeftBound; + } + else + { + m_bTurningRight = true; + m_vecGoalAngles.y = m_iRightBound; + } + + // Randomly look up and down a bit + if (random->RandomFloat(0, 1) < 0.3) + { + m_vecGoalAngles.x = (int)random->RandomFloat(-10,10); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Add the EMP effect +//----------------------------------------------------------------------------- +void CObjectSentrygun::OnStartDisabled( void ) +{ + // stay at current rotation, angle down + m_vecGoalAngles.x = m_vecCurAngles.x; + m_vecGoalAngles.y = m_vecCurAngles.y; + + // target = nULL + + BaseClass::OnStartDisabled(); +} + +//----------------------------------------------------------------------------- +// Purpose: Remove the EMP effect +//----------------------------------------------------------------------------- +void CObjectSentrygun::OnEndDisabled( void ) +{ + // return to normal rotations + if ( m_bTurningRight ) + { + m_bTurningRight = false; + m_vecGoalAngles.y = m_iLeftBound; + } + else + { + m_bTurningRight = true; + m_vecGoalAngles.y = m_iRightBound; + } + + m_vecGoalAngles.x = 0; + + BaseClass::OnEndDisabled(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CObjectSentrygun::GetBaseTurnRate( void ) +{ + if ( m_bPlayerControlled ) + { + return m_iBaseTurnRate * 100; + } + else + { + return m_iBaseTurnRate; + } +} + +//----------------------------------------------------------------------------- +// +//----------------------------------------------------------------------------- +bool CObjectSentrygun::MoveTurret( void ) +{ + bool bMoved = false; + + int iBaseTurnRate = GetBaseTurnRate(); + + if ( IsMiniBuilding() ) + { + iBaseTurnRate *= 1.35f; + } + + // any x movement? + if ( m_vecCurAngles.x != m_vecGoalAngles.x ) + { + float flDir = m_vecGoalAngles.x > m_vecCurAngles.x ? 1 : -1 ; + + m_vecCurAngles.x += SENTRY_THINK_DELAY * ( iBaseTurnRate * 5 ) * flDir; + + // if we started below the goal, and now we're past, peg to goal + if ( flDir == 1 ) + { + if (m_vecCurAngles.x > m_vecGoalAngles.x) + m_vecCurAngles.x = m_vecGoalAngles.x; + } + else + { + if (m_vecCurAngles.x < m_vecGoalAngles.x) + m_vecCurAngles.x = m_vecGoalAngles.x; + } + + SetPoseParameter( m_iPitchPoseParameter, -m_vecCurAngles.x ); + + bMoved = true; + } + + if ( m_vecCurAngles.y != m_vecGoalAngles.y ) + { + float flDir = m_vecGoalAngles.y > m_vecCurAngles.y ? 1 : -1 ; + float flDist = fabs( m_vecGoalAngles.y - m_vecCurAngles.y ); + bool bReversed = false; + + if ( flDist > 180 ) + { + flDist = 360 - flDist; + flDir = -flDir; + bReversed = true; + } + + if ( m_hEnemy.Get() == NULL ) + { + if ( flDist > 30 ) + { + if ( m_flTurnRate < iBaseTurnRate * 10 ) + { + m_flTurnRate += iBaseTurnRate; + } + } + else + { + // Slow down + if ( m_flTurnRate > (iBaseTurnRate * 5) ) + m_flTurnRate -= iBaseTurnRate; + } + } + else + { + // When tracking enemies, move faster and don't slow + if ( flDist > 30 ) + { + if (m_flTurnRate < iBaseTurnRate * 30) + { + m_flTurnRate += iBaseTurnRate * 3; + } + } + } + + m_vecCurAngles.y += SENTRY_THINK_DELAY * m_flTurnRate * flDir; + + // if we passed over the goal, peg right to it now + if (flDir == -1) + { + if ( (bReversed == false && m_vecGoalAngles.y > m_vecCurAngles.y) || + (bReversed == true && m_vecGoalAngles.y < m_vecCurAngles.y) ) + { + m_vecCurAngles.y = m_vecGoalAngles.y; + } + } + else + { + if ( (bReversed == false && m_vecGoalAngles.y < m_vecCurAngles.y) || + (bReversed == true && m_vecGoalAngles.y > m_vecCurAngles.y) ) + { + m_vecCurAngles.y = m_vecGoalAngles.y; + } + } + + if ( m_vecCurAngles.y < 0 ) + { + m_vecCurAngles.y += 360; + } + else if ( m_vecCurAngles.y >= 360 ) + { + m_vecCurAngles.y -= 360; + } + + if ( flDist < ( SENTRY_THINK_DELAY * 0.5 * iBaseTurnRate ) ) + { + m_vecCurAngles.y = m_vecGoalAngles.y; + } + + QAngle angles = GetAbsAngles(); + + float flYaw = m_vecCurAngles.y - angles.y; + + SetPoseParameter( m_iYawPoseParameter, -flYaw ); + + InvalidatePhysicsRecursive( ANIMATION_CHANGED ); + + bMoved = true; + } + + if ( !bMoved || m_flTurnRate <= 0 ) + { + m_flTurnRate = iBaseTurnRate * 5; + } + + return bMoved; +} + +//----------------------------------------------------------------------------- +// Purpose: Note our last attacked time +//----------------------------------------------------------------------------- +int CObjectSentrygun::OnTakeDamage( const CTakeDamageInfo &info ) +{ + CTakeDamageInfo newInfo = info; + + // As we increase in level, we get more resistant to minigun bullets, to compensate for + // our increased surface area taking more minigun hits. + if ( ( info.GetDamageType() & DMG_BULLET ) && ( info.GetDamageCustom() == TF_DMG_CUSTOM_MINIGUN ) ) + { + float flDamage = newInfo.GetDamage(); + flDamage *= ( 1.0 - m_flHeavyBulletResist ); + newInfo.SetDamage( flDamage ); + } + + // If we are shielded due to player control, we take less damage. + bool bFullyShielded = ( m_nShieldLevel > 0 ) && !HasSapper() && !IsPlasmaDisabled(); + if ( bFullyShielded ) + { + float flDamage = newInfo.GetDamage(); + flDamage *= ( m_nShieldLevel == SHIELD_NORMAL ) ? SHIELD_NORMAL_VALUE : SHIELD_MAX_VALUE; + newInfo.SetDamage( flDamage ); + } + + // Check to see if we are being sapped. + if ( HasSapper() ) + { + // Get the sapper owner. + CBaseObject *pSapper = GetObjectOfTypeOnMe( OBJ_ATTACHMENT_SAPPER ); + + // Take less damage if the owner is causing additional damage. + if ( pSapper && ( info.GetAttacker() == pSapper->GetOwner() ) ) + { + float flDamage = newInfo.GetDamage() * SENTRYGUN_SAPPER_OWNER_DAMAGE_MODIFIER; + newInfo.SetDamage( flDamage ); + } + } + + int iDamageTaken = BaseClass::OnTakeDamage( newInfo ); + + if ( iDamageTaken > 0 ) + { + m_flLastAttackedTime = gpGlobals->curtime; + + // check for achievement + if ( bFullyShielded ) + { + int iPrevLifetimeShieldedDamage = m_iLifetimeShieldedDamage; + m_iLifetimeShieldedDamage += iDamageTaken; + const int kMaxDamageForAchievement = tf_sentrygun_max_absorbed_damage_while_controlled_for_achievement.GetInt(); + if ( iPrevLifetimeShieldedDamage <= kMaxDamageForAchievement && m_iLifetimeShieldedDamage > kMaxDamageForAchievement ) + { + CTFPlayer *pOwner = ToTFPlayer( GetOwner() ); + if ( pOwner && pOwner->IsPlayerClass( TF_CLASS_ENGINEER ) ) + { + pOwner->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_MANUAL_SENTRY_ABSORB_DMG ); + } + } + } + } + + return iDamageTaken; +} + +//----------------------------------------------------------------------------- +// Purpose: Called when this object is destroyed +//----------------------------------------------------------------------------- +void CObjectSentrygun::Killed( const CTakeDamageInfo &info ) +{ + CTFPlayer *pTFKiller = ToTFPlayer( info.GetAttacker() ); + if ( pTFKiller && pTFKiller->IsPlayerClass( TF_CLASS_SOLDIER ) ) + { + if ( pTFKiller->GetAbsOrigin().DistTo( GetAbsOrigin() ) > SENTRY_MAX_RANGE ) + { + pTFKiller->AwardAchievement( ACHIEVEMENT_TF_SOLDIER_DESTROY_SENTRY_OUT_OF_RANGE ); + } + //If we are in the corridor map, then we check for the achievement for it. + else if ( m_hEnemy && !( pTFKiller->GetFlags() & FL_ONGROUND ) ) + { + CBaseEntity *pDamager = GetBuilder(); + + if ( NULL == pDamager ) + { + pDamager = this; + } + + static const float DAMAGE_INTERVAL = 2.0f; + if ( pTFKiller->m_AchievementData.IsDamagerInHistory( pDamager, DAMAGE_INTERVAL ) ) + { + //Check the map. + if ( 0 == Q_stricmp( "tra_sol_corridor", STRING( gpGlobals->mapname ) ) ) + { +#ifdef TF_SOLDIER_TRAINING_ACHIEVEMENTS + //If the attacker was in the air when this sentry died, give him an achievement. + pTFKiller->AwardAchievement( ACHIEVEMENT_TF_SOLDIER_TRAINING_COR_SENTRY_FROM_AIR ); +#endif // TF_SOLDIER_TRAINING_ACHIEVEMENTS + } + } + } + } + + // Tell our owner's shotgun the sentry died. + CTFPlayer *pOwner = ToTFPlayer( GetOwner() ); + if ( pOwner ) + { + CTFShotgun_Revenge* pShotgun = dynamic_cast<CTFShotgun_Revenge*>( pOwner->Weapon_OwnsThisID( TF_WEAPON_SENTRY_REVENGE ) ); + if ( pShotgun ) + { + pShotgun->SentryKilled( GetKills() * 2 + GetAssists() ); + } + } + + // find nearby sentry hint + if ( TFGameRules() && TFGameRules()->IsInTraining() ) + { + CTFBotHintSentrygun *sentryHint; + for( sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( NULL, "bot_hint_sentrygun" ) ); + sentryHint; + sentryHint = static_cast< CTFBotHintSentrygun * >( gEntList.FindEntityByClassname( sentryHint, "bot_hint_sentrygun" ) ) ) + { + if ( sentryHint->IsEnabled() && sentryHint->InSameTeam( this ) ) + { + Vector toMe = GetAbsOrigin() - sentryHint->GetAbsOrigin(); + float dist2 = toMe.LengthSqr(); + if ( dist2 < 1.0f ) + { + sentryHint->OnSentryGunDestroyed( this ); + sentryHint->DecrementUseCount(); + break; + } + } + } + } + + // Engineers destroying their own sentry don't escape the buster. + // Destroying disposable sentries doesn't reset the buster. + if ( info.GetAttacker() != this && !IsDisposableBuilding() ) + { + // Sentry Buster mission accomplished + if ( pOwner ) + { + pOwner->ResetAccumulatedSentryGunDamageDealt(); + pOwner->ResetAccumulatedSentryGunKillCount(); + } + } + + // do normal handling + BaseClass::Killed( info ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::SetModel( const char *pModel ) +{ + float flPoseParam0 = 0.0; + float flPoseParam1 = 0.0; + + // Save pose parameters across model change + if ( m_iPitchPoseParameter >= 0 ) + { + flPoseParam0 = GetPoseParameter( m_iPitchPoseParameter ); + } + + if ( m_iYawPoseParameter >= 0 ) + { + flPoseParam1 = GetPoseParameter( m_iYawPoseParameter ); + } + + BaseClass::SetModel( pModel ); + + // Reset this after model change + SetBuildingSize(); + SetSolid( SOLID_BBOX ); + + // Restore pose parameters + m_iPitchPoseParameter = LookupPoseParameter( "aim_pitch" ); + m_iYawPoseParameter = LookupPoseParameter( "aim_yaw" ); + + SetPoseParameter( m_iPitchPoseParameter, flPoseParam0 ); + SetPoseParameter( m_iYawPoseParameter, flPoseParam1 ); + + CreateBuildPoints(); + + ReattachChildren(); + + ResetSequenceInfo(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::SetBuildingSize() +{ + // Mini's do NOT need to have their size set here, SetModelScale already handles scaling for hulls (change from MvM) + UTIL_SetSize( this, SENTRYGUN_MINS, SENTRYGUN_MAXS ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::MakeCarriedObject( CTFPlayer *pCarrier ) +{ + BaseClass::MakeCarriedObject( pCarrier ); + + m_iOldAmmoShells = m_iAmmoShells; + m_iOldAmmoRockets = m_iAmmoRockets; + + m_nShieldLevel.Set( SHIELD_NONE ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::MakeDisposableBuilding( CTFPlayer* pPlayer ) +{ + // We don't have our main gun + if ( !( pPlayer->GetNumObjects( OBJ_SENTRYGUN ) && pPlayer->CanBuild( OBJ_SENTRYGUN ) == CB_CAN_BUILD ) ) + return; + + // We're carrying our main gun + if ( pPlayer->m_Shared.IsCarryingObject() && pPlayer->m_Shared.GetCarriedObject() && !pPlayer->m_Shared.GetCarriedObject()->IsDisposableBuilding() ) + return; + + if ( IsDisposableBuilding() ) + return; + + SetMaxHealth( SENTRYGUN_MINI_MAX_HEALTH ); + SetHealth( SENTRYGUN_MINI_MAX_HEALTH ); + + SetModelScale( DISPOSABLE_SCALE ); + + BaseClass::MakeDisposableBuilding( pPlayer ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::RemoveAllAmmo() +{ + m_iOldAmmoShells = m_iAmmoShells; + m_iOldAmmoRockets = m_iAmmoRockets; + + m_iAmmoShells = 0; + m_iAmmoRockets = 0; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::EmitSentrySound( IRecipientFilter& filter, int iEntIndex, const char *soundname ) +{ + EmitSound_t params; + params.m_pSoundName = soundname; + params.m_flSoundTime = 0; + params.m_pflSoundDuration = 0; + params.m_bWarnOnDirectWaveReference = true; + + if ( IsMiniBuilding() ) + { + StopSound( soundname ); + params.m_nPitch = PITCH_HIGH; + params.m_nFlags = SND_CHANGE_PITCH; + } + + EmitSound( filter, entindex(), params ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::EmitSentrySound( const char* soundname ) +{ + CPASAttenuationFilter filter( this, soundname ); + + EmitSound_t params; + params.m_pSoundName = soundname; + params.m_flSoundTime = 0; + params.m_pflSoundDuration = 0; + params.m_bWarnOnDirectWaveReference = true; + + if ( IsMiniBuilding() || m_flFireRate != 1.f ) + { + StopSound( soundname ); + params.m_nPitch = IsMiniBuilding() ? PITCH_HIGH : RemapValClamped( m_flFireRate, 1.0f, 0.5f, 100.f, 120.f ); + params.m_nFlags = SND_CHANGE_PITCH; + } + + EmitSound( filter, entindex(), params ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CTFPlayer *CObjectSentrygun::GetAssistingTeammate( float maxAssistDuration ) const +{ + if ( maxAssistDuration > 0.0f && ( !m_lastTeammateWrenchHitTimer.HasStarted() || m_lastTeammateWrenchHitTimer.IsGreaterThen( maxAssistDuration ) ) ) + return NULL; + + return m_lastTeammateWrenchHit; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CObjectSentrygun::SetAutoAimTarget( CTFPlayer* pPlayer ) +{ + if ( !pPlayer ) + return; + + // No auto aim target if a dummy is found + CBaseEntity *pTargetOld = m_hEnemy.Get(); + if ( pTargetOld ) + { + CTFTargetDummy *pDummy = dynamic_cast<CTFTargetDummy*>( pTargetOld ); + if ( pDummy ) + { + m_hAutoAimTarget = NULL; + return; + } + } + + m_hAutoAimTarget = pPlayer; + m_flAutoAimStartTime = gpGlobals->curtime; +} + + +//----------------------------------------------------------------------------- +void CObjectSentrygun::UpdateNavMeshCombatStatus( void ) +{ + // mark region as 'in combat' + if ( m_inCombatThrottleTimer.IsElapsed() ) + { + // important to keep this at one second, so rate cvars make sense (units/sec) + m_inCombatThrottleTimer.Start( 1.0f ); + + UpdateLastKnownArea(); + + // only search up/down StepHeight as a cheap substitute for line of sight + CUtlVector< CNavArea * > nearbyAreaVector; + CollectSurroundingAreas( &nearbyAreaVector, GetLastKnownArea(), tf_nav_in_combat_range.GetFloat(), StepHeight, StepHeight ); + + for( int i=0; i<nearbyAreaVector.Count(); ++i ) + { + CTFNavArea *area = static_cast< CTFNavArea * >( nearbyAreaVector[i] ); + + // hacky - we want sentry gunfire to immediately heat the area since it is so dangerous + area->OnCombat(); + area->OnCombat(); + area->OnCombat(); + area->OnCombat(); + area->OnCombat(); + } + } +} +//------------------------------------------------------------------------------------------------------------------------------- +int CObjectSentrygun::GetUpgradeMetalRequired() +{ + int iMetal = BaseClass::GetUpgradeMetalRequired(); + int iSmallSentry = 0; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetOwner(), iSmallSentry, build_small_sentries ); + if ( iSmallSentry ) + { + iMetal *= 0.75f; + } + + return iMetal; +} + +//------------------------------------------------------------------------------------------------------------------------------- +int CObjectSentrygun::GetMaxHealthForCurrentLevel( void ) +{ + int iHealth = BaseClass::GetMaxHealthForCurrentLevel(); + if ( IsScaledSentry() ) + { + iHealth *= 0.66f; + } + return iHealth; +} +//------------------------------------------------------------------------------------------------------------------------------- +void CObjectSentrygun::MakeScaledBuilding( CTFPlayer *pPlayer ) +{ + int iSmallSentry = 0; + CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetOwner(), iSmallSentry, build_small_sentries ); + if ( iSmallSentry ) + { + m_flScaledSentry = iSmallSentry ? SMALL_SENTRY_SCALE : 1.0f; + + SetModelScale( m_flScaledSentry ); + + int iHealth = GetMaxHealthForCurrentLevel(); + + SetMaxHealth( iHealth ); + SetHealth( iHealth ); + SetBuildingSize(); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( tf_projectile_sentryrocket, CTFProjectile_SentryRocket ); + +IMPLEMENT_NETWORKCLASS_ALIASED( TFProjectile_SentryRocket, DT_TFProjectile_SentryRocket ) + +BEGIN_NETWORK_TABLE( CTFProjectile_SentryRocket, DT_TFProjectile_SentryRocket ) +END_NETWORK_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: Creation +//----------------------------------------------------------------------------- +CTFProjectile_SentryRocket *CTFProjectile_SentryRocket::Create( const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity *pOwner, CBaseEntity *pScorer ) +{ + CTFProjectile_SentryRocket *pRocket = static_cast<CTFProjectile_SentryRocket*>( CTFBaseRocket::Create( NULL, "tf_projectile_sentryrocket", vecOrigin, vecAngles, pOwner ) ); + + if ( pRocket ) + { + pRocket->SetScorer( pScorer ); + } + + return pRocket; +} + +CTFProjectile_SentryRocket::CTFProjectile_SentryRocket() +{ + UseClientSideAnimation(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTFProjectile_SentryRocket::Spawn() +{ + BaseClass::Spawn(); + + SetModel( SENTRY_ROCKET_MODEL ); + + UTIL_SetSize( this, vec3_origin, vec3_origin ); + + ResetSequence( LookupSequence("idle") ); +} + +#ifdef STAGING_ONLY +//----------------------------------------------------------------------------- +// Purpose: Directly create a sentry gun at the precise position and orientation desired +//----------------------------------------------------------------------------- +void CC_SentrygunSpawn( const CCommand& args ) +{ + if ( !UTIL_IsCommandIssuedByServerAdmin() ) + return; + + CObjectSentrygun *sentry = (CObjectSentrygun *)CreateEntityByName( "obj_sentrygun" ); + if ( sentry ) + { + CBasePlayer* pPlayer = UTIL_GetCommandClient(); + trace_t tr; + Vector forward; + pPlayer->EyeVectors( &forward ); + UTIL_TraceLine( pPlayer->EyePosition(), + pPlayer->EyePosition() + forward * MAX_TRACE_LENGTH,MASK_SOLID, + pPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction != 1.0 ) + { + sentry->SetAbsOrigin( tr.endpos ); + QAngle angles = pPlayer->BodyAngles(); + angles.x = 0.0f; + angles.z = 0.0f; + sentry->SetAbsAngles( angles ); + } + + int iSentryLevel = 2; + int iTeamNum = pPlayer->GetTeamNumber(); + + if ( args.ArgC() > 1 ) + { + int i = atoi(args[1]); + if ( abs(i) >= 1 && abs(i) <= 3) + { + iSentryLevel = abs(i)-1; + } + + if ( i < 0) + { + iTeamNum = GetEnemyTeam( iTeamNum ); + } + } + + sentry->m_nDefaultUpgradeLevel = iSentryLevel; + + sentry->Spawn(); + sentry->ChangeTeam( iTeamNum ); + + sentry->InitializeMapPlacedObject(); + } +} +static ConCommand sentrygun_spawn( "sentrygun_spawn", CC_SentrygunSpawn, "Spawns a Sentrygun where the player is looking. Takes a parameter for level of sentry [1-3: default 3]. If the passed sentry level < 0, an enemy sentry is spawned.", FCVAR_GAMEDLL | FCVAR_CHEAT ); + +#endif // STAGING_ONLY
\ No newline at end of file |