//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: TF Pipebomb Grenade. // //=============================================================================// #include "cbase.h" #include "tf_weaponbase.h" #include "tf_gamerules.h" #include "npcevent.h" #include "engine/IEngineSound.h" #include "tf_weapon_grenade_pipebomb.h" #include "tf_weapon_pipebomblauncher.h" #include "tf_weapon_grenadelauncher.h" // Client specific. #ifdef CLIENT_DLL #include "c_tf_player.h" #include "IEffects.h" #include "materialsystem/imaterialvar.h" #include "functionproxy.h" // Server specific. #else #include "tf_player.h" #include "items.h" #include "tf_weaponbase_grenadeproj.h" #include "soundent.h" #include "KeyValues.h" #include "IEffects.h" #include "props.h" #include "func_respawnroom.h" #include "tf_ammo_pack.h" #include "takedamageinfo.h" #include "tf_team.h" #include "physics_collisionevent.h" #ifdef TF_RAID_MODE #include "player_vs_environment/boss_alpha/boss_alpha.h" #endif // TF_RAID_MODE #include "tf_weapon_medigun.h" #endif #define TF_WEAPON_PIPEBOMB_TIMER 3.0f //Seconds #define TF_WEAPON_PIPEBOMB_GRAVITY 0.5f #define TF_WEAPON_PIPEBOMB_FRICTION 0.8f #define TF_WEAPON_PIPEBOMB_ELASTICITY 0.45f #define TF_WEAPON_PIPEBOMB_TIMER_DMG_REDUCTION 0.6 extern ConVar tf_grenadelauncher_max_chargetime; ConVar tf_grenadelauncher_chargescale( "tf_grenadelauncher_chargescale", "1.0", FCVAR_CHEAT | FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY ); ConVar tf_grenadelauncher_livetime( "tf_grenadelauncher_livetime", "0.8", FCVAR_CHEAT | FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY ); extern ConVar tf_sticky_radius_ramp_time; extern ConVar tf_sticky_airdet_radius; #ifndef CLIENT_DLL ConVar tf_grenadelauncher_min_contact_speed( "tf_grenadelauncher_min_contact_speed", "100", FCVAR_DEVELOPMENTONLY ); extern ConVar tf_obj_gib_velocity_min; extern ConVar tf_obj_gib_velocity_max; extern ConVar tf_obj_gib_maxspeed; #endif IMPLEMENT_NETWORKCLASS_ALIASED( TFGrenadePipebombProjectile, DT_TFProjectile_Pipebomb ) BEGIN_NETWORK_TABLE( CTFGrenadePipebombProjectile, DT_TFProjectile_Pipebomb ) #ifdef CLIENT_DLL RecvPropInt( RECVINFO( m_bTouched ) ), RecvPropInt( RECVINFO( m_iType ) ), RecvPropEHandle( RECVINFO( m_hLauncher ) ), RecvPropBool( RECVINFO( m_bDefensiveBomb ) ), #else SendPropBool( SENDINFO( m_bTouched ) ), SendPropInt( SENDINFO( m_iType ), 3 ), SendPropEHandle( SENDINFO( m_hLauncher ) ), SendPropBool( SENDINFO( m_bDefensiveBomb ) ), #endif END_NETWORK_TABLE() #ifdef GAME_DLL static string_t s_iszTrainName; #endif //----------------------------------------------------------------------------- // Purpose: // Input : - //----------------------------------------------------------------------------- CTFGrenadePipebombProjectile::CTFGrenadePipebombProjectile() { m_bTouched = false; m_flChargeTime = 0.0f; m_bDetonateOnPulse = false; #ifdef GAME_DLL s_iszTrainName = AllocPooledString( "models/props_vehicles/train_enginecar.mdl" ); m_flDeflectedTime = 0.0f; m_bWallShatter = false; m_bDefensiveBomb = false; m_bSendPlayerDestroyedEvent = true; m_bCanTakeDamage = true; #else pEffectTrail = NULL; pEffectCrit = NULL; m_iCachedDeflect = 0; m_bHighlight = false; #endif } //----------------------------------------------------------------------------- // Purpose: // Input : - //----------------------------------------------------------------------------- CTFGrenadePipebombProjectile::~CTFGrenadePipebombProjectile() { #ifdef CLIENT_DLL if ( pEffectTrail ) { ParticleProp()->StopEmission( pEffectTrail ); } if ( pEffectCrit ) { ParticleProp()->StopEmission( pEffectCrit ); } if ( m_pGlowEffect ) { delete m_pGlowEffect; m_pGlowEffect = NULL; } #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CTFGrenadePipebombProjectile::GetWeaponID( void ) const { if ( m_iType == TF_GL_MODE_CANNONBALL ) { return TF_WEAPON_CANNON; } return ( HasStickyEffects() ? TF_WEAPON_GRENADE_PIPEBOMB : TF_WEAPON_GRENADE_DEMOMAN ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CTFGrenadePipebombProjectile::GetDamageType( void ) { int iDmgType = BaseClass::GetDamageType(); // If we're a pipebomb, we do distance based damage falloff for just the first few seconds of our life if ( m_iType == TF_GL_MODE_REMOTE_DETONATE ) { if ( gpGlobals->curtime - m_flCreationTime < 5.0 ) { iDmgType |= DMG_USEDISTANCEMOD; } } return iDmgType; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFGrenadePipebombProjectile::ShouldMiniCritOnReflect() const { return GetType() == TF_GL_MODE_REGULAR; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadePipebombProjectile::UpdateOnRemove( void ) { // Tell our launcher that we were removed CTFPipebombLauncher *pLauncher = dynamic_cast( m_hLauncher.Get() ); if ( pLauncher ) { pLauncher->DeathNotice( this ); } BaseClass::UpdateOnRemove(); } #ifdef CLIENT_DLL //============================================================================= // // TF Pipebomb Grenade Projectile functions (Client specific). // //----------------------------------------------------------------------------- // Purpose: // Output : const char //----------------------------------------------------------------------------- const char *CTFGrenadePipebombProjectile::GetTrailParticleName( void ) { int iTeamNumber = GetTeamNumber(); if ( GetDeflected() && m_iType != TF_GL_MODE_REMOTE_DETONATE ) { CTFPlayer *pOwner = ToTFPlayer( GetDeflectOwner() ); if ( pOwner ) { iTeamNumber = pOwner->GetTeamNumber(); } } if ( HasStickyEffects() ) { if ( iTeamNumber == TF_TEAM_BLUE ) { return "stickybombtrail_blue"; } else { return "stickybombtrail_red"; } } else { if ( iTeamNumber == TF_TEAM_BLUE ) { return "pipebombtrail_blue"; } else { return "pipebombtrail_red"; } } } //----------------------------------------------------------------------------- // Purpose: // Input : updateType - //----------------------------------------------------------------------------- void CTFGrenadePipebombProjectile::OnDataChanged(DataUpdateType_t updateType) { BaseClass::OnDataChanged( updateType ); if ( updateType == DATA_UPDATE_CREATED ) { m_flCreationTime = gpGlobals->curtime; m_bPulsed = false; CTFPipebombLauncher *pLauncher = dynamic_cast( m_hLauncher.Get() ); if ( pLauncher ) { pLauncher->AddPipeBomb( this ); } if ( m_bDefensiveBomb && C_BasePlayer::GetLocalPlayer() == GetThrower() ) { if ( GetTeamNumber() == TF_TEAM_RED ) { m_pGlowEffect = new CGlowObject( this, Vector( 150, 0, 0 ), 1.0, true ); } else { m_pGlowEffect = new CGlowObject( this, Vector( 0, 0, 150 ), 1.0, true ); } } CreateTrailParticles(); } else if ( m_bTouched ) { //ParticleProp()->StopEmission(); } if ( m_iCachedDeflect != GetDeflected() ) { CreateTrailParticles(); } m_iCachedDeflect = GetDeflected(); } void CTFGrenadePipebombProjectile::CreateTrailParticles( void ) { if ( pEffectTrail ) { ParticleProp()->StopEmission( pEffectTrail ); } if ( pEffectCrit ) { ParticleProp()->StopEmission( pEffectCrit ); } pEffectTrail = ParticleProp()->Create( GetTrailParticleName(), PATTACH_ABSORIGIN_FOLLOW ); int iTeamNumber = GetTeamNumber(); if ( GetDeflected() && m_iType != TF_GL_MODE_REMOTE_DETONATE ) { CTFPlayer *pOwner = ToTFPlayer( GetDeflectOwner() ); if ( pOwner ) { iTeamNumber = pOwner->GetTeamNumber(); } } if ( m_bCritical ) { switch( iTeamNumber ) { case TF_TEAM_BLUE: if ( HasStickyEffects() ) { pEffectCrit = ParticleProp()->Create( "critical_grenade_blue", PATTACH_ABSORIGIN_FOLLOW ); } else { pEffectCrit = ParticleProp()->Create( "critical_pipe_blue", PATTACH_ABSORIGIN_FOLLOW ); } break; case TF_TEAM_RED: if ( HasStickyEffects() ) { pEffectCrit = ParticleProp()->Create( "critical_grenade_red", PATTACH_ABSORIGIN_FOLLOW ); } else { pEffectCrit = ParticleProp()->Create( "critical_pipe_red", PATTACH_ABSORIGIN_FOLLOW ); } break; default: break; } } } extern ConVar tf_grenadelauncher_livetime; void CTFGrenadePipebombProjectile::Simulate( void ) { BaseClass::Simulate(); if ( !HasStickyEffects() ) return; if ( m_bPulsed == false ) { if ( (gpGlobals->curtime - m_flCreationTime) >= GetLiveTime() ) { if ( GetTeamNumber() == TF_TEAM_RED ) { ParticleProp()->Create( "stickybomb_pulse_red", PATTACH_ABSORIGIN_FOLLOW ); } else { ParticleProp()->Create( "stickybomb_pulse_blue", PATTACH_ABSORIGIN_FOLLOW ); } m_bPulsed = true; if ( m_bDetonateOnPulse ) { Detonate(); } } } } //------------------------------------------------------------------------------ // Purpose: Don't draw if we haven't yet gone past our original spawn point // Input : flags - //----------------------------------------------------------------------------- int CTFGrenadePipebombProjectile::DrawModel( int flags ) { if ( gpGlobals->curtime < ( m_flCreationTime + 0.1 ) ) return 0; return BaseClass::DrawModel( flags ); } #else //============================================================================= // // TF Pipebomb Grenade Projectile functions (Server specific). // #define TF_WEAPON_PIPEGRENADE_MODEL "models/weapons/w_models/w_grenade_grenadelauncher.mdl" #define TF_WEAPON_CANNONBALL_MODEL "models/weapons/w_models/w_cannonball.mdl" #define TF_WEAPON_PIPEBOMB_MODEL "models/weapons/w_models/w_stickybomb.mdl" #define TF_WEAPON_PIPEBOMB2_MODEL "models/weapons/w_models/w_stickybomb2.mdl" #define TF_WEAPON_PIPEBOMBD_MODEL "models/weapons/w_models/w_stickybomb_d.mdl" #define TF_WEAPON_PIPEBOMB_BOUNCE_SOUND "Weapon_Grenade_Pipebomb.Bounce" #define TF_WEAPON_CANNON_IMPACT_SOUND "Weapon_LooseCannon.BallImpact" #define TF_WEAPON_GRENADE_DETONATE_TIME 2.0f #define TF_WEAPON_GRENADE_XBOX_DAMAGE 112 BEGIN_DATADESC( CTFGrenadePipebombProjectile ) END_DATADESC() LINK_ENTITY_TO_CLASS( tf_projectile_pipe_remote, CTFGrenadePipebombProjectile ); PRECACHE_WEAPON_REGISTER( tf_projectile_pipe_remote ); LINK_ENTITY_TO_CLASS( tf_projectile_pipe, CTFGrenadePipebombProjectile ); PRECACHE_WEAPON_REGISTER( tf_projectile_pipe ); //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const char* CTFGrenadePipebombProjectile::GetPipebombClass( int iPipeBombType ) { switch ( iPipeBombType ) { case TF_GL_MODE_REGULAR: return "tf_projectile_pipe"; case TF_GL_MODE_REMOTE_DETONATE: case TF_GL_MODE_REMOTE_DETONATE_PRACTICE: return "tf_projectile_pipe_remote"; default: return "tf_projectile_pipe"; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CTFGrenadePipebombProjectile* CTFGrenadePipebombProjectile::Create( const Vector &position, const QAngle &angles, const Vector &velocity, const AngularImpulse &angVelocity, CBaseCombatCharacter *pOwner, const CTFWeaponInfo &weaponInfo, int iPipeBombType, float flMultDmg ) { // Translate a projectile type into a pipebomb type. int iPipeBombDetonateType; switch ( iPipeBombType ) { case TF_PROJECTILE_PIPEBOMB_REMOTE: { iPipeBombDetonateType = TF_GL_MODE_REMOTE_DETONATE; } break; case TF_PROJECTILE_PIPEBOMB_PRACTICE: { iPipeBombDetonateType = TF_GL_MODE_REMOTE_DETONATE_PRACTICE; } break; case TF_PROJECTILE_CANNONBALL: { iPipeBombDetonateType = TF_GL_MODE_CANNONBALL; } break; default: iPipeBombDetonateType = TF_GL_MODE_REGULAR; } const char* pszBombClass = GetPipebombClass( iPipeBombDetonateType ); CTFGrenadePipebombProjectile *pGrenade = static_cast( CBaseEntity::CreateNoSpawn( pszBombClass, position, angles, pOwner ) ); if ( pGrenade ) { // Set the pipebomb mode before calling spawn, so the model & associated vphysics get setup properly pGrenade->SetPipebombMode( iPipeBombDetonateType ); DispatchSpawn( pGrenade ); pGrenade->InitGrenade( velocity, angVelocity, pOwner, weaponInfo ); pGrenade->SetDamage( pGrenade->GetDamage() * flMultDmg ); pGrenade->SetFullDamage( pGrenade->GetDamage() ); if ( pGrenade->m_iType != TF_GL_MODE_REMOTE_DETONATE ) { // Some hackery here. Reduce the damage, so that if we explode on timeout, // we'll do less damage. If we explode on contact, we'll restore this to full damage. pGrenade->SetDamage( pGrenade->GetDamage() * TF_WEAPON_PIPEBOMB_TIMER_DMG_REDUCTION ); } pGrenade->ApplyLocalAngularVelocityImpulse( angVelocity ); if ( pOwner ) { pGrenade->SetTruceValidForEnt( pOwner->IsTruceValidForEnt() ); } } return pGrenade; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadePipebombProjectile::Spawn() { if ( HasStickyEffects() ) { // Set this to max, so effectively they do not self-implode. if ( m_iType == TF_GL_MODE_REMOTE_DETONATE_PRACTICE ) { SetModel( TF_WEAPON_PIPEBOMB2_MODEL ); } else { SetModel( TF_WEAPON_PIPEBOMB_MODEL ); } SetDetonateTimerLength( FLT_MAX ); SetContextThink( &CTFGrenadePipebombProjectile::PreArmThink, gpGlobals->curtime + 0.001f, "PRE_ARM_THINK" ); // Next frame. SetTouch( &CTFGrenadePipebombProjectile::StickybombTouch ); } else { if ( m_iType == TF_GL_MODE_CANNONBALL ) { SetModel( TF_WEAPON_CANNONBALL_MODEL ); } else { SetModel( TF_WEAPON_PIPEGRENADE_MODEL ); } SetDetonateTimerLength( TF_WEAPON_GRENADE_DETONATE_TIME ); SetTouch( &CTFGrenadePipebombProjectile::PipebombTouch ); } SetCustomPipebombModel(); BaseClass::Spawn(); m_bTouched = false; m_flCreationTime = gpGlobals->curtime; // We want to get touch functions called so we can damage enemy players AddSolidFlags( FSOLID_TRIGGER ); m_flMinSleepTime = 0; AddFlag( FL_GRENADE ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadePipebombProjectile::Precache() { int iModel = PrecacheModel( TF_WEAPON_PIPEBOMB_MODEL ); PrecacheGibsForModel( iModel ); iModel = PrecacheModel( TF_WEAPON_PIPEBOMB2_MODEL ); PrecacheGibsForModel( iModel ); iModel = PrecacheModel( TF_WEAPON_PIPEBOMBD_MODEL ); PrecacheGibsForModel( iModel ); iModel = PrecacheModel( TF_WEAPON_PIPEGRENADE_MODEL ); PrecacheGibsForModel( iModel ); iModel = PrecacheModel( TF_WEAPON_CANNONBALL_MODEL ); PrecacheGibsForModel( iModel ); // Must add All custom Models here iModel = PrecacheModel( "models/workshop/weapons/c_models/c_kingmaker_sticky/w_kingmaker_stickybomb.mdl" ); iModel = PrecacheModel( "models/workshop/weapons/c_models/c_quadball/w_quadball_grenade.mdl" ); PrecacheParticleSystem( "stickybombtrail_blue" ); PrecacheParticleSystem( "stickybombtrail_red" ); PrecacheScriptSound( TF_WEAPON_PIPEBOMB_BOUNCE_SOUND ); PrecacheScriptSound( TF_WEAPON_CANNON_IMPACT_SOUND ); BaseClass::Precache(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadePipebombProjectile::SetPipebombMode( int iPipebombMode /* = TF_GL_MODE_REGULAR */ ) { m_iType.Set( iPipebombMode ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadePipebombProjectile::BounceSound( void ) { EmitSound( TF_WEAPON_PIPEBOMB_BOUNCE_SOUND ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadePipebombProjectile::Detonate() { if ( gpGlobals->curtime > m_flDetonateTime ) { if ( GetLauncher() ) { float flFizzle = 0; CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetLauncher(), flFizzle, stickybomb_fizzle_time ); if ( flFizzle ) { Fizzle(); } } } if ( m_bFizzle ) { g_pEffects->Sparks( GetAbsOrigin(), 1, 2 ); Destroy( false ); if ( HasStickyEffects() ) { CreatePipebombGibs(); } return; } BaseClass::Detonate(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CTFGrenadePipebombProjectile::DetonateStickies() { if ( !GetLauncher() ) return false; bool bDetonateSticky = false; Vector vecOrigin = GetAbsOrigin(); const int maxEntities = 64; CBaseEntity *pObjects[ maxEntities ]; int count = UTIL_EntitiesInSphere( pObjects, maxEntities, vecOrigin, GetDamageRadius(), FL_GRENADE ); int iStickiesRemoved = 0; trace_t tr; for ( int i = 0; i < count; i++ ) { if ( pObjects[i]->GetTeamNumber() == GetLauncher()->GetTeamNumber() ) continue; CTFGrenadePipebombProjectile *pGrenade = dynamic_cast < CTFGrenadePipebombProjectile*> ( pObjects[i] ); if ( !pGrenade ) continue; if ( pGrenade->m_iType != TF_GL_MODE_REMOTE_DETONATE ) continue; if ( pGrenade->m_bFizzle ) continue; UTIL_TraceLine( vecOrigin, pGrenade->GetAbsOrigin(), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); if ( tr.fraction < 1.0 ) continue; // No line of sight to the bomb. pGrenade->Fizzle(); pGrenade->Detonate(); iStickiesRemoved++; bDetonateSticky = true; } CTFPlayer *pOwner = ToTFPlayer( GetThrower() ); if ( iStickiesRemoved && pOwner ) { pOwner->AwardAchievement( ACHIEVEMENT_TF_DEMOMAN_DESTROY_X_STICKYBOMBS, iStickiesRemoved ); } return bDetonateSticky; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadePipebombProjectile::CreatePipebombGibs( void ) { CPVSFilter filter( GetAbsOrigin() ); UserMessageBegin( filter, "CheapBreakModel" ); WRITE_SHORT( GetModelIndex() ); WRITE_VEC3COORD( GetAbsOrigin() ); MessageEnd(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadePipebombProjectile::Fizzle( void ) { m_bFizzle = true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadePipebombProjectile::StickybombTouch( CBaseEntity *pOther ) { #ifdef GAME_DLL #ifdef TF_RAID_MODE if ( TFGameRules()->IsRaidMode() ) { if ( dynamic_cast< CBossAlpha * >( pOther ) != NULL ) { // stickies stick to the boss m_bTouched = true; VPhysicsGetObject()->EnableMotion( false ); SetParent( pOther ); } } #endif #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFGrenadePipebombProjectile::PipebombTouch( CBaseEntity *pOther ) { if ( pOther == GetThrower() ) return; // Verify a correct "other." if ( !pOther->IsSolid() || pOther->IsSolidFlagSet( FSOLID_VOLUME_CONTENTS ) ) return; // Handle hitting skybox (disappear). trace_t pTrace; Vector velDir = GetAbsVelocity(); VectorNormalize( velDir ); Vector vOrigin = GetAbsOrigin(); Vector vecSpot = vOrigin - velDir * 32; UTIL_TraceLine( vecSpot, vecSpot + velDir * 64, MASK_SOLID, this, COLLISION_GROUP_NONE, &pTrace ); if ( pTrace.fraction < 1.0 && pTrace.surface.flags & SURF_SKY ) { UTIL_Remove( this ); return; } // PASSTIME always explode when it hits the ball // fixme find a non-strcmp way to do this if ( !V_strcmp( pOther->GetClassname(), "passtime_ball" ) ) { Explode( &pTrace, GetDamageType() ); return; } //If we already touched a surface then we're not exploding on contact anymore. if ( m_bTouched == true ) return; bool bExploded = false; // Blow up if we hit an enemy we can damage if ( pOther->GetTeamNumber() && pOther->GetTeamNumber() != GetTeamNumber() && pOther->m_takedamage != DAMAGE_NO ) { // Check to see if this is a respawn room. if ( !pOther->IsPlayer() ) { CFuncRespawnRoom *pRespawnRoom = dynamic_cast( pOther ); if ( pRespawnRoom ) { if ( !pRespawnRoom->PointIsWithin( vOrigin ) ) return; } } if ( m_iType == TF_GL_MODE_CANNONBALL ) { // Damage the player to push them back CBaseEntity *pAttacker = GetThrower(); if ( pAttacker && ( pOther->IsPlayer() || pOther->IsBaseObject() ) ) { // check if we already penetrate through this victim if ( !m_penetratedEntities.HasElement( pOther ) ) { // Impact damage scales with distance float flDistanceSq = (pOther->GetAbsOrigin() - pAttacker->GetAbsOrigin()).LengthSqr(); float flImpactDamage = RemapValClamped( flDistanceSq, 512 * 512, 1024 * 1024, 50, 25 ); CTakeDamageInfo info( this, pAttacker, m_hLauncher, vec3_origin, vOrigin, flImpactDamage, GetDamageType(), TF_DMG_CUSTOM_CANNONBALL_PUSH ); pOther->TakeDamage( info ); CTFPlayer *pVictim = ToTFPlayer( pOther ); if ( pVictim ) { // apply airblast - Apply stun if they are effectively grounded so we can knock them up if ( !pVictim->m_Shared.InCond( TF_COND_KNOCKED_INTO_AIR ) ) { pVictim->m_Shared.StunPlayer( 0.5, 1.f, TF_STUN_MOVEMENT, ToTFPlayer( pAttacker ) ); } Vector vecToTarget = pVictim->WorldSpaceCenter() - pAttacker->WorldSpaceCenter(); VectorNormalize( vecToTarget ); vecToTarget *= 400; vecToTarget.z += 350; // Mimic Flamethrower AirBlast pVictim->ApplyAirBlastImpulse( vecToTarget ); } m_penetratedEntities.AddToTail( pOther ); EmitSound( TF_WEAPON_CANNON_IMPACT_SOUND ); // Add this guy to our donk list. If this grenade explodes and hits anyone on our launcher's // donk list, they get minicrit CTFGrenadeLauncher* pLauncher = dynamic_cast( GetLauncher() ); if( pLauncher ) { pLauncher->AddDonkVictim( pOther ); } } return; } } // Save this entity as enemy, they will take 100% damage. m_hEnemy = pOther; // Restore damage. See comment in CTFGrenadePipebombProjectile::Create() above to understand this. m_flDamage = m_flFullDamage; Explode( &pTrace, GetDamageType() ); bExploded = true; } // Train hack! if ( !bExploded && pOther->GetModelName() == s_iszTrainName && ( pOther->GetAbsVelocity().LengthSqr() > 1.0f ) ) { Explode( &pTrace, GetDamageType() ); bExploded = true; } // Explode on contact with a Boss, too if ( !bExploded && TFGameRules()->GetActiveBoss() && pOther == TFGameRules()->GetActiveBoss() ) { Explode( &pTrace, GetDamageType() ); bExploded = true; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- extern bool PropDynamic_CollidesWithGrenades( CBaseEntity* pBaseEntity ); void CTFGrenadePipebombProjectile::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) { BaseClass::VPhysicsCollision( index, pEvent ); int otherIndex = !index; CBaseEntity *pHitEntity = pEvent->pEntities[otherIndex]; if ( !pHitEntity ) return; if ( m_bWallShatter ) { Fizzle(); Detonate(); return; } if ( m_iType == TF_GL_MODE_REGULAR || m_iType == TF_GL_MODE_CANNONBALL ) { if ( PropDynamic_CollidesWithGrenades( pHitEntity) ) { if ( m_bTouched == false ) { SetThink( &CTFGrenadePipebombProjectile::Detonate ); SetNextThink( gpGlobals->curtime ); } } // Blow up if we hit an enemy we can damage else if ( pHitEntity->GetTeamNumber() && pHitEntity->GetTeamNumber() != GetTeamNumber() && pHitEntity->m_takedamage != DAMAGE_NO ) { SetThink( &CTFGrenadePipebombProjectile::Detonate ); SetNextThink( gpGlobals->curtime ); } if ( m_bTouched == false ) { SetDamage( GetDamageScaleOnWorldContact() * GetDamage() ); int iNoBounce = 0; if ( GetLauncher() ) { CALL_ATTRIB_HOOK_INT_ON_OTHER( GetLauncher(), iNoBounce, grenade_no_bounce ) if ( iNoBounce ) { Vector velocity; AngularImpulse angularVelocity; VPhysicsGetObject()->GetVelocity( &velocity, &angularVelocity ); velocity *= 0.1f; VPhysicsGetObject()->SetVelocity( &velocity, &angularVelocity ); } } } m_bTouched = true; return; } // Handle hitting skybox (disappear). surfacedata_t *pprops = physprops->GetSurfaceData( pEvent->surfaceProps[otherIndex] ); if ( pprops->game.material == 'X' ) { // uncomment to destroy grenade upon hitting sky brush //SetThink( &CTFGrenadePipebombProjectile::SUB_Remove ); //SetNextThink( gpGlobals->curtime ); return; } bool bIsDynamicProp = ( NULL != dynamic_cast( pHitEntity ) ); // Temp: Don't stick to the saw blades in sawmill. // We should make the saws their own entity type for networking. if ( FStrEq( pHitEntity->m_iParent.ToCStr(), "sawmovelinear01" ) || FStrEq( pHitEntity->m_iParent.ToCStr(), "sawmovelinear02" ) || PropDynamic_CollidesWithGrenades( pHitEntity) ) { bIsDynamicProp = false; } // Pipebombs stick to the world when they touch it if ( pHitEntity && ( pHitEntity->IsWorld() || bIsDynamicProp ) && gpGlobals->curtime > m_flMinSleepTime ) { m_bTouched = true; g_PostSimulationQueue.QueueCall( VPhysicsGetObject(), &IPhysicsObject::EnableMotion, false ); // Save impact data for explosions. m_bUseImpactNormal = true; pEvent->pInternalData->GetSurfaceNormal( m_vecImpactNormal ); m_vecImpactNormal.Negate(); m_flTouchedTime = gpGlobals->curtime; float flFizzle = 0; CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetLauncher(), flFizzle, stickybomb_fizzle_time ); if ( flFizzle > 0 ) { SetDetonateTimerLength( flFizzle ); } } } ConVar tf_grenade_forcefrom_bullet( "tf_grenade_forcefrom_bullet", "2.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); ConVar tf_grenade_forcefrom_buckshot( "tf_grenade_forcefrom_buckshot", "0.75", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); ConVar tf_grenade_forcefrom_blast( "tf_grenade_forcefrom_blast", "0.15", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); ConVar tf_grenade_force_sleeptime( "tf_grenade_force_sleeptime", "1.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); // How long after being shot will we re-stick to the world. ConVar tf_pipebomb_force_to_move( "tf_pipebomb_force_to_move", "1500.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); ConVar tf_pipebomb_deflect_reset_time( "tf_pipebomb_deflect_reset_time", "10.0", FCVAR_CHEAT | FCVAR_DEVELOPMENTONLY ); //----------------------------------------------------------------------------- // Purpose: If we are shot after being stuck to the world, move a bit //----------------------------------------------------------------------------- int CTFGrenadePipebombProjectile::OnTakeDamage( const CTakeDamageInfo &info ) { if ( !info.GetAttacker() ) { Assert( !info.GetAttacker() ); return 0; } bool bSameTeam = ( info.GetAttacker()->GetTeamNumber() == GetTeamNumber() ); if ( !bSameTeam && CanTakeDamage() ) { if ( m_bTouched && HasStickyEffects() && ( info.GetDamageType() & (DMG_BULLET|DMG_BUCKSHOT|DMG_BLAST|DMG_SONIC|DMG_MELEE) ) ) { Vector vecForce = info.GetDamageForce(); bool bBreakPipes = false; if ( info.GetDamageType() & (DMG_BULLET|DMG_MELEE) ) { vecForce *= tf_grenade_forcefrom_bullet.GetFloat(); bBreakPipes = true; } if ( info.GetDamageType() & DMG_SONIC ) { vecForce *= tf_grenade_forcefrom_bullet.GetFloat(); } else if ( info.GetDamageType() & DMG_BUCKSHOT ) { vecForce *= tf_grenade_forcefrom_buckshot.GetFloat(); bBreakPipes = true; } else if ( info.GetDamageType() & DMG_BLAST ) { vecForce *= tf_grenade_forcefrom_blast.GetFloat(); } if ( bBreakPipes == true ) { // we might get multiple calls for the same pipe when shooting it with a shotgun, // so make sure it only sends the player_destroyed_pipebomb event once if ( m_bSendPlayerDestroyedEvent ) { if ( info.GetAttacker()->IsPlayer() ) { CTFPlayer *pPlayer = ToTFPlayer( info.GetAttacker() ); if ( pPlayer ) { IGameEvent * event = gameeventmanager->CreateEvent( "player_destroyed_pipebomb" ); if ( event ) { event->SetInt( "userid", pPlayer->GetUserID() ); gameeventmanager->FireEvent( event ); } if ( pPlayer->IsPlayerClass( TF_CLASS_ENGINEER ) ) { // If we are near a building, award achievement progress. CTFTeam *pTeam = pPlayer->GetTFTeam(); if ( pTeam ) { for ( int i=0; iGetNumObjects(); i++ ) { CBaseObject *pObject = pTeam->GetObject(i); if ( pObject && pObject->GetAbsOrigin().DistTo( GetAbsOrigin() ) < 100 && pObject->ObjectType() != OBJ_ATTACHMENT_SAPPER ) { pPlayer->AwardAchievement( ACHIEVEMENT_TF_ENGINEER_DESTROY_STICKIES, 1 ); break; // Only one award per sticky. } } } } } m_bSendPlayerDestroyedEvent = false; } } Fizzle(); Detonate(); } // If the force is sufficient, detach & move the pipebomb float flForce = tf_pipebomb_force_to_move.GetFloat(); if ( vecForce.LengthSqr() > (flForce*flForce) ) { if ( VPhysicsGetObject() ) { VPhysicsGetObject()->EnableMotion( true ); } CTakeDamageInfo newInfo = info; newInfo.SetDamageForce( vecForce ); VPhysicsTakeDamage( newInfo ); // The pipebomb will re-stick to the ground after this time expires m_flMinSleepTime = gpGlobals->curtime + tf_grenade_force_sleeptime.GetFloat(); m_bTouched = false; // It has moved the data is no longer valid. m_bUseImpactNormal = false; m_vecImpactNormal.Init(); return 1; } } } return 0; } void CTFGrenadePipebombProjectile::IncrementDeflected( void ) { BaseClass::IncrementDeflected(); if ( GetDeflected() && HasStickyEffects() ) { m_flDeflectedTime = gpGlobals->curtime + tf_pipebomb_deflect_reset_time.GetFloat(); } int iTeamNumber = GetTeamNumber(); CTFPlayer *pOwner = ToTFPlayer( GetDeflectOwner() ); if ( pOwner ) { iTeamNumber = pOwner->GetTeamNumber(); } if ( !HasStickyEffects() ) { m_nSkin = ( iTeamNumber == TF_TEAM_BLUE ) ? 1 : 0; } } void CTFGrenadePipebombProjectile::DetonateThink( void ) { BaseClass::DetonateThink(); if ( m_flDeflectedTime <= gpGlobals->curtime && HasStickyEffects() ) { ResetDeflected(); SetDeflectOwner( NULL ); } // If we received our crit via a medic, make sure they still exist. if ( m_CritMedics.Count() ) { if ( TFGameRules() && ( TFGameRules()->InSetup() || TFGameRules()->State_Get() == GR_STATE_BETWEEN_RNDS ) ) { bool bRemove = true; FOR_EACH_VEC( m_CritMedics, i ) { if ( m_CritMedics[i] && m_CritMedics[i]->GetPlayerClass()->GetClassIndex() == TF_CLASS_MEDIC ) { bRemove = false; break; } } // No medic(s) if ( bRemove ) { Fizzle(); Detonate(); return; } } else { // Clear the vector when the game starts m_CritMedics.RemoveAll(); } } } void CTFGrenadePipebombProjectile::PreArmThink( void ) { SetContextThink( &CTFGrenadePipebombProjectile::ArmThink, gpGlobals->curtime + GetLiveTime(), "ARM_THINK" ); } void CTFGrenadePipebombProjectile::ArmThink( void ) { // When between waves in MvM, players sometimes switch to medic just so demos can place crit stickies, // and then switch back. This code removes the sticky if the medic switches ( in DetonateThink() ) if ( IsCritical() && HasStickyEffects() && TFGameRules() && ( TFGameRules()->InSetup() || TFGameRules()->State_Get() == GR_STATE_BETWEEN_RNDS ) ) { CTFPlayer *pOwner = ToTFPlayer( GetThrower() ); if ( pOwner && pOwner->m_Shared.InCond( TF_COND_CRITBOOSTED ) && !pOwner->m_Shared.InCond( TF_COND_CRITBOOSTED_USER_BUFF ) ) { // Find the medic(s) for ( int i = 0; i < pOwner->m_Shared.GetNumHealers(); i++ ) { CTFPlayer *pMedic = ToTFPlayer( pOwner->m_Shared.GetHealerByIndex( i ) ); if ( !pMedic ) continue; CWeaponMedigun *pMedigun = dynamic_cast ( pMedic->GetActiveTFWeapon() ); if ( pMedigun && pMedigun->IsReleasingCharge() && pMedigun->GetChargeType() == MEDIGUN_CHARGE_CRITICALBOOST ) { m_CritMedics.AddToTail( pMedic ); } } // We didn't find the medic. What provided TF_COND_CRITBOOSTED? Assert( m_CritMedics.Count() ); } } if ( m_bDetonateOnPulse ) { Detonate(); } } #endif //------------------------------------------------------------------------------ // Purpose: //----------------------------------------------------------------------------- float CTFGrenadePipebombProjectile::GetLiveTime( void ) { float flLiveTime = tf_grenadelauncher_livetime.GetFloat(); CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetLauncher(), flLiveTime, sticky_arm_time ); if ( TFGameRules() && TFGameRules()->IsPowerupMode() ) { CTFPlayer *pOwner = ToTFPlayer( GetThrower() ); if ( pOwner ) { if ( pOwner->m_Shared.GetCarryingRuneType() == RUNE_HASTE ) { flLiveTime *= 0.5f; } else if ( pOwner->m_Shared.GetCarryingRuneType() == RUNE_KING || pOwner->m_Shared.InCond( TF_COND_KING_BUFFED ) ) { flLiveTime *= 0.75f; } } } return flLiveTime; } #ifdef GAME_DLL //----------------------------------------------------------------------------- // Purpose: Grenade was deflected. //----------------------------------------------------------------------------- void CTFGrenadePipebombProjectile::Deflected( CBaseEntity *pDeflectedBy, Vector& vecDir ) { CTFPlayer *pTFDeflector = ToTFPlayer( pDeflectedBy ); if ( !pTFDeflector ) return; CTFPlayer* pOldOwner = NULL; if ( HasStickyEffects() ) { CTakeDamageInfo info; float flForceMultiplier = 1.0f; ITFChargeUpWeapon *pWeapon = dynamic_cast( pTFDeflector->GetActiveWeapon() ); if ( pWeapon ) { flForceMultiplier = RemapValClamped( ( gpGlobals->curtime - pWeapon->GetChargeBeginTime() ), 0.0f, pWeapon->GetChargeMaxTime(), 1.0f, 2.0f ); } Vector vecForce = vecDir * flForceMultiplier * -CTFWeaponBase::DeflectionForce( WorldAlignSize(), 90, 12.0f ); pOldOwner = ToTFPlayer( GetThrower() ); info.SetAttacker( pDeflectedBy ); info.SetDamageForce( vecForce ); info.SetDamageType( DMG_SONIC ); info.SetWeapon( pTFDeflector->GetActiveTFWeapon() ); OnTakeDamage( info ); } else { ChangeTeam( pTFDeflector->GetTeamNumber() ); SetLauncher( pTFDeflector->GetActiveWeapon() ); pOldOwner = ToTFPlayer( GetThrower() ); SetThrower( pTFDeflector ); if ( pTFDeflector->m_Shared.IsCritBoosted() ) { SetCritical( true ); } } if ( pOldOwner ) { pOldOwner->SpeakConceptIfAllowed( MP_CONCEPT_DEFLECTED, "projectile:1,victim:1" ); } CTFWeaponBase::SendObjectDeflectedEvent( pTFDeflector, pOldOwner, GetWeaponID(), this ); SetDeflectOwner( pTFDeflector ); IncrementDeflected(); } #endif #ifdef CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: Highlight FX //----------------------------------------------------------------------------- class CProxyStickybombGlowColor : public CResultProxy { public: void OnBind( void *pC_BaseEntity ) { Assert( m_pResult ); if ( !pC_BaseEntity ) { m_pResult->SetVecValue( 1, 1, 1 ); return; } CTFGrenadePipebombProjectile *pGrenade = NULL; C_BaseEntity *pEntity = BindArgToEntity( pC_BaseEntity ); if ( !pEntity ) { m_pResult->SetVecValue( 1, 1, 1 ); return; } // default to [1 1 1] Vector vResult = Vector( 1, 1, 1 ); pGrenade = dynamic_cast( pEntity ); if ( pGrenade ) { if ( pGrenade->IsHighlighted() ) { int iTeamNumber = pGrenade->GetTeamNumber(); if ( iTeamNumber == TF_TEAM_RED ) { vResult = Vector ( 100.f, 0.f, 0.f ); if ( pGrenade->m_pGlowEffect ) { pGrenade->m_pGlowEffect->SetColor( Vector( 250, 0, 0 ) ); } } else { vResult = Vector ( 0.f, 0.f, 100.f ); if ( pGrenade->m_pGlowEffect ) { pGrenade->m_pGlowEffect->SetColor( Vector( 0, 0, 250 ) ); } } } else { int iTeamNumber = pGrenade->GetTeamNumber(); if ( iTeamNumber == TF_TEAM_RED ) { if ( pGrenade->m_pGlowEffect ) { pGrenade->m_pGlowEffect->SetColor( Vector( 200, 100, 100 ) ); } } else { if ( pGrenade->m_pGlowEffect ) { pGrenade->m_pGlowEffect->SetColor( Vector( 100, 100, 200 ) ); } } } } m_pResult->SetVecValue( vResult.x, vResult.y, vResult.z ); } }; EXPOSE_INTERFACE( CProxyStickybombGlowColor, IMaterialProxy, "StickybombGlowColor" IMATERIAL_PROXY_INTERFACE_VERSION ); #endif //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- #if GAME_DLL int CTFGrenadePipebombProjectile::GetDamageCustom() { if ( m_iType == TF_GL_MODE_REMOTE_DETONATE ) { if ( !m_bTouched ) { return TF_DMG_CUSTOM_AIR_STICKY_BURST; } else if ( m_bDefensiveBomb ) { return TF_DMG_CUSTOM_DEFENSIVE_STICKY; } else { return TF_DMG_CUSTOM_STANDARD_STICKY; } } else if ( m_iType == TF_GL_MODE_REMOTE_DETONATE_PRACTICE ) { return TF_DMG_CUSTOM_PRACTICE_STICKY; } return BaseClass::GetDamageCustom(); } float CTFGrenadePipebombProjectile::GetDamageScaleOnWorldContact() { float flGrenadeDamageScaleOnWorldContact = 1.f; if ( GetLauncher() ) { CALL_ATTRIB_HOOK_FLOAT_ON_OTHER( GetLauncher(),flGrenadeDamageScaleOnWorldContact, grenade_damage_reduction_on_world_contact ); } return flGrenadeDamageScaleOnWorldContact; } #endif //----------------------------------------------------------------------------- float CTFGrenadePipebombProjectile::GetDamageRadius() { float flRadiusMod = 1.0f; #ifdef GAME_DLL // winbomb prevention. // Air Det if ( m_iType == TF_GL_MODE_REMOTE_DETONATE ) { if ( m_bTouched == false ) { float flArmTime = tf_grenadelauncher_livetime.GetFloat(); flRadiusMod *= RemapValClamped( gpGlobals->curtime - m_flCreationTime, flArmTime, flArmTime + tf_sticky_radius_ramp_time.GetFloat(), tf_sticky_airdet_radius.GetFloat(), 1.0 ); } } #endif // GAME_DLL return BaseClass::GetDamageRadius() * flRadiusMod; }