//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // //============================================================================= #include "cbase.h" #include "tf_weapon_bat.h" #include "decals.h" // Client specific. #ifdef CLIENT_DLL #include "c_basedoor.h" #include "c_tf_player.h" #include "IEffects.h" #include "bone_setup.h" #include "c_tf_gamestats.h" // Server specific. #else #include "doors.h" #include "tf_player.h" #include "tf_ammo_pack.h" #include "tf_gamestats.h" #include "ilagcompensationmanager.h" #include "collisionutils.h" #include "particle_parse.h" #include "tf_projectile_base.h" #include "tf_gamerules.h" #endif const float DEFAULT_ORNAMENT_EXPLODE_RADIUS = 50.0f; const float DEFAULT_ORNAMENT_EXPLODE_DAMAGE_MULT = 0.9f; //============================================================================= // // Weapon Bat tables. // // TFBat -- IMPLEMENT_NETWORKCLASS_ALIASED( TFBat, DT_TFWeaponBat ) BEGIN_NETWORK_TABLE( CTFBat, DT_TFWeaponBat ) END_NETWORK_TABLE() BEGIN_PREDICTION_DATA( CTFBat ) END_PREDICTION_DATA() LINK_ENTITY_TO_CLASS( tf_weapon_bat, CTFBat ); PRECACHE_WEAPON_REGISTER( tf_weapon_bat ); // -- TFBat // TFBat_Fish -- IMPLEMENT_NETWORKCLASS_ALIASED( TFBat_Fish, DT_TFWeaponBat_Fish ) BEGIN_NETWORK_TABLE( CTFBat_Fish, DT_TFWeaponBat_Fish ) END_NETWORK_TABLE() BEGIN_PREDICTION_DATA( CTFBat_Fish ) END_PREDICTION_DATA() LINK_ENTITY_TO_CLASS( tf_weapon_bat_fish, CTFBat_Fish ); PRECACHE_WEAPON_REGISTER( tf_weapon_bat_fish ); // -- TFBat_Fish // TFBat_Wood -- IMPLEMENT_NETWORKCLASS_ALIASED( TFBat_Wood, DT_TFWeaponBat_Wood ) BEGIN_NETWORK_TABLE( CTFBat_Wood, DT_TFWeaponBat_Wood ) END_NETWORK_TABLE() BEGIN_PREDICTION_DATA( CTFBat_Wood ) END_PREDICTION_DATA() LINK_ENTITY_TO_CLASS( tf_weapon_bat_wood, CTFBat_Wood ); PRECACHE_WEAPON_REGISTER( tf_weapon_bat_wood ); // -- TFBat_Wood // CTFBat_Giftwrap -- IMPLEMENT_NETWORKCLASS_ALIASED( TFBat_Giftwrap, DT_TFWeaponBat_Giftwrap ) BEGIN_NETWORK_TABLE( CTFBat_Giftwrap, DT_TFWeaponBat_Giftwrap ) END_NETWORK_TABLE() BEGIN_PREDICTION_DATA( CTFBat_Giftwrap ) END_PREDICTION_DATA() LINK_ENTITY_TO_CLASS( tf_weapon_bat_giftwrap, CTFBat_Giftwrap ); PRECACHE_WEAPON_REGISTER( tf_weapon_bat_giftwrap ); // -- CTFBat_Giftwrap // TFStunBall -- IMPLEMENT_NETWORKCLASS_ALIASED( TFStunBall, DT_TFProjectile_StunBall ) BEGIN_NETWORK_TABLE( CTFStunBall, DT_TFProjectile_StunBall ) END_NETWORK_TABLE() LINK_ENTITY_TO_CLASS( tf_projectile_stun_ball, CTFStunBall ); PRECACHE_WEAPON_REGISTER( tf_projectile_stun_ball ); #define TF_WEAPON_STUNBALL_VM_MODEL "models/weapons/v_models/v_baseball.mdl" #define TF_WEAPON_STUNBALL_MODEL "models/weapons/w_models/w_baseball.mdl" #if defined( GAME_DLL ) ConVar tf_scout_stunball_base_duration( "tf_scout_stunball_base_duration", "6.0", FCVAR_DEVELOPMENTONLY ); ConVar tf_scout_stunball_base_speed( "tf_scout_stunball_base_speed", "3000", FCVAR_DEVELOPMENTONLY ); ConVar sv_proj_stunball_damage( "sv_proj_stunball_damage", "15", FCVAR_DEVELOPMENTONLY ); #endif // -- TFStunBall // CTFBall_Ornament -- IMPLEMENT_NETWORKCLASS_ALIASED( TFBall_Ornament, DT_TFProjectileBall_Ornament ) BEGIN_NETWORK_TABLE( CTFBall_Ornament, DT_TFProjectileBall_Ornament ) END_NETWORK_TABLE() LINK_ENTITY_TO_CLASS( tf_projectile_ball_ornament, CTFBall_Ornament ); PRECACHE_WEAPON_REGISTER( tf_projectile_ball_ornament ); #define TF_WEAPON_BALL_ORNAMENT_VM_MODEL "models/weapons/c_models/c_xms_festive_ornament.mdl" #define TF_WEAPON_BALL_ORNAMENT_MODEL "models/weapons/c_models/c_xms_festive_ornament.mdl" #if defined( GAME_DLL ) //ConVar tf_scout_stunball_base_duration( "tf_scout_stunball_base_duration", "6.0", FCVAR_DEVELOPMENTONLY ); #endif // -- CTFBall_Ornament static string_t s_iszTrainName; //============================================================================= #define STUNBALL_TRAIL_ALPHA 128 //============================================================================= // // CTFBat // //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CTFBat::CTFBat() { } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBat::Smack( void ) { BaseClass::Smack(); #ifdef GAME_DLL if ( BatDeflects() ) { #ifdef TF_RAID_MODE if ( TFGameRules()->IsRaidMode() ) { } else #endif // TF_RAID_MODE { DeflectProjectiles(); } } #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBat::PlayDeflectionSound( bool bPlayer ) { WeaponSound( MELEE_HIT_WORLD ); } //============================================================================= // // CTFBat_Wood // CTFBat_Wood::CTFBat_Wood() { m_iEnemyBallID = 0; #ifdef CLIENT_DLL m_hStunBallVM = NULL; #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- ConVar tf_scout_bat_launch_delay( "tf_scout_bat_launch_delay", "0.1", FCVAR_REPLICATED | FCVAR_DEVELOPMENTONLY ); //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBat_Wood::LaunchBallThink( void ) { CTFPlayer *pPlayer = GetTFPlayerOwner(); if ( !pPlayer ) return; LaunchBall(); #ifdef GAME_DLL pPlayer->SpeakWeaponFire( MP_CONCEPT_BAT_BALL ); CTF_GameStats.Event_PlayerFiredWeapon( pPlayer, IsCurrentAttackACrit() ); #endif #ifdef CLIENT_DLL C_CTF_GameStats.Event_PlayerFiredWeapon( pPlayer, IsCurrentAttackACrit() ); #endif } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBat_Wood::SecondaryAttackAnim( CTFPlayer *pPlayer ) { pPlayer->DoAnimationEvent( PLAYERANIMEVENT_ATTACK_SECONDARY ); } // SERVER ONLY -- #ifdef GAME_DLL //----------------------------------------------------------------------------- // Purpose: Calculate the ball's initial position, angle, and velocity. //----------------------------------------------------------------------------- void CTFBat_Wood::GetBallDynamics( Vector& vecLoc, QAngle& vecAngles, Vector& vecVelocity, AngularImpulse& angImpulse, CTFPlayer* pPlayer ) { Vector vecForward, vecUp; AngleVectors( pPlayer->EyeAngles(), &vecForward, NULL, &vecUp ); vecLoc = pPlayer->GetAbsOrigin() + pPlayer->GetModelScale() * ( Vector( 0, 0, 50 ) + vecForward * 32.f ); vecAngles = pPlayer->GetAbsAngles(); // Calculate the initial impulse on the item. vecVelocity = Vector( 0.0f, 0.0f, 0.0f ); vecVelocity += vecForward * 10; vecVelocity += vecUp * 1; VectorNormalize( vecVelocity ); vecVelocity *= tf_scout_stunball_base_speed.GetInt(); angImpulse = AngularImpulse( 0, random->RandomFloat( 0, 100 ), 0 ); } // -- SERVER ONLY #endif //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBat_Wood::SecondaryAttack( void ) { CTFPlayer *pPlayer = GetTFPlayerOwner(); if ( !pPlayer ) return; if ( !CanAttack() ) return; if ( m_flNextPrimaryAttack > gpGlobals->curtime ) return; // Do we have any balls? If so, use them. int iBallCount = pPlayer->GetAmmoCount( TF_AMMO_GRENADES1 ); if ( (iBallCount > 0) && CanCreateBall( pPlayer ) ) { SecondaryAttackAnim( pPlayer ); SendWeaponAnim( ACT_VM_PRIMARYATTACK ); SetContextThink( &CTFBat_Wood::LaunchBallThink, gpGlobals->curtime + tf_scout_bat_launch_delay.GetFloat(), "LAUNCH_BALL_THINK" ); m_flNextPrimaryAttack = gpGlobals->curtime + 0.25; #ifdef GAME_DLL if ( pPlayer->m_Shared.IsStealthed() ) { pPlayer->RemoveInvisibility(); } #endif // GAME_DLL } } //----------------------------------------------------------------------------- // Purpose: Client Only. Show the stunball view model if necessary. //----------------------------------------------------------------------------- #ifdef CLIENT_DLL void CTFBat_Wood::SetWeaponVisible( bool visible ) { BaseClass::SetWeaponVisible( visible ); if ( !m_hStunBallVM ) return; if ( visible ) { m_hStunBallVM->RemoveEffects( EF_NODRAW ); } else { m_hStunBallVM->AddEffects( EF_NODRAW ); } } #endif #ifdef CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBat_Wood::OnDataChanged( DataUpdateType_t updateType ) { BaseClass::OnDataChanged( updateType ); bool bLocalPlayerAmmo = true; if ( GetPlayerOwner() == C_BasePlayer::GetLocalPlayer() ) { bLocalPlayerAmmo = GetPlayerOwner()->GetAmmoCount( TF_AMMO_GRENADES1 ) > 0; } if ( IsCarrierAlive() && ( WeaponState() == WEAPON_IS_ACTIVE ) && bLocalPlayerAmmo == true ) { AddBallChild(); } else { RemoveBallChild(); } } //----------------------------------------------------------------------------- // Purpose: Client Only. Show the stunball view model if necessary. //----------------------------------------------------------------------------- void CTFBat_Wood::AddBallChild( void ) { CTFPlayer *pPlayer = GetTFPlayerOwner(); if ( !pPlayer ) return; if ( !pPlayer->IsLocalPlayer() ) return; if ( !pPlayer->GetViewModel() ) return; if ( m_hStunBallVM ) return; CTFViewModel* pBall = new class CTFViewModel(); if ( pBall != NULL ) { pBall->InitializeAsClientEntity( GetBallViewModelName(), RENDER_GROUP_OPAQUE_ENTITY ); pBall->SetAbsOrigin( pPlayer->GetViewModel()->GetAbsOrigin() ); pBall->SetModel( GetBallViewModelName() ); pBall->m_nSkin = ( pPlayer->GetTeamNumber() == TF_TEAM_BLUE ) ? 1 : 0; CStudioHdr *pStudioHdr = pPlayer->GetViewModel()->GetModelPtr(); if ( pStudioHdr ) { int iAttachment = Studio_FindAttachment( pStudioHdr, "weapon_bone_L" ) + 1; pBall->SetParent( pPlayer->GetViewModel(), iAttachment ); } pBall->AddEffects( EF_BONEMERGE ); pBall->SetMoveType( MOVETYPE_NONE ); pBall->AddSolidFlags( FSOLID_NOT_SOLID ); m_hStunBallVM.Set( pBall ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBat_Wood::Drop( const Vector &vecVelocity ) { BaseClass::Drop( vecVelocity ); RemoveBallChild(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBat_Wood::WeaponReset( void ) { RemoveBallChild(); BaseClass::WeaponReset(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBat_Wood::UpdateOnRemove( void ) { RemoveBallChild(); BaseClass::UpdateOnRemove(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBat_Wood::RemoveBallChild() { if ( m_hStunBallVM ) { m_hStunBallVM->Remove(); m_hStunBallVM = NULL; } } #endif //----------------------------------------------------------------------------- // Purpose: Determines if there is space to create a ball. //----------------------------------------------------------------------------- bool CTFBat_Wood::CanCreateBall( CTFPlayer* pPlayer ) { int iWeaponMod = 0; CALL_ATTRIB_HOOK_INT( iWeaponMod, set_weapon_mode ); if ( iWeaponMod == 0 ) return false; if ( pPlayer->GetWaterLevel() == WL_Eyes ) return false; Vector vecForward, vecUp; AngleVectors( pPlayer->EyeAngles(), &vecForward, NULL, &vecUp ); Vector vecBallStart = pPlayer->GetAbsOrigin() + Vector( 0, 0, 50 ); Vector vecBallEnd = vecBallStart + vecForward * 32.f; // Trace out and see if we hit a wall. trace_t trace; CTraceFilterSimple traceFilter( this, COLLISION_GROUP_NONE ); UTIL_TraceHull( vecBallStart, vecBallEnd, -Vector(8,8,8), Vector(8,8,8), MASK_SOLID_BRUSHONLY, &traceFilter, &trace ); if ( trace.DidHitWorld() || trace.startsolid ) return false; else { if ( trace.m_pEnt ) { // Don't let the player bat through doors. CBaseDoor *pDoor = dynamic_cast( trace.m_pEnt ); if ( pDoor ) return false; } return true; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBat_Wood::LaunchBall( void ) { CTFPlayer *pPlayer = GetTFPlayerOwner(); if ( !pPlayer ) return; #if GAME_DLL // Make a ball. CBaseEntity* pBall = CreateBall(); if ( !pBall ) return; if ( IsCurrentAttackACrit() ) { WeaponSound( BURST ); } WeaponSound( SPECIAL2 ); pPlayer->RemoveAmmo( 1, TF_AMMO_GRENADES1 ); #endif StartEffectBarRegen(); } // SERVER ONLY -- #ifdef GAME_DLL //----------------------------------------------------------------------------- // Purpose: The wooden bat creates a baseball that stuns whomever it hits. //----------------------------------------------------------------------------- CBaseEntity* CTFBat_Wood::CreateBall( void ) { CTFPlayer *pPlayer = GetTFPlayerOwner(); if ( !pPlayer ) return NULL; // Do another check here, as the player may have moved to an invalid position // since the first check (0.1 seconds ago). This fixes the ball sometimes // going through thin geometry, such as windows and spawn blockers. if ( !CanCreateBall( pPlayer ) ) return NULL; // Determine the ball's initial location, angles, and velocity. Vector vecLocation, vecVelocity; QAngle vecAngles; AngularImpulse angImpulse; GetBallDynamics( vecLocation, vecAngles, vecVelocity, angImpulse, pPlayer ); // Create a stun ball. CTFStunBall* pBall = CTFStunBall::Create( vecLocation, vecAngles, pPlayer ); Assert( pBall ); if ( !pBall ) return NULL; CalcIsAttackCritical(); pBall->m_iOriginalOwnerID = m_iEnemyBallID; m_iEnemyBallID = 0; pBall->SetCritical( IsCurrentAttackACrit() ); pBall->InitGrenade( vecVelocity, angImpulse, pPlayer, GetTFWpnData() ); pBall->SetLauncher( this ); pBall->SetOwnerEntity( pPlayer ); pBall->SetInitialSpeed( tf_scout_stunball_base_speed.GetInt() ); return pBall; } // -- SERVER ONLY #endif //----------------------------------------------------------------------------- // Purpose: Play pickup anim when we grab a new ball. //----------------------------------------------------------------------------- void CTFBat_Wood::PickedUpBall( void ) { if ( WeaponState() == WEAPON_IS_ACTIVE ) { SendWeaponAnim( ACT_VM_PULLBACK_SPECIAL ); } } //----------------------------------------------------------------------------- // Purpose: Play animation appropriate to ball status. //----------------------------------------------------------------------------- bool CTFBat_Wood::SendWeaponAnim( int iActivity ) { CTFPlayer *pPlayer = GetTFPlayerOwner(); if ( !pPlayer ) return BaseClass::SendWeaponAnim( iActivity ); if ( pPlayer->GetAmmoCount( TF_AMMO_GRENADES1 ) > 0 ) { switch ( iActivity ) { case ACT_VM_DRAW: iActivity = ACT_VM_DRAW_SPECIAL; break; case ACT_VM_HOLSTER: iActivity = ACT_VM_HOLSTER_SPECIAL; break; case ACT_VM_IDLE: iActivity = ACT_VM_IDLE_SPECIAL; break; case ACT_VM_PULLBACK: iActivity = ACT_VM_PULLBACK_SPECIAL; break; case ACT_VM_PRIMARYATTACK: iActivity = ACT_VM_PRIMARYATTACK_SPECIAL; break; case ACT_VM_SECONDARYATTACK: iActivity = ACT_VM_PRIMARYATTACK_SPECIAL; break; case ACT_VM_HITCENTER: iActivity = ACT_VM_HITCENTER_SPECIAL; break; case ACT_VM_SWINGHARD: iActivity = ACT_VM_SWINGHARD_SPECIAL; break; case ACT_VM_IDLE_TO_LOWERED: iActivity = ACT_VM_IDLE_TO_LOWERED_SPECIAL; break; case ACT_VM_IDLE_LOWERED: iActivity = ACT_VM_IDLE_LOWERED_SPECIAL; break; case ACT_VM_LOWERED_TO_IDLE: iActivity = ACT_VM_LOWERED_TO_IDLE_SPECIAL; break; default: break; } } return BaseClass::SendWeaponAnim( iActivity ); } //============================================================================= // // CTFStunBall // // SERVER ONLY -- #ifdef GAME_DLL CTFStunBall::CTFStunBall() { s_iszTrainName = AllocPooledString( "models/props_vehicles/train_enginecar.mdl" ); m_iOriginalOwnerID = 0; m_pBallTrail = NULL; m_flBallTrailLife = 1.0f; } //----------------------------------------------------------------------------- // Purpose: Static entity factory. //----------------------------------------------------------------------------- CTFStunBall* CTFStunBall::Create( const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity *pOwner ) { CTFStunBall* pBall = static_cast( CBaseAnimating::CreateNoSpawn( "tf_projectile_stun_ball", vecOrigin, vecAngles, pOwner ) ); if ( pBall ) { DispatchSpawn( pBall ); } return pBall; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFStunBall::Precache( void ) { PrecacheModel( GetBallModelName() ); PrecacheModel( GetBallViewModelName() ); PrecacheModel( "effects/baseballtrail_red.vmt" ); PrecacheModel( "effects/baseballtrail_blu.vmt" ); BaseClass::Precache(); } //----------------------------------------------------------------------------- const char *CTFStunBall::GetBallModelName( void ) const { return TF_WEAPON_STUNBALL_MODEL; } //----------------------------------------------------------------------------- const char *CTFStunBall::GetBallViewModelName( void ) const { return TF_WEAPON_STUNBALL_VM_MODEL; } //----------------------------------------------------------------------------- // Purpose: Sets up initial properties. //----------------------------------------------------------------------------- void CTFStunBall::Spawn( void ) { BaseClass::Spawn(); SetModel( GetBallModelName() ); VPhysicsDestroyObject(); VPhysicsInitNormal( SOLID_BBOX, 0, false ); AddSolidFlags( FSOLID_TRIGGER ); AddFlag( FL_GRENADE ); SetCollisionGroup( COLLISION_GROUP_PROJECTILE ); m_takedamage = DAMAGE_NO; SetContextThink( &CBaseEntity::SUB_Remove, gpGlobals->curtime + 15, "DieContext" ); // Draw the trail for the Baseball on spawn if ( !m_pBallTrail ) { const char *pTrailTeamName = ( GetTeamNumber() == TF_TEAM_RED ) ? "effects/baseballtrail_red.vmt" : "effects/baseballtrail_blu.vmt"; CSpriteTrail *pTempTrail = NULL; pTempTrail = CSpriteTrail::SpriteTrailCreate( pTrailTeamName, GetAbsOrigin(), true ); pTempTrail->FollowEntity( this ); pTempTrail->SetTransparency( kRenderTransAlpha, 255, 255, 255, STUNBALL_TRAIL_ALPHA, kRenderFxNone ); pTempTrail->SetStartWidth( 9 ); pTempTrail->SetTextureResolution( 1.0f / ( 96.0f * 1.0f ) ); pTempTrail->SetLifeTime( 0.4 ); pTempTrail->TurnOn(); pTempTrail->SetAttachment( this, 0 ); m_pBallTrail = pTempTrail; SetContextThink( &CTFStunBall::RemoveBallTrail, gpGlobals->curtime + 3, "FadeBallTrail"); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFStunBall::Explode( trace_t *pTrace, int bitsDamageType ) { if ( !IsAllowedToExplode() ) return; BaseClass::Explode( pTrace, bitsDamageType ); } //----------------------------------------------------------------------------- // Purpose: Stun the person we smashed into. //----------------------------------------------------------------------------- #define FLIGHT_TIME_TO_MAX_STUN 1.f void CTFStunBall::ApplyBallImpactEffectOnVictim( CBaseEntity *pOther ) { if ( !pOther || !pOther->IsPlayer() ) return; CTFPlayer* pPlayer = ToTFPlayer( pOther ); if ( !pPlayer ) return; CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; if ( m_bTouched ) return; // Can't stun an invul player. if ( pPlayer->m_Shared.IsInvulnerable() || pPlayer->m_Shared.InCond( TF_COND_INVULNERABLE_WEARINGOFF ) ) return; // We have a more intense stun based on our travel time. float flLifeTime = MIN( gpGlobals->curtime - m_flCreationTime, FLIGHT_TIME_TO_MAX_STUN ); float flLifeTimeRatio = flLifeTime / FLIGHT_TIME_TO_MAX_STUN; if ( flLifeTimeRatio > 0.1f ) { float flStun = 0.5f; float flStunDuration = tf_scout_stunball_base_duration.GetFloat() * flLifeTimeRatio; if ( IsCritical() ) flStunDuration += 2.0; // Extra two seconds of effect time if we're a critical hit. int iStunFlags = TF_STUN_LOSER_STATE | TF_STUN_MOVEMENT; if ( flLifeTimeRatio >= 1.f ) { flStunDuration += 1.0; iStunFlags = TF_STUN_CONTROLS; iStunFlags |= TF_STUN_SPECIAL_SOUND; CTF_GameStats.Event_PlayerStunBall( pOwner, true ); } else { CTF_GameStats.Event_PlayerStunBall( pOwner, false ); } // Adjust stun amount and flags if we're hitting a boss or scaled enemy if ( TFGameRules() && TFGameRules()->GameModeUsesMiniBosses() && ( pPlayer->IsMiniBoss() || pPlayer->GetModelScale() > 1.0f ) ) { // If max range, freeze them in place - otherwise adjust it based on distance flStun = flLifeTimeRatio >= 1.f ? 1.f : RemapValClamped( flLifeTimeRatio, 0.1f, 0.99f, 0.5f, 0.75 ); iStunFlags = flLifeTimeRatio >= 1.f ? ( TF_STUN_SPECIAL_SOUND | TF_STUN_MOVEMENT ) : TF_STUN_MOVEMENT; } if ( pPlayer->GetWaterLevel() != WL_Eyes ) { pPlayer->m_Shared.StunPlayer( flStunDuration, flStun, iStunFlags, pOwner ); if ( pPlayer->GetUserID() == m_iOriginalOwnerID ) { // Holy crap! We just stunned a scout with their own ball. // Give the player an achievement for this. if ( pOwner->IsPlayerClass( TF_CLASS_SCOUT ) ) { pOwner->AwardAchievement( ACHIEVEMENT_TF_SCOUT_STUN_SCOUT_WITH_THEIR_BALL ); } } } } // Give 'em a love tap. const trace_t *pTrace = &CBaseEntity::GetTouchTrace(); trace_t *pNewTrace = const_cast( pTrace ); CBaseEntity *pInflictor = GetLauncher(); CTakeDamageInfo info; info.SetAttacker( GetOwnerEntity() ); info.SetInflictor( pInflictor ); info.SetWeapon( pInflictor ); info.SetDamage( GetDamage() ); info.SetDamageCustom( TF_DMG_CUSTOM_BASEBALL ); info.SetDamageForce( GetDamageForce() ); info.SetDamagePosition( GetAbsOrigin() ); int iDamageType = GetDamageType(); if ( IsCritical() ) iDamageType |= DMG_CRITICAL; info.SetDamageType( iDamageType ); // Hurt 'em. Vector dir; AngleVectors( GetAbsAngles(), &dir ); pPlayer->DispatchTraceAttack( info, dir, pNewTrace ); ApplyMultiDamage(); // Make this ball fade faster now that it's hit something. SetContextThink( &CBaseEntity::SUB_Remove, gpGlobals->curtime + 4, "DieContext" ); m_bTouched = true; } float CTFStunBall::GetDamage( void ) { return sv_proj_stunball_damage.GetFloat(); } Vector CTFStunBall::GetDamageForce( void ) { Vector vecVelocity = GetAbsVelocity(); IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); if ( pPhysicsObject ) { pPhysicsObject->GetVelocity( &vecVelocity, NULL ); VectorNormalize( vecVelocity ); } return (vecVelocity * GetDamage()); } //----------------------------------------------------------------------------- // Purpose: We hit something. //----------------------------------------------------------------------------- void CTFStunBall::PipebombTouch( CBaseEntity *pOther ) { CTFPlayer* pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; if ( !pOther || !pOther->IsSolid() || pOther->IsSolidFlagSet( FSOLID_VOLUME_CONTENTS ) ) { pOwner->SpeakConceptIfAllowed( MP_CONCEPT_BALL_MISSED ); return; } // Go away if we're hit by a moving train. if ( pOther->GetModelName() == s_iszTrainName && ( pOther->GetAbsVelocity().LengthSqr() > 1.0f ) ) { UTIL_Remove( this ); return; } // Go away if we hit the skybox. trace_t pTrace; Vector velDir = GetAbsVelocity(); VectorNormalize( velDir ); Vector vecSpot = GetAbsOrigin() - 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; } // Ignore things that aren't players. if ( !pOther->IsPlayer() ) return; // If we hit a scout, pickup as ammo if ( m_bTouched ) { CTFPlayer* pPlayer = ToTFPlayer( pOther ); if ( pPlayer && pPlayer->IsPlayerClass( TF_CLASS_SCOUT ) && (pPlayer->GetAmmoCount( TF_AMMO_GRENADES1 ) < pPlayer->GetMaxAmmo( TF_AMMO_GRENADES1 )) ) { pPlayer->GiveAmmo( 1, TF_AMMO_GRENADES1 ); RemoveBallTrail(); UTIL_Remove( this ); CTFBat_Wood *pBat = (CTFBat_Wood *) pPlayer->Weapon_OwnsThisID( TF_WEAPON_BAT_WOOD ); if ( pBat ) { // If this ball came from an enemy scout, remember who they were... if ( pPlayer->GetTeamNumber() != GetTeamNumber() ) { if ( pOwner ) { pBat->m_iEnemyBallID = pOwner->GetUserID(); } } // If we have the bat up, we need to play the correct anim. pBat->PickedUpBall(); } // Say something. pPlayer->SpeakConceptIfAllowed( MP_CONCEPT_GRAB_BALL, (pOther->GetTeamNumber() == GetTeamNumber()) ? "my_team:1" : "my_team:0" ); } return; } if ( pOther == GetThrower() ) return; if ( !InSameTeam( pOther ) && pOther->m_takedamage != DAMAGE_NO ) { ApplyBallImpactEffectOnVictim( pOther ); } } //----------------------------------------------------------------------------- // Purpose: We hit something. //----------------------------------------------------------------------------- void CTFStunBall::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) { CTFPlayer* pOwner = ToTFPlayer( GetOwnerEntity() ); bool bWasTouched = m_bTouched; BaseClass::VPhysicsCollision( index, pEvent ); if ( pOwner && !bWasTouched && m_bTouched ) { pOwner->SpeakConceptIfAllowed( MP_CONCEPT_BALL_MISSED ); } } //----------------------------------------------------------------------------- // Purpose: Fade and kill the trail //----------------------------------------------------------------------------- void CTFStunBall::RemoveBallTrail( void ) { if (!m_pBallTrail) return; if (m_pBallTrail) { if (m_flBallTrailLife <= 0) { UTIL_Remove( m_pBallTrail); m_flBallTrailLife = 1.0f; } else { float fAlpha = STUNBALL_TRAIL_ALPHA * m_flBallTrailLife; CSpriteTrail *pTempTrail = dynamic_cast< CSpriteTrail*>( m_pBallTrail.Get() ); if ( pTempTrail ) { pTempTrail->SetBrightness( int(fAlpha) ); } m_flBallTrailLife = m_flBallTrailLife - 0.1f; SetContextThink( &CTFStunBall::RemoveBallTrail, gpGlobals->curtime + 0.05, "FadeBallTrail"); } } } // -- SERVER ONLY #endif #ifdef CLIENT_DLL //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const char *CTFStunBall::GetTrailParticleName( void ) { int iTeamNumber = GetTeamNumber(); if ( GetDeflected() ) { CTFPlayer *pOwner = ToTFPlayer( GetDeflectOwner() ); if ( pOwner ) { iTeamNumber = pOwner->GetTeamNumber(); } } if ( iTeamNumber == TF_TEAM_BLUE ) { return "stunballtrail_blue"; } else { return "stunballtrail_red"; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFStunBall::CreateTrailParticles( void ) { if ( pEffectTrail ) { ParticleProp()->StopEmission( pEffectTrail ); } if ( pEffectCrit ) { ParticleProp()->StopEmission( pEffectCrit ); } pEffectTrail = ParticleProp()->Create( GetTrailParticleName(), PATTACH_ABSORIGIN_FOLLOW ); int iTeamNumber = GetTeamNumber(); if ( GetDeflected() ) { CTFPlayer *pOwner = ToTFPlayer( GetDeflectOwner() ); if ( pOwner ) { iTeamNumber = pOwner->GetTeamNumber(); } } if ( m_bCritical ) { if ( iTeamNumber == TF_TEAM_BLUE ) { pEffectCrit = ParticleProp()->Create( "stunballtrail_blue_crit", PATTACH_ABSORIGIN_FOLLOW ); } else { pEffectCrit = ParticleProp()->Create( "stunballtrail_red_crit", PATTACH_ABSORIGIN_FOLLOW ); } } } #endif //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBat_Giftwrap::Spawn( void ) { BaseClass::Spawn(); m_nSkin = ( GetTeamNumber() == TF_TEAM_BLUE ) ? 1 : 0; } #ifdef GAME_DLL //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CBaseEntity *CTFBat_Giftwrap::CreateBall( void ) { CTFPlayer *pPlayer = GetTFPlayerOwner(); if ( !pPlayer ) return NULL; // Do another check here, as the player may have moved to an invalid position // since the first check (0.1 seconds ago). This fixes the ball sometimes // going through thin geometry, such as windows and spawn blockers. if ( !CanCreateBall( pPlayer ) ) return NULL; // Determine the ball's initial location, angles, and velocity. Vector vecLocation, vecVelocity; QAngle vecAngles; AngularImpulse angImpulse; GetBallDynamics( vecLocation, vecAngles, vecVelocity, angImpulse, pPlayer ); // Create the ornament ball. CTFBall_Ornament *pBall = CTFBall_Ornament::Create( vecLocation, vecAngles, pPlayer ); Assert( pBall ); if ( !pBall ) return NULL; CalcIsAttackCritical(); pBall->m_iOriginalOwnerID = m_iEnemyBallID; m_iEnemyBallID = 0; pBall->SetCritical( IsCurrentAttackACrit() ); pBall->InitGrenade( vecVelocity, angImpulse, pPlayer, GetTFWpnData() ); pBall->SetLauncher( this ); pBall->SetOwnerEntity( pPlayer ); pBall->SetInitialSpeed( tf_scout_stunball_base_speed.GetInt() ); pBall->m_nSkin = ( pPlayer->GetTeamNumber() == TF_TEAM_BLUE ) ? 1 : 0; return pBall; } //----------------------------------------------------------------------------- void CTFBall_Ornament::Precache( void ) { PrecacheScriptSound( "BallBuster.OrnamentImpactRange" ); PrecacheScriptSound( "BallBuster.OrnamentImpact" ); PrecacheScriptSound( "BallBuster.HitBall" ); PrecacheScriptSound( "BallBuster.HitFlesh" ); PrecacheScriptSound( "BallBuster.HitWorld" ); PrecacheScriptSound( "BallBuster.DrawCatch" ); PrecacheScriptSound( "BallBuster.Ornament_DrawCatch" ); PrecacheScriptSound( "BallBuster.Ball_HitWorld" ); BaseClass::Precache(); } //----------------------------------------------------------------------------- CTFBall_Ornament *CTFBall_Ornament::Create( const Vector &vecOrigin, const QAngle &vecAngles, CBaseEntity *pOwner ) { CTFBall_Ornament* pBall = static_cast< CTFBall_Ornament * >( CBaseAnimating::CreateNoSpawn( "tf_projectile_ball_ornament", vecOrigin, vecAngles, pOwner ) ); if ( pBall ) { DispatchSpawn( pBall ); } return pBall; } //----------------------------------------------------------------------------- const char *CTFBall_Ornament::GetBallModelName( void ) const { return TF_WEAPON_BALL_ORNAMENT_MODEL; } //----------------------------------------------------------------------------- const char *CTFBall_Ornament::GetBallViewModelName( void ) const { return TF_WEAPON_BALL_ORNAMENT_VM_MODEL; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CTFBall_Ornament::ApplyBallImpactEffectOnVictim( CBaseEntity *pOther ) { if ( !pOther || !pOther->IsPlayer() ) return; CTFPlayer* pPlayer = ToTFPlayer( pOther ); if ( !pPlayer ) return; CTFPlayer *pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; if ( m_bTouched ) return; // Can't bleed an invul player. if ( pPlayer->m_Shared.IsInvulnerable() || pPlayer->m_Shared.InCond( TF_COND_INVULNERABLE_WEARINGOFF ) ) return; bool bIsCriticalHit = IsCritical(); float flBleedTime = 5.0f; bool bIsLongRangeHit = false; // long distance hit is always a crit float flLifeTime = gpGlobals->curtime - m_flCreationTime; if ( flLifeTime >= FLIGHT_TIME_TO_MAX_STUN ) { bIsCriticalHit = true; bIsLongRangeHit = true; } // just do the bleed effect directly since the bleed // attribute comes from the inflictor, which is the bat. pPlayer->m_Shared.MakeBleed( pOwner, (CTFBat_Giftwrap *)GetLauncher(), flBleedTime ); // Apply particle effect to victim (the remaining effects happen inside Explode) DispatchParticleEffect( "xms_ornament_glitter", PATTACH_POINT_FOLLOW, pPlayer, "head" ); // Give 'em a love tap. const trace_t *pTrace = &CBaseEntity::GetTouchTrace(); trace_t *pNewTrace = const_cast( pTrace ); CBaseEntity *pInflictor = GetLauncher(); CTakeDamageInfo info; info.SetAttacker( GetOwnerEntity() ); info.SetInflictor( pInflictor ); info.SetWeapon( pInflictor ); info.SetDamage( GetDamage() ); info.SetDamageCustom( TF_DMG_CUSTOM_BASEBALL ); info.SetDamageForce( GetDamageForce() ); info.SetDamagePosition( GetAbsOrigin() ); int iDamageType = GetDamageType(); if ( bIsCriticalHit ) iDamageType |= DMG_CRITICAL; info.SetDamageType( iDamageType ); // Hurt 'em. Vector dir; AngleVectors( GetAbsAngles(), &dir ); pPlayer->DispatchTraceAttack( info, dir, pNewTrace ); ApplyMultiDamage(); // the ball shatters UTIL_Remove( this ); m_bTouched = true; } void CTFBall_Ornament::PipebombTouch( CBaseEntity *pOther ) { CTFPlayer* pOwner = ToTFPlayer( GetOwnerEntity() ); if ( !pOwner ) return; // Go away if we're hit by a moving train. if ( pOther->GetModelName() == s_iszTrainName && ( pOther->GetAbsVelocity().LengthSqr() > 1.0f ) ) { UTIL_Remove( this ); return; } // Go away if we hit the skybox. trace_t pTrace; Vector velDir = GetAbsVelocity(); VectorNormalize( velDir ); Vector vecSpot = GetAbsOrigin() - 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; } if ( pOther == GetThrower() ) return; // Explode (does radius damage, triggers particles and sound effects). Explode( &pTrace, DMG_BLAST|DMG_PREVENT_PHYSICS_FORCE ); if ( !InSameTeam( pOther ) && pOther->m_takedamage != DAMAGE_NO ) { ApplyBallImpactEffectOnVictim( pOther ); } } void CTFBall_Ornament::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) { BaseClass::VPhysicsCollision( index, pEvent ); int otherIndex = !index; CBaseEntity *pHitEntity = pEvent->pEntities[otherIndex]; if ( !pHitEntity ) return; // Break if we hit the world. if ( pHitEntity->IsWorld() ) { // Explode immediately next frame. (Can't explode in the collision callback.) m_vCollisionVelocity = pEvent->preVelocity[index]; SetContextThink( &CTFBall_Ornament::VPhysicsCollisionThink, gpGlobals->curtime, "OrnamentCollisionThink" ); } } void CTFBall_Ornament::VPhysicsCollisionThink( void ) { trace_t pTrace; Vector velDir = m_vCollisionVelocity; VectorNormalize( velDir ); Vector vecSpot = GetAbsOrigin() - velDir * 16; UTIL_TraceLine( vecSpot, vecSpot + velDir * 32, MASK_SOLID, this, COLLISION_GROUP_NONE, &pTrace ); Explode( &pTrace, DMG_BLAST|DMG_PREVENT_PHYSICS_FORCE ); } void CTFBall_Ornament::Explode( trace_t *pTrace, int bitsDamageType ) { // Create smashed glass particles when we explode CTFPlayer* pOwner = ToTFPlayer( GetOwnerEntity() ); if ( pOwner && pOwner->GetTeamNumber() == TF_TEAM_RED ) { DispatchParticleEffect( "xms_ornament_smash_red", GetAbsOrigin(), GetAbsAngles() ); } else { DispatchParticleEffect( "xms_ornament_smash_blue", GetAbsOrigin(), GetAbsAngles() ); } Vector vecOrigin = GetAbsOrigin(); // sound effects EmitSound_t params; params.m_flSoundTime = 0; params.m_pflSoundDuration = 0; params.m_pSoundName = "BallBuster.OrnamentImpact"; CPASFilter filter( vecOrigin ); filter.RemoveRecipient( pOwner ); EmitSound( filter, entindex(), params ); CSingleUserRecipientFilter attackerFilter( pOwner ); EmitSound( attackerFilter, pOwner->entindex(), params ); // Explosion damage is some fraction of our base damage float flExplodeDamage = GetDamage() * DEFAULT_ORNAMENT_EXPLODE_DAMAGE_MULT; // Do radius damage Vector vecBlastForce(0.0f, 0.0f, 0.0f); CTakeDamageInfo info( this, GetThrower(), m_hLauncher, vecBlastForce, GetAbsOrigin(), flExplodeDamage, bitsDamageType, TF_DMG_CUSTOM_BASEBALL, &vecOrigin ); CTFRadiusDamageInfo radiusinfo( &info, vecOrigin, DEFAULT_ORNAMENT_EXPLODE_RADIUS, nullptr, 0.0f, 0.0f ); TFGameRules()->RadiusDamage( radiusinfo ); UTIL_Remove( this ); } #endif