//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Team Fortress specific special triggers // //===========================================================================// #include "cbase.h" #include "player.h" #include "gamerules.h" #include "entityapi.h" #include "entitylist.h" #include "saverestore_utlvector.h" #include "tf_player.h" #include "triggers.h" #include "tf_triggers.h" #include "tf_weapon_compound_bow.h" #include "doors.h" #include "bot/tf_bot.h" #include "trigger_area_capture.h" #include "particle_parse.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" BEGIN_DATADESC( CTriggerStun ) // Function Pointers DEFINE_FUNCTION( StunThink ), // Fields DEFINE_KEYFIELD( m_flTriggerDelay, FIELD_FLOAT, "trigger_delay" ), DEFINE_KEYFIELD( m_flStunDuration, FIELD_FLOAT, "stun_duration" ), DEFINE_KEYFIELD( m_flMoveSpeedReduction, FIELD_FLOAT, "move_speed_reduction" ), DEFINE_KEYFIELD( m_iStunType, FIELD_INTEGER, "stun_type" ), DEFINE_KEYFIELD( m_bStunEffects, FIELD_INTEGER, "stun_effects" ), DEFINE_UTLVECTOR( m_stunEntities, FIELD_EHANDLE ), // Outputs DEFINE_OUTPUT( m_OnStunPlayer, "OnStunPlayer" ), END_DATADESC() LINK_ENTITY_TO_CLASS( trigger_stun, CTriggerStun ); //----------------------------------------------------------------------------- // Purpose: Called when spawning, after keyvalues have been handled. //----------------------------------------------------------------------------- void CTriggerStun::Spawn( void ) { BaseClass::Spawn(); InitTrigger(); SetNextThink( TICK_NEVER_THINK ); SetThink( NULL ); } //----------------------------------------------------------------------------- // Purpose: When touched, a stun trigger applies its stunflags to the other for a duration. // Input : pOther - The entity that is touching us. //----------------------------------------------------------------------------- bool CTriggerStun::StunEntity( CBaseEntity *pOther ) { if ( !pOther->m_takedamage || !PassesTriggerFilters(pOther) ) return false; CTFPlayer* pTFPlayer = ToTFPlayer( pOther ); if ( !pTFPlayer ) return false; int iStunFlags = TF_STUN_MOVEMENT; switch ( m_iStunType ) { case 0: // Movement Only break; case 1: // Controls iStunFlags |= TF_STUN_CONTROLS; break; case 2: // Loser State iStunFlags |= TF_STUN_LOSER_STATE; break; } if ( !m_bStunEffects ) { iStunFlags |= TF_STUN_NO_EFFECTS; } iStunFlags |= TF_STUN_BY_TRIGGER; pTFPlayer->m_Shared.StunPlayer( m_flStunDuration, m_flMoveSpeedReduction, iStunFlags, NULL ); m_OnStunPlayer.FireOutput( pOther, this ); m_stunEntities.AddToTail( EHANDLE(pOther) ); return true; } void CTriggerStun::StunThink() { // If I stun anyone, think again. if ( StunAllTouchers( 0.5 ) <= 0 ) { SetThink(NULL); } else { SetNextThink( gpGlobals->curtime + 0.5f ); } } void CTriggerStun::EndTouch( CBaseEntity *pOther ) { if ( PassesTriggerFilters(pOther) ) { EHANDLE hOther; hOther = pOther; // If this guy has never been stunned... if ( !m_stunEntities.HasElement( hOther ) ) { StunEntity( pOther ); } } BaseClass::EndTouch( pOther ); } int CTriggerStun::StunAllTouchers( float dt ) { m_flLastStunTime = gpGlobals->curtime; m_stunEntities.RemoveAll(); int stunCount = 0; touchlink_t *root = ( touchlink_t * )GetDataObject( TOUCHLINK ); if ( root ) { for ( touchlink_t *link = root->nextLink; link != root; link = link->nextLink ) { CBaseEntity *pTouch = link->entityTouched; if ( pTouch ) { if ( StunEntity( pTouch ) ) { stunCount++; } } } } return stunCount; } void CTriggerStun::Touch( CBaseEntity *pOther ) { if ( m_pfnThink == NULL ) { SetThink( &CTriggerStun::StunThink ); SetNextThink( gpGlobals->curtime + m_flTriggerDelay ); } } //----------------------------------------------------------------------------- // Purpose: Ignites the arrows of any bow carried by a player who touches this trigger //----------------------------------------------------------------------------- class CTriggerIgniteArrows : public CBaseTrigger { public: DECLARE_CLASS( CTriggerIgniteArrows, CBaseTrigger ); void Spawn( void ); void Touch( CBaseEntity *pOther ); DECLARE_DATADESC(); }; BEGIN_DATADESC( CTriggerIgniteArrows ) END_DATADESC() LINK_ENTITY_TO_CLASS( trigger_ignite_arrows, CTriggerIgniteArrows ); //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTriggerIgniteArrows::Spawn( void ) { BaseClass::Spawn(); InitTrigger(); } //----------------------------------------------------------------------------- // Purpose: Ignites the arrows of any bow carried by a player who touches this trigger //----------------------------------------------------------------------------- void CTriggerIgniteArrows::Touch( CBaseEntity *pOther ) { if (!PassesTriggerFilters(pOther)) return; if ( !pOther->IsPlayer() ) return; CTFPlayer *pPlayer = ToTFPlayer( pOther ); // Ignore non-snipers if ( !pPlayer || !pPlayer->IsPlayerClass(TF_CLASS_SNIPER) ) return; // Make sure they're looking at the origin Vector vecPos, vecForward, vecUp, vecRight; pPlayer->EyePositionAndVectors( &vecPos, &vecForward, &vecUp, &vecRight ); Vector vTargetDir = GetAbsOrigin() - vecPos; VectorNormalize(vTargetDir); float fDotPr = DotProduct(vecForward,vTargetDir); if (fDotPr < 0.95) return; // Does he have the bow? CTFWeaponBase *pWpn = pPlayer->GetActiveTFWeapon(); if ( pWpn && pWpn->GetWeaponID() == TF_WEAPON_COMPOUND_BOW ) { CTFCompoundBow *pBow = static_cast( pWpn ); pBow->SetArrowAlight( true ); } } //----------------------------------------------------------------------------- // Purpose: Trigger that controls door speed by specified time //----------------------------------------------------------------------------- class CTriggerTimerDoor : public CTriggerAreaCapture { public: DECLARE_CLASS( CTriggerTimerDoor, CTriggerAreaCapture ); DECLARE_DATADESC(); virtual void Spawn( void ) OVERRIDE; virtual void StartTouch( CBaseEntity *pOther ) OVERRIDE; virtual void OnStartCapture( int iTeam ) OVERRIDE; virtual void OnEndCapture( int iTeam ) OVERRIDE; protected: virtual bool CaptureModeScalesWithPlayers() const OVERRIDE { return false; } private: CHandle m_hDoor; //the door that we are linked to! string_t m_iszDoorName; }; BEGIN_DATADESC( CTriggerTimerDoor ) DEFINE_KEYFIELD( m_iszDoorName, FIELD_STRING, "door_name" ), END_DATADESC() LINK_ENTITY_TO_CLASS( trigger_timer_door, CTriggerTimerDoor ); #define GATE_THINK_TIME 0.1f //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTriggerTimerDoor::Spawn( void ) { BaseClass::Spawn(); InitTrigger(); } //----------------------------------------------------------------------------- // Purpose: Bot enters the trigger, open the door //----------------------------------------------------------------------------- void CTriggerTimerDoor::StartTouch( CBaseEntity *pOther ) { if ( m_bDisabled ) return; if (!PassesTriggerFilters(pOther)) return; if ( !m_hDoor ) { m_hDoor = dynamic_cast< CBaseDoor* >( gEntList.FindEntityByName(NULL, m_iszDoorName ) ); if ( !m_hDoor ) { Warning( "trigger_bot_gate failed to find \"%s\" door entity", STRING( m_iszDoorName ) ); return; } else { float flDoorTravelDistance = ( m_hDoor->m_vecPosition2 - m_hDoor->m_vecPosition1 ).Length(); m_hDoor->m_flSpeed = flDoorTravelDistance / GetCapTime(); } } BaseClass::StartTouch( pOther ); } //----------------------------------------------------------------------------- // Purpose: Bot starts opening the door //----------------------------------------------------------------------------- void CTriggerTimerDoor::OnStartCapture( int iTeam ) { BaseClass::OnStartCapture( iTeam ); if ( FStrEq( gpGlobals->mapname.ToCStr(), "mvm_mannhattan" ) ) { TFGameRules()->RandomPlayersSpeakConceptIfAllowed( MP_CONCEPT_MANNHATTAN_GATE_ATK, 1, TF_TEAM_RED ); } } //----------------------------------------------------------------------------- // Purpose: Bot finishes opening the door //----------------------------------------------------------------------------- void CTriggerTimerDoor::OnEndCapture( int iTeam ) { BaseClass::OnEndCapture( iTeam ); if ( FStrEq( gpGlobals->mapname.ToCStr(), "mvm_mannhattan" ) ) { TFGameRules()->RandomPlayersSpeakConceptIfAllowed( MP_CONCEPT_MANNHATTAN_GATE_TAKE, 1, TF_TEAM_RED ); } } //----------------------------------------------------------------------------- // Purpose: Trigger that adds tag to bots //----------------------------------------------------------------------------- class CTriggerBotTag : public CBaseTrigger { public: DECLARE_DATADESC(); DECLARE_CLASS( CTriggerBotTag, CBaseTrigger ); virtual void Spawn( void ); virtual void Touch( CBaseEntity *pOther ); private: string_t m_iszTags; bool m_bAdd; CUtlStringList m_tags; }; BEGIN_DATADESC( CTriggerBotTag ) DEFINE_KEYFIELD( m_iszTags, FIELD_STRING, "tags" ), DEFINE_KEYFIELD( m_bAdd, FIELD_BOOLEAN, "add" ), END_DATADESC() LINK_ENTITY_TO_CLASS( trigger_bot_tag, CTriggerBotTag ); void CTriggerBotTag::Spawn() { BaseClass::Spawn(); InitTrigger(); m_tags.RemoveAll(); // chop space-delimited string into individual tokens const char *tags = STRING( m_iszTags ); if ( tags ) { CSplitString splitStrings( tags, " " ); for( int i=0; iIsPlayer() ) { return; } CTFBot *pBot = ToTFBot( pOther ); if ( !pBot ) { return; } if ( m_bAdd ) { for ( int i=0; iAddTag( m_tags[i] ); } } else { for ( int i=0; iRemoveTag( m_tags[i] ); } } } //----------------------------------------------------------------------------- // Purpose: Trigger that adds a condition to players //----------------------------------------------------------------------------- class CTriggerAddTFPlayerCondition : public CBaseTrigger { public: DECLARE_DATADESC(); DECLARE_CLASS( CTriggerAddTFPlayerCondition, CBaseTrigger ); virtual void Spawn( void ); virtual void StartTouch( CBaseEntity *pOther ); virtual void EndTouch( CBaseEntity *pOther ); private: ETFCond m_nCondition; float m_flDuration; }; BEGIN_DATADESC( CTriggerAddTFPlayerCondition ) DEFINE_KEYFIELD( m_nCondition, FIELD_INTEGER, "condition" ), DEFINE_KEYFIELD( m_flDuration, FIELD_FLOAT, "duration" ), END_DATADESC() LINK_ENTITY_TO_CLASS( trigger_add_tf_player_condition, CTriggerAddTFPlayerCondition ); void CTriggerAddTFPlayerCondition::Spawn() { BaseClass::Spawn(); InitTrigger(); } void CTriggerAddTFPlayerCondition::StartTouch( CBaseEntity *pOther ) { if ( m_bDisabled ) { return; } if ( !PassesTriggerFilters(pOther) ) { return; } if ( !pOther->IsPlayer() ) { return; } CTFPlayer *pPlayer = ToTFPlayer( pOther ); if ( !pPlayer ) { return; } if ( m_nCondition != TF_COND_INVALID ) { pPlayer->m_Shared.AddCond( m_nCondition, m_flDuration ); BaseClass::StartTouch( pOther ); } else { Warning( "Invalid Condition ID [%d] in trigger %s\n", m_nCondition, GetEntityName().ToCStr() ); } } void CTriggerAddTFPlayerCondition::EndTouch( CBaseEntity *pOther ) { if ( m_flDuration != PERMANENT_CONDITION ) { return; } if ( m_bDisabled ) { return; } if ( !PassesTriggerFilters(pOther) ) { return; } if ( !pOther->IsPlayer() ) { return; } CTFPlayer *pPlayer = ToTFPlayer( pOther ); if ( !pPlayer ) { return; } if ( m_nCondition != TF_COND_INVALID ) { pPlayer->m_Shared.RemoveCond( m_nCondition ); BaseClass::EndTouch( pOther ); } else { Warning( "Invalid Condition ID [%d] in trigger %s\n", m_nCondition, GetEntityName().ToCStr() ); } } //----------------------------------------------------------------------------- // Purpose: CTriggerPlayerRespawnOverride //----------------------------------------------------------------------------- BEGIN_DATADESC( CTriggerPlayerRespawnOverride ) DEFINE_KEYFIELD( m_flRespawnTime, FIELD_FLOAT, "RespawnTime" ), DEFINE_KEYFIELD( m_strRespawnEnt, FIELD_STRING, "RespawnName" ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetRespawnTime", InputSetRespawnTime ), DEFINE_INPUTFUNC( FIELD_STRING, "SetRespawnName", InputSetRespawnName ), END_DATADESC() LINK_ENTITY_TO_CLASS( trigger_player_respawn_override, CTriggerPlayerRespawnOverride ); IMPLEMENT_AUTO_LIST( ITriggerPlayerRespawnOverride ); //----------------------------------------------------------------------------- // Purpose: Trigger that ignites players //----------------------------------------------------------------------------- class CTriggerIgnite : public CBaseTrigger { public: DECLARE_DATADESC(); DECLARE_CLASS( CTriggerIgnite, CBaseTrigger ); CTriggerIgnite(); virtual void Spawn( void ); virtual void Precache( void ); virtual void StartTouch( CBaseEntity *pOther ); void BurnThink(); private: void IgniteEntity( CBaseEntity *pOther ); int BurnEntities(); float m_flBurnDuration; float m_flDamagePercentPerSecond; string_t m_iszIgniteParticleName; string_t m_iszIgniteSoundName; float m_flLastBurnTime; }; BEGIN_DATADESC( CTriggerIgnite ) DEFINE_KEYFIELD( m_flBurnDuration, FIELD_FLOAT, "burn_duration" ), DEFINE_KEYFIELD( m_flDamagePercentPerSecond, FIELD_FLOAT, "damage_percent_per_second" ), DEFINE_KEYFIELD( m_iszIgniteParticleName, FIELD_STRING, "ignite_particle_name" ), DEFINE_KEYFIELD( m_iszIgniteSoundName, FIELD_STRING, "ignite_sound_name" ), END_DATADESC() LINK_ENTITY_TO_CLASS( trigger_ignite, CTriggerIgnite ); #define BURN_INTERVAL 0.1f CTriggerIgnite::CTriggerIgnite() { m_flBurnDuration = 1.f; m_flDamagePercentPerSecond = 10.f; m_flLastBurnTime = 0.f; } void CTriggerIgnite::Spawn() { BaseClass::Spawn(); InitTrigger(); SetNextThink( TICK_NEVER_THINK ); SetThink( NULL ); } void CTriggerIgnite::Precache( void ) { BaseClass::Precache(); const char *pszParticleName = STRING( m_iszIgniteParticleName ); if ( pszParticleName && *pszParticleName ) { PrecacheParticleSystem( pszParticleName ); } const char *pszSoundName = STRING( m_iszIgniteSoundName ); if ( pszSoundName && *pszSoundName ) { PrecacheScriptSound( pszSoundName ); } } void CTriggerIgnite::BurnThink() { if ( BurnEntities() > 0 ) { SetNextThink( gpGlobals->curtime + BURN_INTERVAL ); } else { SetThink( NULL ); } } void CTriggerIgnite::StartTouch( CBaseEntity *pOther ) { if ( m_bDisabled ) { return; } if ( !PassesTriggerFilters(pOther) ) { return; } IgniteEntity( pOther ); BaseClass::StartTouch( pOther ); if ( m_pfnThink == NULL ) { m_flLastBurnTime = gpGlobals->curtime; SetThink( &CTriggerIgnite::BurnThink ); SetNextThink( gpGlobals->curtime + BURN_INTERVAL ); } } void CTriggerIgnite::IgniteEntity( CBaseEntity *pOther ) { Vector vecEffectPos = pOther->GetAbsOrigin(); const char *pszParticleName = STRING( m_iszIgniteParticleName ); if ( pszParticleName && *pszParticleName ) { DispatchParticleEffect( pszParticleName, vecEffectPos, vec3_angle ); } const char *pszSoundName = STRING( m_iszIgniteSoundName ); if ( pszSoundName && *pszSoundName ) { CSoundParameters params; if ( CBaseEntity::GetParametersForSound( pszSoundName, params, NULL ) ) { CPASAttenuationFilter soundFilter( vecEffectPos, params.soundlevel ); EmitSound_t ep( params ); ep.m_pOrigin = &vecEffectPos; EmitSound( soundFilter, entindex(), ep ); } } if ( pOther->IsPlayer() ) { CTFPlayer *pTFPlayer = ToTFPlayer( pOther ); if ( pTFPlayer && !pTFPlayer->m_Shared.IsInvulnerable() && !pTFPlayer->m_Shared.InCond( TF_COND_BURNING ) ) { pTFPlayer->m_Shared.SelfBurn( m_flBurnDuration ); } } else { UTIL_Remove( pOther ); } } int CTriggerIgnite::BurnEntities() { if ( m_hTouchingEntities.IsEmpty() ) return 0; float flDT = gpGlobals->curtime - m_flLastBurnTime; m_flLastBurnTime = gpGlobals->curtime; int nBurn = 0; FOR_EACH_VEC( m_hTouchingEntities, i ) { CBaseEntity *pEnt = m_hTouchingEntities[i]; if ( pEnt && pEnt->IsPlayer() ) { CTFPlayer *pTFPlayer = ToTFPlayer( pEnt ); if ( pTFPlayer ) { float flDamageScale = m_flDamagePercentPerSecond * 0.01f; float flDamage = flDT * flDamageScale * pTFPlayer->GetMaxHealth(); CTakeDamageInfo info( this, this, flDamage, DMG_BURN ); if ( !pTFPlayer->m_Shared.IsInvulnerable() && !pTFPlayer->m_Shared.InCond( TF_COND_BURNING ) ) // if player enters trigger invuln, we need to ignite them when it wears off. We also don't want to ignite an already burning player { IgniteEntity( pTFPlayer ); } pTFPlayer->TakeDamage( info ); nBurn++; } } } return nBurn; } //----------------------------------------------------------------------------- // Purpose: Trigger that spawn particles on entities //----------------------------------------------------------------------------- class CTriggerParticle : public CBaseTrigger { public: DECLARE_DATADESC(); DECLARE_CLASS( CTriggerParticle, CBaseTrigger ); CTriggerParticle(); virtual void Spawn( void ); virtual void Precache( void ); virtual void StartTouch( CBaseEntity *pOther ); private: string_t m_iszParticleName; string_t m_iszAttachmentName; ParticleAttachment_t m_nAttachType; }; BEGIN_DATADESC( CTriggerParticle ) DEFINE_KEYFIELD( m_iszParticleName, FIELD_STRING, "particle_name" ), DEFINE_KEYFIELD( m_iszAttachmentName, FIELD_STRING, "attachment_name" ), DEFINE_KEYFIELD( m_nAttachType, FIELD_INTEGER, "attachment_type" ), END_DATADESC() LINK_ENTITY_TO_CLASS( trigger_particle, CTriggerParticle ); CTriggerParticle::CTriggerParticle() { m_nAttachType = PATTACH_ABSORIGIN; } void CTriggerParticle::Spawn() { BaseClass::Spawn(); InitTrigger(); SetNextThink( TICK_NEVER_THINK ); SetThink( NULL ); } void CTriggerParticle::Precache( void ) { BaseClass::Precache(); const char *pszParticleName = STRING( m_iszParticleName ); if ( pszParticleName && *pszParticleName ) { PrecacheParticleSystem( pszParticleName ); } } void CTriggerParticle::StartTouch( CBaseEntity *pOther ) { if ( m_bDisabled ) { return; } if ( !PassesTriggerFilters(pOther) ) { return; } BaseClass::StartTouch( pOther ); const char *pszParticleName = STRING( m_iszParticleName ); const char *pszAttachmentName = STRING( m_iszAttachmentName ); int iAttachment = -1; if ( pszAttachmentName && *pszAttachmentName ) { iAttachment = pOther->GetBaseAnimating()->LookupAttachment( pszAttachmentName ); } DispatchParticleEffect( pszParticleName, m_nAttachType, pOther, iAttachment ); } class CTriggerRemoveTFPlayerCondition : public CBaseTrigger { public: DECLARE_DATADESC(); DECLARE_CLASS( CTriggerRemoveTFPlayerCondition, CBaseTrigger ); virtual void Spawn( void ); virtual void StartTouch( CBaseEntity *pOther ); private: ETFCond m_nCondition; }; BEGIN_DATADESC( CTriggerRemoveTFPlayerCondition ) DEFINE_KEYFIELD( m_nCondition, FIELD_INTEGER, "condition" ), END_DATADESC() LINK_ENTITY_TO_CLASS( trigger_remove_tf_player_condition, CTriggerRemoveTFPlayerCondition ); void CTriggerRemoveTFPlayerCondition::Spawn() { BaseClass::Spawn(); InitTrigger(); } void CTriggerRemoveTFPlayerCondition::StartTouch( CBaseEntity *pOther ) { if ( m_bDisabled ) { return; } if ( !PassesTriggerFilters(pOther) ) { return; } if ( !pOther->IsPlayer() ) { return; } CTFPlayer *pPlayer = ToTFPlayer( pOther ); if ( !pPlayer ) { return; } if ( m_nCondition != TF_COND_INVALID ) { pPlayer->m_Shared.RemoveCond( m_nCondition ); // Hack for Bank until we re-address this for parachuting MvM bots CTFBot *pTFBot = dynamic_cast< CTFBot* >( pPlayer ); if ( pTFBot ) { pTFBot->ClearLastKnownArea(); } } else { pPlayer->m_Shared.RemoveAllCond(); } BaseClass::StartTouch( pOther ); } class CTriggerAddOrRemoveTFPlayerAttributes : public CBaseTrigger { public: DECLARE_DATADESC(); DECLARE_CLASS( CTriggerAddOrRemoveTFPlayerAttributes, CBaseTrigger ); virtual void Spawn( void ); virtual void StartTouch( CBaseEntity *pOther ); virtual void EndTouch( CBaseEntity *pOther ); private: bool m_bRemove; string_t m_iszAttributeName; float m_flAttributeValue; float m_flDuration; bool m_bValidAttribute; }; BEGIN_DATADESC( CTriggerAddOrRemoveTFPlayerAttributes ) DEFINE_KEYFIELD( m_bRemove, FIELD_BOOLEAN, "add_or_remove" ), DEFINE_KEYFIELD( m_iszAttributeName, FIELD_STRING, "attribute_name" ), DEFINE_KEYFIELD( m_flAttributeValue, FIELD_FLOAT, "value" ), DEFINE_KEYFIELD( m_flDuration, FIELD_FLOAT, "duration" ), END_DATADESC() LINK_ENTITY_TO_CLASS( trigger_add_or_remove_tf_player_attributes, CTriggerAddOrRemoveTFPlayerAttributes ); void CTriggerAddOrRemoveTFPlayerAttributes::Spawn() { BaseClass::Spawn(); InitTrigger(); m_bValidAttribute = false; const char *pszAttrName = STRING( m_iszAttributeName ); if ( pszAttrName && *pszAttrName ) { CSchemaAttributeDefHandle pAttrTest( pszAttrName ); if ( pAttrTest ) { m_bValidAttribute = true; } else { Warning( "Invalid attribute name '%s' from trigger trigger_add_or_remove_tf_player_attributes name '%s'\n", pszAttrName, STRING( GetEntityName() ) ); } } } void CTriggerAddOrRemoveTFPlayerAttributes::StartTouch( CBaseEntity *pOther ) { if ( m_bDisabled ) { return; } if ( !m_bValidAttribute ) { return; } if ( !PassesTriggerFilters(pOther) ) { return; } if ( !pOther->IsPlayer() ) { return; } CTFPlayer *pPlayer = ToTFPlayer( pOther ); if ( !pPlayer ) { return; } const char *pszAttrName = STRING( m_iszAttributeName ); if ( m_bRemove ) { pPlayer->RemoveCustomAttribute( pszAttrName ); } else { pPlayer->AddCustomAttribute( pszAttrName, m_flAttributeValue, m_flDuration ); } BaseClass::StartTouch( pOther ); } void CTriggerAddOrRemoveTFPlayerAttributes::EndTouch( CBaseEntity *pOther ) { if ( m_bDisabled ) { return; } if ( !m_bValidAttribute ) { return; } // only run auto remove on added attribute if ( m_bRemove ) { return; } // ignore timer attribute. it'll remove itself if ( m_flDuration > 0.f ) { return; } if ( !PassesTriggerFilters(pOther) ) { return; } if ( !pOther->IsPlayer() ) { return; } CTFPlayer *pPlayer = ToTFPlayer( pOther ); if ( !pPlayer ) { return; } // remove permanent attribute on exist trigger const char *pszAttrName = STRING( m_iszAttributeName ); pPlayer->RemoveCustomAttribute( pszAttrName ); BaseClass::EndTouch( pOther ); }