//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Slowly damages the object it's attached to // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "tf_player.h" #include "tf_team.h" #include "tf_gamerules.h" #include "tf_obj.h" #include "tf_obj_sentrygun.h" #include "tf_obj_sapper.h" #include "ndebugoverlay.h" #include "tf_gamestats.h" #include "tf_obj_teleporter.h" #include "tf_weapon_builder.h" #include "tf_fx.h" #include "bot/tf_bot.h" ConVar tf_mvm_notice_sapped_squadmates_delay( "tf_mvm_notice_sapped_squadmates_delay", "1", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY, "How long it takes for a squad leader to notice his squadmate was sapped" ); // ------------------------------------------------------------------------ // #define SAPPER_MINS Vector(0, 0, 0) #define SAPPER_MAXS Vector(1, 1, 1) const char * g_sapperModel = "models/buildables/sapper_placed.mdl"; const char * g_sapperPlacementModel = "models/buildables/sapper_placement.mdl"; BEGIN_DATADESC( CObjectSapper ) DEFINE_THINKFUNC( SapperThink ), END_DATADESC() IMPLEMENT_SERVERCLASS_ST(CObjectSapper, DT_ObjectSapper) END_SEND_TABLE(); LINK_ENTITY_TO_CLASS(obj_attachment_sapper, CObjectSapper); PRECACHE_REGISTER(obj_attachment_sapper); ConVar obj_sapper_amount( "obj_sapper_amount", "25", FCVAR_NONE, "Amount of health inflicted by a Sapper object per second" ); #define SAPPER_THINK_CONTEXT "SapperThink" #define SAPPER_REMOVE_DISABLE_TIME 0.5f //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CObjectSapper::CObjectSapper() { m_szPlacementModel[ 0 ] = '\0'; m_szSapperModel[ 0 ] = '\0'; szSapperSound[ 0 ] = '\0'; m_iHealth = GetBaseHealth(); SetMaxHealth( m_iHealth ); m_flSelfDestructTime = 0; UseClientSideAnimation(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CObjectSapper::UpdateOnRemove() { StopSound( "Weapon_Sapper.Timer" ); StopSound( "Weapon_sd_sapper.Timer" ); StopSound( "Weapon_p2rec.Timer" ); #ifdef STAGING_ONLY StopSound( "WeaponDynamiteSapper.TickTock" ); StopSound( "WeaponDynamiteSapper.BellRing" ); #endif if( GetBuilder() ) { GetBuilder()->OnSapperFinished( m_flSapperStartTime ); } BaseClass::UpdateOnRemove(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CObjectSapper::Spawn() { SetModel( GetSapperModelName( SAPPER_MODEL_PLACEMENT ) ); m_takedamage = DAMAGE_YES; m_iHealth = GetBaseHealth(); SetType( OBJ_ATTACHMENT_SAPPER ); BaseClass::Spawn(); Vector mins = SAPPER_MINS; Vector maxs = SAPPER_MAXS; CollisionProp()->SetSurroundingBoundsType( USE_SPECIFIED_BOUNDS, &mins, &maxs ); int nFlags = m_fObjectFlags | OF_ALLOW_REPEAT_PLACEMENT; // Don't allow repeat placement as a human spy in MvM if ( TFGameRules() && TFGameRules()->GameModeUsesMiniBosses() && GetBuilder() && !GetBuilder()->IsBot() ) { nFlags &= ~( OF_ALLOW_REPEAT_PLACEMENT ); } m_fObjectFlags.Set( nFlags ); SetSolid( SOLID_NONE ); #ifdef STAGING_ONLY m_bIsRinging = false; #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CObjectSapper::Precache() { Precache( "c_sapper.mdl" ); // Precache the placed and placement models for the sappers #ifdef STAGING_ONLY Precache( "c_sd_sapper.mdl" ); #else Precache( "w_sd_sapper.mdl" ); #endif Precache( "c_p2rec.mdl" ); Precache( "c_sapper_xmas.mdl" ); Precache( "c_breadmonster_sapper.mdl" ); PrecacheScriptSound( "Weapon_Sapper.Plant" ); PrecacheScriptSound( "Weapon_Sapper.Timer" ); PrecacheScriptSound( "Weapon_sd_sapper.Timer" ); PrecacheScriptSound( "Weapon_p2rec.Timer" ); #ifdef STAGING_ONLY PrecacheScriptSound( "WeaponDynamiteSapper.TickTock" ); PrecacheScriptSound( "WeaponDynamiteSapper.BellRing" ); #endif // Precache the Wheatley Sapper sounds PrecacheScriptSound( "PSap.null" ); PrecacheScriptSound( "Psap.Attached" ); PrecacheScriptSound( "Psap.AttachedPW" ); PrecacheScriptSound( "PSap.Damage" ); PrecacheScriptSound( "PSap.Death" ); PrecacheScriptSound( "PSap.DeathLong" ); PrecacheScriptSound( "PSap.Deploy" ); PrecacheScriptSound( "PSap.DeployAgain" ); PrecacheScriptSound( "PSap.DeployIntro" ); PrecacheScriptSound( "PSap.Hacked" ); PrecacheScriptSound( "Psap.HackedFollowup" ); PrecacheScriptSound( "Psap.HackedLoud" ); PrecacheScriptSound( "PSap.Hacking" ); PrecacheScriptSound( "PSap.HackingPW" ); PrecacheScriptSound( "PSap.HackingShort" ); PrecacheScriptSound( "PSap.Holster" ); PrecacheScriptSound( "PSap.HolsterFast" ); PrecacheScriptSound( "Psap.Idle" ); PrecacheScriptSound( "Psap.IdleHack02" ); PrecacheScriptSound( "Psap.IdleHarmless02" ); PrecacheScriptSound( "PSap.IdleIntro01" ); PrecacheScriptSound( "PSap.IdleIntro02" ); PrecacheScriptSound( "PSap.IdleIntro03" ); PrecacheScriptSound( "PSap.IdleIntro04" ); PrecacheScriptSound( "PSap.IdleKnife02" ); PrecacheScriptSound( "PSap.IdleKnife03" ); PrecacheScriptSound( "PSap.Sneak" ); BaseClass::Precache(); } void CObjectSapper::Precache( const char *pchBaseModel ) { m_szPlacementModel[ 0 ] = '\0'; m_szSapperModel[ 0 ] = '\0'; int iModelIndex; iModelIndex = PrecacheModel( GetSapperModelName( SAPPER_MODEL_PLACED, pchBaseModel ) ); PrecacheGibsForModel( iModelIndex ); PrecacheModel( GetSapperModelName( SAPPER_MODEL_PLACEMENT, pchBaseModel ) ); m_szPlacementModel[ 0 ] = '\0'; m_szSapperModel[ 0 ] = '\0'; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CObjectSapper::FinishedBuilding( void ) { BaseClass::FinishedBuilding(); CBaseEntity *pEntity = m_hBuiltOnEntity.Get(); if ( pEntity ) { if ( GetParentObject() ) { GetParentObject()->OnAddSapper(); CBaseObject *pObject = dynamic_cast( m_hBuiltOnEntity.Get() ); if ( pObject ) { if ( GetBuilder() && pObject->GetBuilder() ) { IGameEvent * event = gameeventmanager->CreateEvent( "player_sapped_object" ); if ( event ) { event->SetInt( "userid", GetBuilder()->GetUserID() ); event->SetInt( "ownerid", pObject->GetBuilder()->GetUserID() ); event->SetInt( "object", pObject->ObjectType() ); event->SetInt( "sapperid", entindex() ); gameeventmanager->FireEvent( event ); } } } } } if( GetBuilder() ) { m_flSapperStartTime = gpGlobals->curtime; GetBuilder()->OnSapperStarted( m_flSapperStartTime ); } EmitSound( "Weapon_Sapper.Plant" ); EmitSound( GetSapperSoundName() ); // start looping "Weapon_Sapper.Timer", killed when we die m_flSapperDamageAccumulator = 0; m_flLastThinkTime = gpGlobals->curtime; m_flLastHealthLeachTime = gpGlobals->curtime; SetContextThink( &CObjectSapper::SapperThink, gpGlobals->curtime + 0.1, SAPPER_THINK_CONTEXT ); } //----------------------------------------------------------------------------- // Purpose: Change our model based on the object we are attaching to //----------------------------------------------------------------------------- void CObjectSapper::SetupAttachedVersion( void ) { if ( !IsParentValid() ) return; if ( IsPlacing() ) { CBaseEntity *pEntity = m_hBuiltOnEntity.Get(); if ( pEntity ) { SetModel( GetSapperModelName( SAPPER_MODEL_PLACEMENT ) ); } } BaseClass::SetupAttachedVersion(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CObjectSapper::OnGoActive( void ) { if ( !IsParentValid() ) return; // set new model CBaseEntity *pEntity = m_hBuiltOnEntity.Get(); m_flSelfDestructTime = 0; CTFPlayer *pBuilder = ToTFPlayer( GetBuilder() ); if ( pEntity ) { SetModel( GetSapperModelName( SAPPER_MODEL_PLACED ) ); if ( pEntity->IsPlayer() ) // Sapped bot in MvM mode, or player in bountymode { float flTime = 4.f; if ( pBuilder ) { int iRoboSapper = 0; CALL_ATTRIB_HOOK_INT_ON_OTHER( pBuilder, iRoboSapper, robo_sapper ); CTFPlayer *pTFParent = ToTFPlayer( GetParentEntity() ); if ( pTFParent && pTFParent->IsAlive() ) { int nRadius = 200; switch( iRoboSapper ) { case 2: flTime = 5.5f; nRadius = 225; break; case 3: flTime = 7.f; nRadius = 250; break; default: break; } // Unlimited, single-target version of the RoboSapper if ( GetObjectMode() == MODE_SAPPER_ANTI_ROBOT ) { nRadius = 0; } ApplyRoboSapper( pTFParent, flTime, nRadius ); } } m_flSelfDestructTime = gpGlobals->curtime + flTime; } #ifdef STAGING_ONLY //if ( pBuilder ) //{ // float flExplodeOnTimer = 0; // CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pBuilder, flExplodeOnTimer, sapper_explodes_on_timer ) // { // if ( flExplodeOnTimer != 0 ) // { // // timer is based on health of the object // // Sappers normally do 25dps // //float flTimer = pEntity->GetMaxHealth() * 0.04f; // //m_flSelfDestructTime = gpGlobals->curtime + flExplodeOnTimer; // } // } //} #endif } UTIL_SetSize( this, SAPPER_MINS, SAPPER_MAXS ); SetSolid( SOLID_NONE ); BaseClass::OnGoActive(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CObjectSapper::IsParentValid( void ) { bool bValid = false; CBaseEntity *pEntity = m_hBuiltOnEntity.Get(); if ( pEntity ) { if ( pEntity->IsPlayer() ) // sapped bot in MvM mode { bValid = true; } else { CBaseObject *pObject = dynamic_cast( pEntity ); if ( pObject ) { bValid = true; } } } if ( !bValid ) { DestroyObject(); } return bValid; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CObjectSapper::DetachObjectFromObject( void ) { CBaseObject *pParent = GetParentObject(); if ( pParent ) { pParent->OnRemoveSapper(); #ifdef STAGING_ONLY CTFPlayer *pBuilder = GetBuilder(); if ( pBuilder && pParent->GetHealth() < 0 ) { // Attr on Det float flExplodeOnTimer = 0; CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pBuilder, flExplodeOnTimer, sapper_explodes_on_det ); if ( flExplodeOnTimer ) { float flDamage = pParent->GetMaxHealth() * 1.5; Vector vecOrigin = GetAbsOrigin(); // Use the building as the det position CTakeDamageInfo detInfo; detInfo.SetDamage( flDamage ); detInfo.SetAttacker( this ); detInfo.SetInflictor( this ); detInfo.SetDamageType( DMG_BLAST ); // Generate Large Radius Damage float flRadius = 200.0f; CTFRadiusDamageInfo radiusinfo( &detInfo, vecOrigin, flRadius, NULL, flRadius ); TFGameRules()->RadiusDamage( radiusinfo ); DispatchParticleEffect( "explosionTrail_seeds_mvm", vecOrigin, GetAbsAngles() ); } } #endif } BaseClass::DetachObjectFromObject(); } //----------------------------------------------------------------------------- const char* CObjectSapper::GetSapperModelName( SapperModel_t nModel, const char *pchModelName /*= NULL */) { // Check to see if we have model names generated, if not we must generate if ( m_szPlacementModel[0] == '\0' || m_szSapperModel[0] == '\0' ) { if ( !pchModelName ) { if ( GetBuilder() ) { CTFWeaponBuilder *pWeapon = dynamic_cast< CTFWeaponBuilder* >( GetBuilder()->Weapon_GetWeaponByType( TF_WPN_TYPE_BUILDING ) ); if ( pWeapon ) { pchModelName = pWeapon->GetWorldModel(); } } } if ( !pchModelName ) { if ( nModel >= SAPPER_MODEL_PLACEMENT ) return g_sapperPlacementModel; return g_sapperModel; } // Generate Models // Name base char szModelName[ _MAX_PATH ]; V_FileBase( pchModelName, szModelName, sizeof( szModelName ) ); pchModelName = szModelName + 2; #ifdef STAGING_ONLY if (!V_strcmp(pchModelName, "sd_sapper")) { V_snprintf(m_szPlacementModel, sizeof(m_szPlacementModel), "models/workshop_partner/buildables/%s%s", pchModelName, "_placement.mdl"); V_snprintf(m_szSapperModel, sizeof(m_szSapperModel), "models/workshop_partner/buildables/%s%s", pchModelName, "_placed.mdl"); } else #endif { V_snprintf(m_szPlacementModel, sizeof(m_szPlacementModel), "models/buildables/%s%s", pchModelName, "_placement.mdl"); V_snprintf(m_szSapperModel, sizeof(m_szSapperModel), "models/buildables/%s%s", pchModelName, "_placed.mdl"); } } if ( nModel >= SAPPER_MODEL_PLACEMENT ) { return m_szPlacementModel; } return m_szSapperModel; } //----------------------------------------------------------------------------- const char* CObjectSapper::GetSapperSoundName( void ) { if ( szSapperSound[ 0 ] == '\0' ) { const char *pchModelName = NULL; if ( GetBuilder() ) { CTFWeaponBuilder *pWeapon = dynamic_cast< CTFWeaponBuilder* >( GetBuilder()->Weapon_GetWeaponByType( TF_WPN_TYPE_BUILDING ) ); if ( pWeapon ) { pchModelName = pWeapon->GetWorldModel(); } } #ifdef STAGING_ONLY // // Attr on Det float flExplodeOnTimer = 0; CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetBuilder(), flExplodeOnTimer, sapper_explodes_on_det ); if ( flExplodeOnTimer ) { EmitSound( "Weapon_Sapper.Timer" ); return "WeaponDynamiteSapper.TickTock"; } #endif if ( !pchModelName ) { return "Weapon_Sapper.Timer"; } char szModelName[ _MAX_PATH ]; V_FileBase( pchModelName, szModelName, sizeof( szModelName ) ); pchModelName = szModelName + 2; V_snprintf( szSapperSound, sizeof( szSapperSound ), "Weapon_%s.Timer", pchModelName ); } return szSapperSound; } //----------------------------------------------------------------------------- // Purpose: Slowly destroy the object I'm attached to //----------------------------------------------------------------------------- void CObjectSapper::SapperThink( void ) { if ( !GetTeam() ) return; bool bThink = true; CBaseEntity *pEntity = m_hBuiltOnEntity.Get(); if ( pEntity ) { if ( pEntity->IsPlayer() ) // sapping bots in MvM mode { bool bDestroy = false; CTFPlayer *pTFOwner = ToTFPlayer( m_hBuiltOnEntity.Get() ); CTFPlayer *pBuilder = GetBuilder(); if ( !pBuilder || !pTFOwner || ( pTFOwner && !pTFOwner->IsAlive() ) ) { bDestroy = true; } #ifdef STAGING_ONLY /*if ( gpGlobals->curtime >= m_flSelfDestructTime ) { bDestroy = true; Explode(); }*/ #else if ( gpGlobals->curtime >= m_flSelfDestructTime ) { bDestroy = true; Explode(); } #endif if ( bDestroy ) { DestroyObject(); bThink = false; return; } } else { CBaseObject *pObject = GetParentObject(); if ( !pObject ) { DestroyObject(); bThink = false; return; } // Don't bring objects back from the dead if ( !pObject->IsAlive() || pObject->IsDying() ) return; CTFPlayer *pBuilder = GetBuilder(); // how much damage to give this think? float flTimeSinceLastThink = gpGlobals->curtime - m_flLastThinkTime; float flDamageToGive = ( flTimeSinceLastThink ) * obj_sapper_amount.GetFloat(); CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pBuilder, flDamageToGive, mult_sapper_damage ); // add to accumulator m_flSapperDamageAccumulator += flDamageToGive; int iDamage = (int)m_flSapperDamageAccumulator; m_flSapperDamageAccumulator -= iDamage; // sapper building damage added to health of Vampire Powerup carrier if ( TFGameRules() && TFGameRules()->IsPowerupMode() ) { CTFPlayer *pTFOwner = ToTFPlayer( GetOwner() ); if ( pTFOwner && pTFOwner->m_Shared.GetCarryingRuneType() == RUNE_VAMPIRE ) { pTFOwner->TakeHealth( flDamageToGive, DMG_GENERIC ); } } int iCustomDamage = 0; if ( GetReversesBuildingConstructionSpeed() != 0.0f ) { iCustomDamage = TF_DMG_CUSTOM_SAPPER_RECORDER_DEATH; } CTakeDamageInfo info; info.SetDamage( iDamage ); info.SetAttacker( this ); info.SetInflictor( this ); info.SetDamageType( DMG_CRUSH ); info.SetDamageCustom( iCustomDamage ); pObject->TakeDamage( info ); if ( gpGlobals->curtime - m_flLastHealthLeachTime > 1.0f ) { m_flLastHealthLeachTime = gpGlobals->curtime; float flHealOwnerPerSecond = 0.0f; CALL_ATTRIB_HOOK_INT_ON_OTHER( pBuilder, flHealOwnerPerSecond, sapper_damage_leaches_health ); if ( flHealOwnerPerSecond ) { CTFPlayer *pSpyOwner = GetOwner(); if ( pSpyOwner && pSpyOwner->IsAlive() ) { pSpyOwner->TakeHealth( flHealOwnerPerSecond, DMG_IGNORE_MAXHEALTH ); pSpyOwner->m_Shared.HealthKitPickupEffects( flHealOwnerPerSecond ); } } } #ifdef STAGING_ONLY if ( !m_bIsRinging && pObject->GetHealth() < 60.0f ) { int iDetonate = 0; CALL_ATTRIB_HOOK_INT_ON_OTHER( pBuilder, iDetonate, sapper_explodes_on_det ); if ( iDetonate ) { EmitSound( "WeaponDynamiteSapper.BellRing" ); m_bIsRinging = true; } } //float flExplodeOnTimer = 0; //CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( pBuilder, flExplodeOnTimer, sapper_explodes_on_det ); ////if ( flExplodeOnTimer != 0 && m_flSelfDestructTime < gpGlobals->curtime ) //if ( flExplodeOnTimer ) //{ // float flDamage = pObject->GetMaxHealth() * 1.5; // Explode(); // DestroyObject(); // Vector vecOrigin = GetAbsOrigin(); // // Use the building as the det position // CTakeDamageInfo detInfo; // detInfo.SetDamage( flDamage ); // detInfo.SetAttacker( this ); // detInfo.SetInflictor( this ); // detInfo.SetDamageType( DMG_BLAST ); // // Destroy the building by doubly applying damage // pObject->TakeDamage( detInfo ); // // Generate Large Radius Damage // float flRadius = 200.0f; // same as pipebomb launcher // CTFRadiusDamageInfo radiusinfo( &detInfo, vecOrigin, flRadius, NULL, flRadius ); // TFGameRules()->RadiusDamage( radiusinfo ); // DispatchParticleEffect( "explosionTrail_seeds_mvm", vecOrigin, GetAbsAngles() ); //} #endif } } if ( bThink ) { SetNextThink( gpGlobals->curtime + 0.1f, SAPPER_THINK_CONTEXT ); } m_flLastThinkTime = gpGlobals->curtime; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CObjectSapper::OnTakeDamage( const CTakeDamageInfo &info ) { if ( info.GetDamageCustom() != TF_DMG_WRENCH_FIX ) { // See if the weapon has a "I damage sappers" attribute on it int iDmgSappers = 0; CTFWeaponBase *pWeapon = dynamic_cast(info.GetWeapon()); if ( pWeapon ) { CALL_ATTRIB_HOOK_INT_ON_OTHER( pWeapon, iDmgSappers, set_dmg_apply_to_sapper ); } if ( iDmgSappers == 0 ) return 0; } // Is the damage from something other than another sapper? (which might be on our matching teleporter) if ( !( info.GetDamageType() & DMG_FROM_OTHER_SAPPER ) ) { if ( GetParentObject() ) { CTakeDamageInfo localDamageInfo = info; localDamageInfo.AddDamageType( DMG_FROM_OTHER_SAPPER ); // If there's a matching teleporter with a sapper then have that sapper take damage, too. CObjectTeleporter *pParentTeleporter = dynamic_cast< CObjectTeleporter * >( GetParentObject() ); if ( pParentTeleporter ) { // GetMatchingTeleporter is set when a matching teleporter is ACTIVE // if we don't find the cache matching teleporter, try to find with a more expensive FindMatch func CObjectTeleporter *pMatchingTeleporter = pParentTeleporter->GetMatchingTeleporter() ? pParentTeleporter->GetMatchingTeleporter() : pParentTeleporter->FindMatch(); if ( pMatchingTeleporter && pMatchingTeleporter->HasSapper() ) { // Do damage to any attached buildings IHasBuildPoints *pBPInterface = dynamic_cast< IHasBuildPoints * >( pMatchingTeleporter ); int iNumObjects = pBPInterface->GetNumObjectsOnMe(); for ( int iPoint = 0 ; iPoint < iNumObjects ; iPoint++ ) { CBaseObject *pObject = pMatchingTeleporter->GetBuildPointObject( iPoint ); if ( pObject && pObject->IsHostileUpgrade() ) { pObject->TakeDamage( localDamageInfo ); } } } } } } return BaseClass::OnTakeDamage( info ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CObjectSapper::Killed( const CTakeDamageInfo &info ) { CBaseEntity *pInflictor = info.GetInflictor(); CBaseEntity *pKiller = info.GetAttacker(); CTFPlayer *pScorer = ToTFPlayer( TFGameRules()->GetDeathScorer( pKiller, pInflictor, this ) ); // We don't own the building we removed the sapper from if ( pScorer && GetParentObject() && GetParentObject()->GetOwner() != pScorer ) { // Give a bonus point for it if ( TFGameRules()->GameModeUsesUpgrades() ) { CTF_GameStats.Event_PlayerAwardBonusPoints( pScorer, this, 10 ); } if ( pScorer->IsPlayerClass( TF_CLASS_ENGINEER ) ) { pScorer->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_DESTROY_SAPPERS, 1 ); } } // Optional: if a weapon was used to destroy this sapper, we give the weapon an opportunity // to adjust its stats. { CTFWeaponBase *pWeapon = dynamic_cast( info.GetWeapon() ); if ( pWeapon ) { EconEntity_OnOwnerKillEaterEvent( dynamic_cast( info.GetWeapon() ), // econ entity pWeapon->GetTFPlayerOwner(), // scorer GetOwner(), // victim kKillEaterEvent_SapperDestroyed ); } } CBaseObject *pParent = GetParentObject(); if ( pParent ) { pParent->SetPlasmaDisabled( SAPPER_REMOVE_DISABLE_TIME ); } BaseClass::Killed( info ); } int CObjectSapper::GetBaseHealth( void ) { float flSapperHealth = SAPPER_MAX_HEALTH; CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetBuilder(), flSapperHealth, mult_sapper_health ); return flSapperHealth; } //----------------------------------------------------------------------------- // Purpose: Search for players to apply RoboSapper effects to //----------------------------------------------------------------------------- void CObjectSapper::ApplyRoboSapper( CTFPlayer *pTarget, float flDuration, int nRadius /*= 200*/ ) { // Apply effects to primary target if ( IsValidRoboSapperTarget( pTarget ) ) { ApplyRoboSapperEffects( pTarget, flDuration ); } // If we have a radius, search it for valid targets if ( nRadius ) { int iCount = 0; for ( int i = 1; i < gpGlobals->maxClients; i++ ) { CTFPlayer *pPlayer = ToTFPlayer( UTIL_PlayerByIndex( i ) ); if ( !pPlayer ) continue; // Ignore the primary target (handled above) if ( pPlayer == pTarget ) continue; // Same team, alive, etc if ( !IsValidRoboSapperTarget( pPlayer ) ) continue; // Range check from pTarget Vector vecDist = pPlayer->GetAbsOrigin() - GetAbsOrigin(); if ( vecDist.LengthSqr() > nRadius * nRadius ) continue; // Ignore bots we can't see trace_t trace; UTIL_TraceLine( pPlayer->WorldSpaceCenter(), WorldSpaceCenter(), MASK_SOLID_BRUSHONLY, NULL, COLLISION_GROUP_NONE, &trace ); if ( trace.fraction < 1.0f ) continue; // Apply if ( ApplyRoboSapperEffects( pPlayer, flDuration ) ) iCount++; } // ACHIEVEMENT_TF_MVM_SPY_SAP_ROBOTS if ( iCount >= 10 ) { CTFPlayer *pBuilder = ToTFPlayer( GetBuilder() ); if ( pBuilder && TFGameRules() && TFGameRules()->IsMannVsMachineMode() ) { pBuilder->AwardAchievement( ACHIEVEMENT_TF_MVM_SPY_SAP_ROBOTS ); } } Vector vecOrigin = GetAbsOrigin(); CPVSFilter filter( vecOrigin ); TE_TFParticleEffect( filter, 0.f, "Explosion_ShockWave_01", vecOrigin, vec3_angle ); } } //----------------------------------------------------------------------------- // Purpose: Applies effects of the RoboSapper to pTarget for flDuration //----------------------------------------------------------------------------- bool CObjectSapper::ApplyRoboSapperEffects( CTFPlayer *pTarget, float flDuration ) { if ( !pTarget ) return false; int iStunFlags = TF_STUN_MOVEMENT | TF_STUN_CONTROLS | TF_STUN_NO_EFFECTS; // Giants and players can't be fully incapacitated - only slowed CTFBot *pTFBot = static_cast( pTarget ); if ( ( pTFBot && pTFBot->IsMiniBoss() ) || !pTFBot ) { iStunFlags = TF_STUN_MOVEMENT; } pTarget->m_Shared.StunPlayer( flDuration, 0.85f, iStunFlags, GetBuilder() ); pTarget->m_Shared.AddCond( TF_COND_SAPPED, flDuration, GetBuilder() ); return true; } //----------------------------------------------------------------------------- // Purpose: Valid player to apply RoboSapper effects to? //----------------------------------------------------------------------------- bool CObjectSapper::IsValidRoboSapperTarget( CTFPlayer *pTarget ) { if ( !pTarget ) return false; if ( !pTarget->IsAlive() ) return false; if ( GetBuilder() && GetBuilder()->GetTeamNumber() == pTarget->GetTeam()->GetTeamNumber() ) return false; if ( pTarget->m_Shared.IsInvulnerable() ) return false; if ( pTarget->m_Shared.InCond( TF_COND_PHASE ) ) return false; if ( pTarget->m_Shared.InCond( TF_COND_SAPPED ) ) return false; if ( pTarget->m_Shared.InCond( TF_COND_REPROGRAMMED ) ) return false; return true; } float CObjectSapper::GetReversesBuildingConstructionSpeed( void ) { float flReverseSpeed = 0.0f; CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetBuilder(), flReverseSpeed, sapper_degenerates_buildings ); return flReverseSpeed; }