//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: combine ball - can be held by the super physcannon and launched // by the AR2's alt-fire // //=============================================================================// #include "cbase.h" #include "prop_combine_ball.h" #include "props.h" #include "explode.h" #include "saverestore_utlvector.h" #include "hl2_shareddefs.h" #include "materialsystem/imaterial.h" #include "beam_flags.h" #include "physics_prop_ragdoll.h" #include "soundent.h" #include "soundenvelope.h" #include "te_effect_dispatch.h" #include "ai_basenpc.h" #include "npc_bullseye.h" #include "filters.h" #include "SpriteTrail.h" #include "decals.h" #include "hl2_player.h" #include "eventqueue.h" #include "physics_collisionevent.h" #include "gamestats.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" #define PROP_COMBINE_BALL_MODEL "models/effects/combineball.mdl" #define PROP_COMBINE_BALL_SPRITE_TRAIL "sprites/combineball_trail_black_1.vmt" #define PROP_COMBINE_BALL_LIFETIME 4.0f // Seconds #define PROP_COMBINE_BALL_HOLD_DISSOLVE_TIME 8.0f #define SF_COMBINE_BALL_BOUNCING_IN_SPAWNER 0x10000 #define MAX_COMBINEBALL_RADIUS 12 ConVar sk_npc_dmg_combineball( "sk_npc_dmg_combineball","15", FCVAR_REPLICATED); ConVar sk_combineball_guidefactor( "sk_combineball_guidefactor","0.5", FCVAR_REPLICATED); ConVar sk_combine_ball_search_radius( "sk_combine_ball_search_radius", "512", FCVAR_REPLICATED); ConVar sk_combineball_seek_angle( "sk_combineball_seek_angle","15.0", FCVAR_REPLICATED); ConVar sk_combineball_seek_kill( "sk_combineball_seek_kill","0", FCVAR_REPLICATED); // For our ring explosion int s_nExplosionTexture = -1; //----------------------------------------------------------------------------- // Context think //----------------------------------------------------------------------------- static const char *s_pWhizThinkContext = "WhizThinkContext"; static const char *s_pHoldDissolveContext = "HoldDissolveContext"; static const char *s_pExplodeTimerContext = "ExplodeTimerContext"; static const char *s_pAnimThinkContext = "AnimThinkContext"; static const char *s_pCaptureContext = "CaptureContext"; static const char *s_pRemoveContext = "RemoveContext"; //----------------------------------------------------------------------------- // Purpose: // Input : radius - // Output : CBaseEntity //----------------------------------------------------------------------------- CBaseEntity *CreateCombineBall( const Vector &origin, const Vector &velocity, float radius, float mass, float lifetime, CBaseEntity *pOwner ) { CPropCombineBall *pBall = static_cast( CreateEntityByName( "prop_combine_ball" ) ); pBall->SetRadius( radius ); pBall->SetAbsOrigin( origin ); pBall->SetOwnerEntity( pOwner ); pBall->SetOriginalOwner( pOwner ); pBall->SetAbsVelocity( velocity ); pBall->Spawn(); pBall->SetState( CPropCombineBall::STATE_THROWN ); pBall->SetSpeed( velocity.Length() ); pBall->EmitSound( "NPC_CombineBall.Launch" ); PhysSetGameFlags( pBall->VPhysicsGetObject(), FVPHYSICS_WAS_THROWN ); pBall->StartWhizSoundThink(); pBall->SetMass( mass ); pBall->StartLifetime( lifetime ); pBall->SetWeaponLaunched( true ); return pBall; } //----------------------------------------------------------------------------- // Purpose: Allows game to know if the physics object should kill allies or not //----------------------------------------------------------------------------- CBasePlayer *CPropCombineBall::HasPhysicsAttacker( float dt ) { // Must have an owner if ( GetOwnerEntity() == NULL ) return NULL; // Must be a player if ( GetOwnerEntity()->IsPlayer() == false ) return NULL; // We don't care about the time passed in return static_cast(GetOwnerEntity()); } //----------------------------------------------------------------------------- // Purpose: Determines whether a physics object is a combine ball or not // Input : *pObj - Object to test // Output : Returns true on success, false on failure. // Notes : This function cannot identify a combine ball that is held by // the physcannon because any object held by the physcannon is // COLLISIONGROUP_DEBRIS. //----------------------------------------------------------------------------- bool UTIL_IsCombineBall( CBaseEntity *pEntity ) { // Must be the correct collision group if ( pEntity->GetCollisionGroup() != HL2COLLISION_GROUP_COMBINE_BALL ) return false; //NOTENOTE: This allows ANY combine ball to pass the test /* CPropCombineBall *pBall = dynamic_cast(pEntity); if ( pBall && pBall->WasWeaponLaunched() ) return false; */ return true; } //----------------------------------------------------------------------------- // Purpose: Determines whether a physics object is an AR2 combine ball or not // Input : *pEntity - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool UTIL_IsAR2CombineBall( CBaseEntity *pEntity ) { // Must be the correct collision group if ( pEntity->GetCollisionGroup() != HL2COLLISION_GROUP_COMBINE_BALL ) return false; CPropCombineBall *pBall = dynamic_cast(pEntity); if ( pBall && pBall->WasWeaponLaunched() ) return true; return false; } //----------------------------------------------------------------------------- // Purpose: Uses a deeper casting check to determine if pEntity is a combine // ball. This function exists because the normal (much faster) check // in UTIL_IsCombineBall() can never identify a combine ball held by // the physcannon because the physcannon changes the held entity's // collision group. // Input : *pEntity - Entity to check // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool UTIL_IsCombineBallDefinite( CBaseEntity *pEntity ) { CPropCombineBall *pBall = dynamic_cast(pEntity); return pBall != NULL; } //----------------------------------------------------------------------------- // // Spawns combine balls // //----------------------------------------------------------------------------- #define SF_SPAWNER_START_DISABLED 0x1000 #define SF_SPAWNER_POWER_SUPPLY 0x2000 //----------------------------------------------------------------------------- // Implementation of CPropCombineBall //----------------------------------------------------------------------------- LINK_ENTITY_TO_CLASS( prop_combine_ball, CPropCombineBall ); //----------------------------------------------------------------------------- // Save/load: //----------------------------------------------------------------------------- BEGIN_DATADESC( CPropCombineBall ) DEFINE_FIELD( m_flLastBounceTime, FIELD_TIME ), DEFINE_FIELD( m_flRadius, FIELD_FLOAT ), DEFINE_FIELD( m_nState, FIELD_CHARACTER ), DEFINE_FIELD( m_pGlowTrail, FIELD_CLASSPTR ), DEFINE_SOUNDPATCH( m_pHoldingSound ), DEFINE_FIELD( m_bFiredGrabbedOutput, FIELD_BOOLEAN ), DEFINE_FIELD( m_bEmit, FIELD_BOOLEAN ), DEFINE_FIELD( m_bHeld, FIELD_BOOLEAN ), DEFINE_FIELD( m_bLaunched, FIELD_BOOLEAN ), DEFINE_FIELD( m_bStruckEntity, FIELD_BOOLEAN ), DEFINE_FIELD( m_bWeaponLaunched, FIELD_BOOLEAN ), DEFINE_FIELD( m_bForward, FIELD_BOOLEAN ), DEFINE_FIELD( m_flSpeed, FIELD_FLOAT ), DEFINE_FIELD( m_flNextDamageTime, FIELD_TIME ), DEFINE_FIELD( m_flLastCaptureTime, FIELD_TIME ), DEFINE_FIELD( m_bCaptureInProgress, FIELD_BOOLEAN ), DEFINE_FIELD( m_nBounceCount, FIELD_INTEGER ), DEFINE_FIELD( m_nMaxBounces, FIELD_INTEGER ), DEFINE_FIELD( m_bBounceDie, FIELD_BOOLEAN ), DEFINE_FIELD( m_hSpawner, FIELD_EHANDLE ), DEFINE_THINKFUNC( ExplodeThink ), DEFINE_THINKFUNC( WhizSoundThink ), DEFINE_THINKFUNC( DieThink ), DEFINE_THINKFUNC( DissolveThink ), DEFINE_THINKFUNC( DissolveRampSoundThink ), DEFINE_THINKFUNC( AnimThink ), DEFINE_THINKFUNC( CaptureBySpawner ), DEFINE_INPUTFUNC( FIELD_VOID, "Explode", InputExplode ), DEFINE_INPUTFUNC( FIELD_VOID, "FadeAndRespawn", InputFadeAndRespawn ), DEFINE_INPUTFUNC( FIELD_VOID, "Kill", InputKill ), DEFINE_INPUTFUNC( FIELD_VOID, "Socketed", InputSocketed ), END_DATADESC() IMPLEMENT_SERVERCLASS_ST( CPropCombineBall, DT_PropCombineBall ) SendPropBool( SENDINFO( m_bEmit ) ), SendPropFloat( SENDINFO( m_flRadius ), 0, SPROP_NOSCALE ), SendPropBool( SENDINFO( m_bHeld ) ), SendPropBool( SENDINFO( m_bLaunched ) ), END_SEND_TABLE() //----------------------------------------------------------------------------- // Gets at the spawner //----------------------------------------------------------------------------- CFuncCombineBallSpawner *CPropCombineBall::GetSpawner() { return m_hSpawner; } //----------------------------------------------------------------------------- // Precache //----------------------------------------------------------------------------- void CPropCombineBall::Precache( void ) { //NOTENOTE: We don't call into the base class because it chains multiple // precaches we don't need to incur PrecacheModel( PROP_COMBINE_BALL_MODEL ); PrecacheModel( PROP_COMBINE_BALL_SPRITE_TRAIL ); s_nExplosionTexture = PrecacheModel( "sprites/lgtning.vmt" ); PrecacheScriptSound( "NPC_CombineBall.Launch" ); PrecacheScriptSound( "NPC_CombineBall.KillImpact" ); if ( hl2_episodic.GetBool() ) { PrecacheScriptSound( "NPC_CombineBall_Episodic.Explosion" ); PrecacheScriptSound( "NPC_CombineBall_Episodic.WhizFlyby" ); PrecacheScriptSound( "NPC_CombineBall_Episodic.Impact" ); } else { PrecacheScriptSound( "NPC_CombineBall.Explosion" ); PrecacheScriptSound( "NPC_CombineBall.WhizFlyby" ); PrecacheScriptSound( "NPC_CombineBall.Impact" ); } PrecacheScriptSound( "NPC_CombineBall.HoldingInPhysCannon" ); } //----------------------------------------------------------------------------- // Spherical vphysics //----------------------------------------------------------------------------- bool CPropCombineBall::OverridePropdata() { return true; } //----------------------------------------------------------------------------- // Spherical vphysics //----------------------------------------------------------------------------- void CPropCombineBall::SetState( int state ) { if ( m_nState != state ) { if ( m_nState == STATE_NOT_THROWN ) { m_flLastCaptureTime = gpGlobals->curtime; } m_nState = state; } } bool CPropCombineBall::IsInField() const { return (m_nState == STATE_NOT_THROWN); } //----------------------------------------------------------------------------- // Sets the radius //----------------------------------------------------------------------------- void CPropCombineBall::SetRadius( float flRadius ) { m_flRadius = clamp( flRadius, 1, MAX_COMBINEBALL_RADIUS ); } //----------------------------------------------------------------------------- // Create vphysics //----------------------------------------------------------------------------- bool CPropCombineBall::CreateVPhysics() { SetSolid( SOLID_BBOX ); float flSize = m_flRadius; SetCollisionBounds( Vector(-flSize, -flSize, -flSize), Vector(flSize, flSize, flSize) ); objectparams_t params = g_PhysDefaultObjectParams; params.pGameData = static_cast(this); int nMaterialIndex = physprops->GetSurfaceIndex("metal_bouncy"); IPhysicsObject *pPhysicsObject = physenv->CreateSphereObject( flSize, nMaterialIndex, GetAbsOrigin(), GetAbsAngles(), ¶ms, false ); if ( !pPhysicsObject ) return false; VPhysicsSetObject( pPhysicsObject ); SetMoveType( MOVETYPE_VPHYSICS ); pPhysicsObject->Wake(); pPhysicsObject->SetMass( 750.0f ); pPhysicsObject->EnableGravity( false ); pPhysicsObject->EnableDrag( false ); float flDamping = 0.0f; float flAngDamping = 0.5f; pPhysicsObject->SetDamping( &flDamping, &flAngDamping ); pPhysicsObject->SetInertia( Vector( 1e30, 1e30, 1e30 ) ); if( WasFiredByNPC() ) { // Don't do impact damage. Just touch them and do your dissolve damage and move on. PhysSetGameFlags( pPhysicsObject, FVPHYSICS_NO_NPC_IMPACT_DMG ); } else { PhysSetGameFlags( pPhysicsObject, FVPHYSICS_DMG_DISSOLVE | FVPHYSICS_HEAVY_OBJECT ); } return true; } //----------------------------------------------------------------------------- // Spawn: //----------------------------------------------------------------------------- void CPropCombineBall::Spawn( void ) { BaseClass::Spawn(); SetModel( PROP_COMBINE_BALL_MODEL ); if( ShouldHitPlayer() ) { // This allows the combine ball to hit the player. SetCollisionGroup( HL2COLLISION_GROUP_COMBINE_BALL_NPC ); } else { SetCollisionGroup( HL2COLLISION_GROUP_COMBINE_BALL ); } CreateVPhysics(); Vector vecAbsVelocity = GetAbsVelocity(); VPhysicsGetObject()->SetVelocity( &vecAbsVelocity, NULL ); m_nState = STATE_NOT_THROWN; m_flLastBounceTime = -1.0f; m_bFiredGrabbedOutput = false; m_bForward = true; m_bCaptureInProgress = false; // No shadow! AddEffects( EF_NOSHADOW ); // Start up the eye trail m_pGlowTrail = CSpriteTrail::SpriteTrailCreate( PROP_COMBINE_BALL_SPRITE_TRAIL, GetAbsOrigin(), false ); if ( m_pGlowTrail != NULL ) { m_pGlowTrail->FollowEntity( this ); m_pGlowTrail->SetTransparency( kRenderTransAdd, 0, 0, 0, 255, kRenderFxNone ); m_pGlowTrail->SetStartWidth( m_flRadius ); m_pGlowTrail->SetEndWidth( 0 ); m_pGlowTrail->SetLifeTime( 0.1f ); m_pGlowTrail->TurnOff(); } m_bEmit = true; m_bHeld = false; m_bLaunched = false; m_bStruckEntity = false; m_bWeaponLaunched = false; m_flNextDamageTime = gpGlobals->curtime; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPropCombineBall::StartAnimating( void ) { // Start our animation cycle. Use the random to avoid everything thinking the same frame SetContextThink( &CPropCombineBall::AnimThink, gpGlobals->curtime + random->RandomFloat( 0.0f, 0.1f), s_pAnimThinkContext ); int nSequence = LookupSequence( "idle" ); SetCycle( 0 ); m_flAnimTime = gpGlobals->curtime; ResetSequence( nSequence ); ResetClientsideFrame(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPropCombineBall::StopAnimating( void ) { SetContextThink( NULL, gpGlobals->curtime, s_pAnimThinkContext ); } //----------------------------------------------------------------------------- // Put it into the spawner //----------------------------------------------------------------------------- void CPropCombineBall::CaptureBySpawner( ) { m_bCaptureInProgress = true; m_bFiredGrabbedOutput = false; // Slow down the ball Vector vecVelocity; VPhysicsGetObject()->GetVelocity( &vecVelocity, NULL ); float flSpeed = VectorNormalize( vecVelocity ); if ( flSpeed > 25.0f ) { vecVelocity *= flSpeed * 0.4f; VPhysicsGetObject()->SetVelocity( &vecVelocity, NULL ); // Slow it down until we can set its velocity ok SetContextThink( &CPropCombineBall::CaptureBySpawner, gpGlobals->curtime + 0.01f, s_pCaptureContext ); return; } // Ok, we're captured SetContextThink( NULL, gpGlobals->curtime, s_pCaptureContext ); ReplaceInSpawner( GetSpawner()->GetBallSpeed() ); m_bCaptureInProgress = false; } //----------------------------------------------------------------------------- // Put it into the spawner //----------------------------------------------------------------------------- void CPropCombineBall::ReplaceInSpawner( float flSpeed ) { m_bForward = true; m_nState = STATE_NOT_THROWN; // Prevent it from exploding ClearLifetime( ); // Stop whiz noises SetContextThink( NULL, gpGlobals->curtime, s_pWhizThinkContext ); // Slam velocity to what the field wants Vector vecTarget, vecVelocity; GetSpawner()->GetTargetEndpoint( m_bForward, &vecTarget ); VectorSubtract( vecTarget, GetAbsOrigin(), vecVelocity ); VectorNormalize( vecVelocity ); vecVelocity *= flSpeed; VPhysicsGetObject()->SetVelocity( &vecVelocity, NULL ); // Set our desired speed to the spawner's speed. This will be // our speed on our first bounce in the field. SetSpeed( flSpeed ); } float CPropCombineBall::LastCaptureTime() const { if ( IsInField() || IsBeingCaptured() ) return gpGlobals->curtime; return m_flLastCaptureTime; } //----------------------------------------------------------------------------- // Purpose: Starts the lifetime countdown on the ball // Input : flDuration - number of seconds to live before exploding //----------------------------------------------------------------------------- void CPropCombineBall::StartLifetime( float flDuration ) { SetContextThink( &CPropCombineBall::ExplodeThink, gpGlobals->curtime + flDuration, s_pExplodeTimerContext ); } //----------------------------------------------------------------------------- // Purpose: Stops the lifetime on the ball from expiring //----------------------------------------------------------------------------- void CPropCombineBall::ClearLifetime( void ) { // Prevent it from exploding SetContextThink( NULL, gpGlobals->curtime, s_pExplodeTimerContext ); } //----------------------------------------------------------------------------- // Purpose: // Input : mass - //----------------------------------------------------------------------------- void CPropCombineBall::SetMass( float mass ) { IPhysicsObject *pObj = VPhysicsGetObject(); if ( pObj != NULL ) { pObj->SetMass( mass ); pObj->SetInertia( Vector( 500, 500, 500 ) ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CPropCombineBall::ShouldHitPlayer() const { if ( GetOwnerEntity() ) { CAI_BaseNPC *pNPC = GetOwnerEntity()->MyNPCPointer(); if ( pNPC && !pNPC->IsPlayerAlly() ) { return true; } } return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPropCombineBall::InputKill( inputdata_t &inputdata ) { // tell owner ( if any ) that we're dead.This is mostly for NPCMaker functionality. CBaseEntity *pOwner = GetOwnerEntity(); if ( pOwner ) { pOwner->DeathNotice( this ); SetOwnerEntity( NULL ); } UTIL_Remove( this ); NotifySpawnerOfRemoval(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPropCombineBall::InputSocketed( inputdata_t &inputdata ) { // tell owner ( if any ) that we're dead.This is mostly for NPCMaker functionality. CBaseEntity *pOwner = GetOwnerEntity(); if ( pOwner ) { pOwner->DeathNotice( this ); SetOwnerEntity( NULL ); } // if our owner is a player, tell them we were socketed CHL2_Player *pPlayer = dynamic_cast( pOwner ); if ( pPlayer ) { pPlayer->CombineBallSocketed( this ); } UTIL_Remove( this ); NotifySpawnerOfRemoval(); } //----------------------------------------------------------------------------- // Cleanup. //----------------------------------------------------------------------------- void CPropCombineBall::UpdateOnRemove() { if ( m_pGlowTrail != NULL ) { UTIL_Remove( m_pGlowTrail ); m_pGlowTrail = NULL; } //Sigh... this is the only place where I can get a message after the ball is done dissolving. if ( hl2_episodic.GetBool() ) { if ( IsDissolving() ) { if ( GetSpawner() ) { GetSpawner()->BallGrabbed( this ); NotifySpawnerOfRemoval(); } } } BaseClass::UpdateOnRemove(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPropCombineBall::ExplodeThink( void ) { DoExplosion(); } //----------------------------------------------------------------------------- // Purpose: Tell the respawner to make a new one //----------------------------------------------------------------------------- void CPropCombineBall::NotifySpawnerOfRemoval( void ) { if ( GetSpawner() ) { GetSpawner()->RespawnBallPostExplosion(); } } //----------------------------------------------------------------------------- // Fade out. //----------------------------------------------------------------------------- void CPropCombineBall::DieThink() { if ( GetSpawner() ) { //Let the spawner know we died so it does it's thing if( hl2_episodic.GetBool() && IsInField() ) { GetSpawner()->BallGrabbed( this ); } GetSpawner()->RespawnBall( 0.1 ); } UTIL_Remove( this ); } //----------------------------------------------------------------------------- // Fade out. //----------------------------------------------------------------------------- void CPropCombineBall::FadeOut( float flDuration ) { AddSolidFlags( FSOLID_NOT_SOLID ); // Start up the eye trail if ( m_pGlowTrail != NULL ) { m_pGlowTrail->SetBrightness( 0, flDuration ); } SetThink( &CPropCombineBall::DieThink ); SetNextThink( gpGlobals->curtime + flDuration ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPropCombineBall::StartWhizSoundThink( void ) { SetContextThink( &CPropCombineBall::WhizSoundThink, gpGlobals->curtime + 2.0f * TICK_INTERVAL, s_pWhizThinkContext ); } //----------------------------------------------------------------------------- // Danger sounds. //----------------------------------------------------------------------------- void CPropCombineBall::WhizSoundThink() { Vector vecPosition, vecVelocity; IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); if ( pPhysicsObject == NULL ) { //NOTENOTE: We should always have been created at this point Assert( 0 ); SetContextThink( &CPropCombineBall::WhizSoundThink, gpGlobals->curtime + 2.0f * TICK_INTERVAL, s_pWhizThinkContext ); return; } pPhysicsObject->GetPosition( &vecPosition, NULL ); pPhysicsObject->GetVelocity( &vecVelocity, NULL ); if ( gpGlobals->maxClients == 1 ) { CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); if ( pPlayer ) { Vector vecDelta; VectorSubtract( pPlayer->GetAbsOrigin(), vecPosition, vecDelta ); VectorNormalize( vecDelta ); if ( DotProduct( vecDelta, vecVelocity ) > 0.5f ) { Vector vecEndPoint; VectorMA( vecPosition, 2.0f * TICK_INTERVAL, vecVelocity, vecEndPoint ); float flDist = CalcDistanceToLineSegment( pPlayer->GetAbsOrigin(), vecPosition, vecEndPoint ); if ( flDist < 200.0f ) { CPASAttenuationFilter filter( vecPosition, ATTN_NORM ); EmitSound_t ep; ep.m_nChannel = CHAN_STATIC; if ( hl2_episodic.GetBool() ) { ep.m_pSoundName = "NPC_CombineBall_Episodic.WhizFlyby"; } else { ep.m_pSoundName = "NPC_CombineBall.WhizFlyby"; } ep.m_flVolume = 1.0f; ep.m_SoundLevel = SNDLVL_NORM; EmitSound( filter, entindex(), ep ); SetContextThink( &CPropCombineBall::WhizSoundThink, gpGlobals->curtime + 0.5f, s_pWhizThinkContext ); return; } } } } SetContextThink( &CPropCombineBall::WhizSoundThink, gpGlobals->curtime + 2.0f * TICK_INTERVAL, s_pWhizThinkContext ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPropCombineBall::SetBallAsLaunched( void ) { // Give the ball a duration StartLifetime( PROP_COMBINE_BALL_LIFETIME ); m_bHeld = false; m_bLaunched = true; SetState( STATE_THROWN ); VPhysicsGetObject()->SetMass( 750.0f ); VPhysicsGetObject()->SetInertia( Vector( 1e30, 1e30, 1e30 ) ); StopLoopingSounds(); EmitSound( "NPC_CombineBall.Launch" ); WhizSoundThink(); } //----------------------------------------------------------------------------- // Lighten the mass so it's zippy toget to the gun //----------------------------------------------------------------------------- void CPropCombineBall::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) { CDefaultPlayerPickupVPhysics::OnPhysGunPickup( pPhysGunUser, reason ); if ( m_nMaxBounces == -1 ) { m_nMaxBounces = 0; } if ( !m_bFiredGrabbedOutput ) { if ( GetSpawner() ) { GetSpawner()->BallGrabbed( this ); } m_bFiredGrabbedOutput = true; } if ( m_pGlowTrail ) { m_pGlowTrail->TurnOff(); m_pGlowTrail->SetRenderColor( 0, 0, 0, 0 ); } if ( reason != PUNTED_BY_CANNON ) { SetState( STATE_HOLDING ); CPASAttenuationFilter filter( GetAbsOrigin(), ATTN_NORM ); filter.MakeReliable(); EmitSound_t ep; ep.m_nChannel = CHAN_STATIC; if( hl2_episodic.GetBool() ) { ep.m_pSoundName = "NPC_CombineBall_Episodic.HoldingInPhysCannon"; } else { ep.m_pSoundName = "NPC_CombineBall.HoldingInPhysCannon"; } ep.m_flVolume = 1.0f; ep.m_SoundLevel = SNDLVL_NORM; // Now we own this ball SetPlayerLaunched( pPhysGunUser ); CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); m_pHoldingSound = controller.SoundCreate( filter, entindex(), ep ); controller.Play( m_pHoldingSound, 1.0f, 100 ); // Don't collide with anything we may have to pull the ball through SetCollisionGroup( COLLISION_GROUP_DEBRIS ); VPhysicsGetObject()->SetMass( 20.0f ); VPhysicsGetObject()->SetInertia( Vector( 100, 100, 100 ) ); // Make it not explode ClearLifetime( ); m_bHeld = true; m_bLaunched = false; //Let the ball know is not being captured by one of those ball fields anymore. // m_bCaptureInProgress = false; SetContextThink( &CPropCombineBall::DissolveRampSoundThink, gpGlobals->curtime + GetBallHoldSoundRampTime(), s_pHoldDissolveContext ); StartAnimating(); } else { Vector vecVelocity; VPhysicsGetObject()->GetVelocity( &vecVelocity, NULL ); SetSpeed( vecVelocity.Length() ); // Set us as being launched by the player SetPlayerLaunched( pPhysGunUser ); SetBallAsLaunched(); StopAnimating(); } } //----------------------------------------------------------------------------- // Purpose: Reset the ball to be deadly to NPCs after we've picked it up //----------------------------------------------------------------------------- void CPropCombineBall::SetPlayerLaunched( CBasePlayer *pOwner ) { // Now we own this ball SetOwnerEntity( pOwner ); SetWeaponLaunched( false ); if( VPhysicsGetObject() ) { PhysClearGameFlags( VPhysicsGetObject(), FVPHYSICS_NO_NPC_IMPACT_DMG ); PhysSetGameFlags( VPhysicsGetObject(), FVPHYSICS_DMG_DISSOLVE | FVPHYSICS_HEAVY_OBJECT ); } } //----------------------------------------------------------------------------- // Activate death-spin! //----------------------------------------------------------------------------- void CPropCombineBall::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) { CDefaultPlayerPickupVPhysics::OnPhysGunDrop( pPhysGunUser, Reason ); SetState( STATE_THROWN ); WhizSoundThink(); m_bHeld = false; m_bLaunched = true; // Stop with the dissolving SetContextThink( NULL, gpGlobals->curtime, s_pHoldDissolveContext ); // We're ready to start colliding again. SetCollisionGroup( HL2COLLISION_GROUP_COMBINE_BALL ); if ( m_pGlowTrail ) { m_pGlowTrail->TurnOn(); m_pGlowTrail->SetRenderColor( 255, 255, 255, 255 ); } // Set our desired speed to be launched at SetSpeed( 1500.0f ); SetPlayerLaunched( pPhysGunUser ); if ( Reason != LAUNCHED_BY_CANNON ) { // Choose a random direction (forward facing) Vector vecForward; pPhysGunUser->GetVectors( &vecForward, NULL, NULL ); QAngle shotAng; VectorAngles( vecForward, shotAng ); // Offset by some small cone shotAng[PITCH] += random->RandomInt( -55, 55 ); shotAng[YAW] += random->RandomInt( -55, 55 ); AngleVectors( shotAng, &vecForward, NULL, NULL ); vecForward *= GetSpeed(); VPhysicsGetObject()->SetVelocity( &vecForward, &vec3_origin ); } else { // This will have the consequence of making it so that the // ball is launched directly down the crosshair even if the player is moving. VPhysicsGetObject()->SetVelocity( &vec3_origin, &vec3_origin ); } SetBallAsLaunched(); StopAnimating(); } //------------------------------------------------------------------------------ // Stop looping sounds //------------------------------------------------------------------------------ void CPropCombineBall::StopLoopingSounds() { if ( m_pHoldingSound ) { CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); controller.Shutdown( m_pHoldingSound ); controller.SoundDestroy( m_pHoldingSound ); m_pHoldingSound = NULL; } } //------------------------------------------------------------------------------ // Pow! //------------------------------------------------------------------------------ void CPropCombineBall::DissolveRampSoundThink( ) { float dt = GetBallHoldDissolveTime() - GetBallHoldSoundRampTime(); if ( m_pHoldingSound ) { CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); controller.SoundChangePitch( m_pHoldingSound, 150, dt ); } SetContextThink( &CPropCombineBall::DissolveThink, gpGlobals->curtime + dt, s_pHoldDissolveContext ); } //------------------------------------------------------------------------------ // Pow! //------------------------------------------------------------------------------ void CPropCombineBall::DissolveThink( ) { DoExplosion(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- float CPropCombineBall::GetBallHoldDissolveTime() { float flDissolveTime = PROP_COMBINE_BALL_HOLD_DISSOLVE_TIME; if( g_pGameRules->IsSkillLevel( 1 ) && hl2_episodic.GetBool() ) { // Give players more time to handle/aim combine balls on Easy. flDissolveTime *= 1.5f; } return flDissolveTime; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- float CPropCombineBall::GetBallHoldSoundRampTime() { return GetBallHoldDissolveTime() - 1.0f; } //------------------------------------------------------------------------------ // Pow! //------------------------------------------------------------------------------ void CPropCombineBall::DoExplosion( ) { // don't do this twice if ( GetMoveType() == MOVETYPE_NONE ) return; if ( PhysIsInCallback() ) { g_PostSimulationQueue.QueueCall( this, &CPropCombineBall::DoExplosion ); return; } // Tell the respawner to make a new one if ( GetSpawner() ) { GetSpawner()->RespawnBallPostExplosion(); } //Shockring CBroadcastRecipientFilter filter2; if ( OutOfBounces() == false ) { if ( hl2_episodic.GetBool() ) { EmitSound( "NPC_CombineBall_Episodic.Explosion" ); } else { EmitSound( "NPC_CombineBall.Explosion" ); } UTIL_ScreenShake( GetAbsOrigin(), 20.0f, 150.0, 1.0, 1250.0f, SHAKE_START ); CEffectData data; data.m_vOrigin = GetAbsOrigin(); DispatchEffect( "cball_explode", data ); te->BeamRingPoint( filter2, 0, GetAbsOrigin(), //origin m_flRadius, //start radius 1024, //end radius s_nExplosionTexture, //texture 0, //halo index 0, //start frame 2, //framerate 0.2f, //life 64, //width 0, //spread 0, //amplitude 255, //r 255, //g 225, //b 32, //a 0, //speed FBEAM_FADEOUT ); //Shockring te->BeamRingPoint( filter2, 0, GetAbsOrigin(), //origin m_flRadius, //start radius 1024, //end radius s_nExplosionTexture, //texture 0, //halo index 0, //start frame 2, //framerate 0.5f, //life 64, //width 0, //spread 0, //amplitude 255, //r 255, //g 225, //b 64, //a 0, //speed FBEAM_FADEOUT ); } else { //Shockring te->BeamRingPoint( filter2, 0, GetAbsOrigin(), //origin 128, //start radius 384, //end radius s_nExplosionTexture, //texture 0, //halo index 0, //start frame 2, //framerate 0.25f, //life 48, //width 0, //spread 0, //amplitude 255, //r 255, //g 225, //b 64, //a 0, //speed FBEAM_FADEOUT ); } if( hl2_episodic.GetBool() ) { CSoundEnt::InsertSound( SOUND_COMBAT | SOUND_CONTEXT_EXPLOSION, WorldSpaceCenter(), 180.0f, 0.25, this ); } // Turn us off and wait because we need our trails to finish up properly SetAbsVelocity( vec3_origin ); SetMoveType( MOVETYPE_NONE ); AddSolidFlags( FSOLID_NOT_SOLID ); m_bEmit = false; if( !m_bStruckEntity && hl2_episodic.GetBool() && GetOwnerEntity() != NULL ) { // Notify the player proxy that this combine ball missed so that it can fire an output. CHL2_Player *pPlayer = dynamic_cast( GetOwnerEntity() ); if ( pPlayer ) { pPlayer->MissedAR2AltFire(); } } SetContextThink( &CPropCombineBall::SUB_Remove, gpGlobals->curtime + 0.5f, s_pRemoveContext ); StopLoopingSounds(); } //----------------------------------------------------------------------------- // Enable/disable //----------------------------------------------------------------------------- void CPropCombineBall::InputExplode( inputdata_t &inputdata ) { DoExplosion(); } //----------------------------------------------------------------------------- // Enable/disable //----------------------------------------------------------------------------- void CPropCombineBall::InputFadeAndRespawn( inputdata_t &inputdata ) { FadeOut( 0.1f ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPropCombineBall::CollisionEventToTrace( int index, gamevcollisionevent_t *pEvent, trace_t &tr ) { UTIL_ClearTrace( tr ); pEvent->pInternalData->GetSurfaceNormal( tr.plane.normal ); pEvent->pInternalData->GetContactPoint( tr.endpos ); tr.plane.dist = DotProduct( tr.plane.normal, tr.endpos ); VectorMA( tr.endpos, -1.0f, pEvent->preVelocity[index], tr.startpos ); tr.m_pEnt = pEvent->pEntities[!index]; tr.fraction = 0.01f; // spoof! } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CPropCombineBall::DissolveEntity( CBaseEntity *pEntity ) { if( pEntity->IsEFlagSet( EFL_NO_DISSOLVE ) ) return false; #ifdef HL2MP if ( pEntity->IsPlayer() ) { m_bStruckEntity = true; return false; } #endif if( !pEntity->IsNPC() && !(dynamic_cast(pEntity)) ) return false; pEntity->GetBaseAnimating()->Dissolve( "", gpGlobals->curtime, false, ENTITY_DISSOLVE_NORMAL ); // Note that we've struck an entity m_bStruckEntity = true; // Force an NPC to not drop their weapon if dissolved // CBaseCombatCharacter *pBCC = ToBaseCombatCharacter( pEntity ); // if ( pBCC != NULL ) // { // pEntity->AddSpawnFlags( SF_NPC_NO_WEAPON_DROP ); // } return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPropCombineBall::OnHitEntity( CBaseEntity *pHitEntity, float flSpeed, int index, gamevcollisionevent_t *pEvent ) { // Detonate on the strider + the bone followers in the strider if ( FClassnameIs( pHitEntity, "npc_strider" ) || (pHitEntity->GetOwnerEntity() && FClassnameIs( pHitEntity->GetOwnerEntity(), "npc_strider" )) ) { DoExplosion(); return; } CTakeDamageInfo info( this, GetOwnerEntity(), GetAbsVelocity(), GetAbsOrigin(), sk_npc_dmg_combineball.GetFloat(), DMG_DISSOLVE ); bool bIsDissolving = (pHitEntity->GetFlags() & FL_DISSOLVING) != 0; bool bShouldHit = pHitEntity->PassesDamageFilter( info ); //One more check //Combine soldiers are not allowed to hurt their friends with combine balls (they can still shoot and hurt each other with grenades). CBaseCombatCharacter *pBCC = pHitEntity->MyCombatCharacterPointer(); if ( pBCC ) { bShouldHit = pBCC->IRelationType( GetOwnerEntity() ) != D_LI; } if ( !bIsDissolving && bShouldHit == true ) { if ( pHitEntity->PassesDamageFilter( info ) ) { if( WasFiredByNPC() || m_nMaxBounces == -1 ) { // Since Combine balls fired by NPCs do a metered dose of damage per impact, we have to ignore touches // for a little while after we hit someone, or the ball will immediately touch them again and do more // damage. if( gpGlobals->curtime >= m_flNextDamageTime ) { EmitSound( "NPC_CombineBall.KillImpact" ); if ( pHitEntity->IsNPC() && pHitEntity->Classify() != CLASS_PLAYER_ALLY_VITAL && hl2_episodic.GetBool() == true ) { if ( pHitEntity->Classify() != CLASS_PLAYER_ALLY || ( pHitEntity->Classify() == CLASS_PLAYER_ALLY && m_bStruckEntity == false ) ) { info.SetDamage( pHitEntity->GetMaxHealth() ); m_bStruckEntity = true; } } else { // Ignore touches briefly. m_flNextDamageTime = gpGlobals->curtime + 0.1f; } pHitEntity->TakeDamage( info ); } } else { if ( (m_nState == STATE_THROWN) && (pHitEntity->IsNPC() || dynamic_cast(pHitEntity) )) { EmitSound( "NPC_CombineBall.KillImpact" ); } if ( (m_nState != STATE_HOLDING) ) { CBasePlayer *pPlayer = ToBasePlayer( GetOwnerEntity() ); if ( pPlayer && UTIL_IsAR2CombineBall( this ) && ToBaseCombatCharacter( pHitEntity ) ) { gamestats->Event_WeaponHit( pPlayer, false, "weapon_ar2", info ); } DissolveEntity( pHitEntity ); if ( pHitEntity->ClassMatches( "npc_hunter" ) ) { DoExplosion(); return; } } } } } Vector vecFinalVelocity; if ( IsInField() ) { // Don't deflect when in a spawner field vecFinalVelocity = pEvent->preVelocity[index]; } else { // Don't slow down when hitting other entities. vecFinalVelocity = pEvent->postVelocity[index]; VectorNormalize( vecFinalVelocity ); vecFinalVelocity *= GetSpeed(); } PhysCallbackSetVelocity( pEvent->pObjects[index], vecFinalVelocity ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPropCombineBall::DoImpactEffect( const Vector &preVelocity, int index, gamevcollisionevent_t *pEvent ) { // Do that crazy impact effect! trace_t tr; CollisionEventToTrace( !index, pEvent, tr ); CBaseEntity *pTraceEntity = pEvent->pEntities[index]; UTIL_TraceLine( tr.startpos - preVelocity * 2.0f, tr.startpos + preVelocity * 2.0f, MASK_SOLID, pTraceEntity, COLLISION_GROUP_NONE, &tr ); if ( tr.fraction < 1.0f ) { // See if we hit the sky if ( tr.surface.flags & SURF_SKY ) { DoExplosion(); return; } // Send the effect over CEffectData data; data.m_flRadius = 16; data.m_vNormal = tr.plane.normal; data.m_vOrigin = tr.endpos + tr.plane.normal * 1.0f; DispatchEffect( "cball_bounce", data ); } if ( hl2_episodic.GetBool() ) { EmitSound( "NPC_CombineBall_Episodic.Impact" ); } else { EmitSound( "NPC_CombineBall.Impact" ); } } //----------------------------------------------------------------------------- // Tells whether this combine ball should consider deflecting towards this entity. //----------------------------------------------------------------------------- bool CPropCombineBall::IsAttractiveTarget( CBaseEntity *pEntity ) { if ( !pEntity->IsAlive() ) return false; if ( pEntity->GetFlags() & EF_NODRAW ) return false; // Don't guide toward striders if ( FClassnameIs( pEntity, "npc_strider" ) ) return false; if( WasFiredByNPC() ) { // Fired by an NPC if( !pEntity->IsNPC() && !pEntity->IsPlayer() ) return false; // Don't seek entities of the same class. if ( pEntity->m_iClassname == GetOwnerEntity()->m_iClassname ) return false; } else { #ifndef HL2MP if ( GetOwnerEntity() ) { // Things we check if this ball has an owner that's not an NPC. if( GetOwnerEntity()->IsPlayer() ) { if( pEntity->Classify() == CLASS_PLAYER || pEntity->Classify() == CLASS_PLAYER_ALLY || pEntity->Classify() == CLASS_PLAYER_ALLY_VITAL ) { // Not attracted to other players or allies. return false; } } } // The default case. if ( !pEntity->IsNPC() ) return false; if( pEntity->Classify() == CLASS_BULLSEYE ) return false; #else if ( pEntity->IsPlayer() == false ) return false; if ( pEntity == GetOwnerEntity() ) return false; //No tracking teammates in teammode! if ( g_pGameRules->IsTeamplay() ) { if ( g_pGameRules->PlayerRelationship( GetOwnerEntity(), pEntity ) == GR_TEAMMATE ) return false; } #endif // We must be able to hit them trace_t tr; UTIL_TraceLine( WorldSpaceCenter(), pEntity->BodyTarget( WorldSpaceCenter() ), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); if ( tr.fraction < 1.0f && tr.m_pEnt != pEntity ) return false; } return true; } //----------------------------------------------------------------------------- // Deflects the ball toward enemies in case of a collision //----------------------------------------------------------------------------- void CPropCombineBall::DeflectTowardEnemy( float flSpeed, int index, gamevcollisionevent_t *pEvent ) { // Bounce toward a particular enemy; choose one that's closest to my new velocity. Vector vecVelDir = pEvent->postVelocity[index]; VectorNormalize( vecVelDir ); CBaseEntity *pBestTarget = NULL; Vector vecStartPoint; pEvent->pInternalData->GetContactPoint( vecStartPoint ); float flBestDist = MAX_COORD_FLOAT; CBaseEntity *list[1024]; Vector vecDelta; float distance, flDot; // If we've already hit something, get accurate bool bSeekKill = m_bStruckEntity && (WasWeaponLaunched() || sk_combineball_seek_kill.GetInt() ); if ( bSeekKill ) { int nCount = UTIL_EntitiesInSphere( list, 1024, GetAbsOrigin(), sk_combine_ball_search_radius.GetFloat(), FL_NPC | FL_CLIENT ); for ( int i = 0; i < nCount; i++ ) { if ( !IsAttractiveTarget( list[i] ) ) continue; VectorSubtract( list[i]->WorldSpaceCenter(), vecStartPoint, vecDelta ); distance = VectorNormalize( vecDelta ); if ( distance < flBestDist ) { // Check our direction if ( DotProduct( vecDelta, vecVelDir ) > 0.0f ) { pBestTarget = list[i]; flBestDist = distance; } } } } else { float flMaxDot = 0.966f; if ( !WasWeaponLaunched() ) { float flMaxDot = sk_combineball_seek_angle.GetFloat(); float flGuideFactor = sk_combineball_guidefactor.GetFloat(); for ( int i = m_nBounceCount; --i >= 0; ) { flMaxDot *= flGuideFactor; } flMaxDot = cos( flMaxDot * M_PI / 180.0f ); if ( flMaxDot > 1.0f ) { flMaxDot = 1.0f; } } // Otherwise only help out a little Vector extents = Vector(256, 256, 256); Ray_t ray; ray.Init( vecStartPoint, vecStartPoint + 2048 * vecVelDir, -extents, extents ); int nCount = UTIL_EntitiesAlongRay( list, 1024, ray, FL_NPC | FL_CLIENT ); for ( int i = 0; i < nCount; i++ ) { if ( !IsAttractiveTarget( list[i] ) ) continue; VectorSubtract( list[i]->WorldSpaceCenter(), vecStartPoint, vecDelta ); distance = VectorNormalize( vecDelta ); flDot = DotProduct( vecDelta, vecVelDir ); if ( flDot > flMaxDot ) { if ( distance < flBestDist ) { pBestTarget = list[i]; flBestDist = distance; } } } } if ( pBestTarget ) { Vector vecDelta; VectorSubtract( pBestTarget->WorldSpaceCenter(), vecStartPoint, vecDelta ); VectorNormalize( vecDelta ); vecDelta *= GetSpeed(); PhysCallbackSetVelocity( pEvent->pObjects[index], vecDelta ); } } //----------------------------------------------------------------------------- // Bounce inside the spawner: //----------------------------------------------------------------------------- void CPropCombineBall::BounceInSpawner( float flSpeed, int index, gamevcollisionevent_t *pEvent ) { GetSpawner()->RegisterReflection( this, m_bForward ); m_bForward = !m_bForward; Vector vecTarget; GetSpawner()->GetTargetEndpoint( m_bForward, &vecTarget ); Vector vecVelocity; VectorSubtract( vecTarget, GetAbsOrigin(), vecVelocity ); VectorNormalize( vecVelocity ); vecVelocity *= flSpeed; PhysCallbackSetVelocity( pEvent->pObjects[index], vecVelocity ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CPropCombineBall::IsHittableEntity( CBaseEntity *pHitEntity ) { if ( pHitEntity->IsWorld() ) return false; if ( pHitEntity->GetMoveType() == MOVETYPE_PUSH ) { if( pHitEntity->GetOwnerEntity() && FClassnameIs(pHitEntity->GetOwnerEntity(), "npc_strider") ) { // The Strider's Bone Followers are MOVETYPE_PUSH, and we want the combine ball to hit these. return true; } // If the entity we hit can take damage, we're good if ( pHitEntity->m_takedamage == DAMAGE_YES ) return true; return false; } return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPropCombineBall::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) { Vector preVelocity = pEvent->preVelocity[index]; float flSpeed = VectorNormalize( preVelocity ); if ( m_nMaxBounces == -1 ) { const surfacedata_t *pHit = physprops->GetSurfaceData( pEvent->surfaceProps[!index] ); if( pHit->game.material != CHAR_TEX_FLESH || !hl2_episodic.GetBool() ) { CBaseEntity *pHitEntity = pEvent->pEntities[!index]; if ( pHitEntity && IsHittableEntity( pHitEntity ) ) { OnHitEntity( pHitEntity, flSpeed, index, pEvent ); } // Remove self without affecting the object that was hit. (Unless it was flesh) NotifySpawnerOfRemoval(); PhysCallbackRemove( this->NetworkProp() ); // disable dissolve damage so we don't kill off the player when he's the one we hit PhysClearGameFlags( VPhysicsGetObject(), FVPHYSICS_DMG_DISSOLVE ); return; } } // Prevents impact sounds, effects, etc. when it's in the field if ( !IsInField() ) { BaseClass::VPhysicsCollision( index, pEvent ); } if ( m_nState == STATE_HOLDING ) return; // If we've collided going faster than our desired, then up our desired if ( flSpeed > GetSpeed() ) { SetSpeed( flSpeed ); } // Make sure we don't slow down Vector vecFinalVelocity = pEvent->postVelocity[index]; VectorNormalize( vecFinalVelocity ); vecFinalVelocity *= GetSpeed(); PhysCallbackSetVelocity( pEvent->pObjects[index], vecFinalVelocity ); CBaseEntity *pHitEntity = pEvent->pEntities[!index]; if ( pHitEntity && IsHittableEntity( pHitEntity ) ) { OnHitEntity( pHitEntity, flSpeed, index, pEvent ); return; } if ( IsInField() ) { if ( HasSpawnFlags( SF_COMBINE_BALL_BOUNCING_IN_SPAWNER ) && GetSpawner() ) { BounceInSpawner( GetSpeed(), index, pEvent ); return; } PhysCallbackSetVelocity( pEvent->pObjects[index], vec3_origin ); // Delay the fade out so that we don't change our // collision rules inside a vphysics callback. variant_t emptyVariant; g_EventQueue.AddEvent( this, "FadeAndRespawn", 0.01, NULL, NULL ); return; } if ( IsBeingCaptured() ) return; // Do that crazy impact effect! DoImpactEffect( preVelocity, index, pEvent ); // Only do the bounce so often if ( gpGlobals->curtime - m_flLastBounceTime < 0.25f ) return; // Save off our last bounce time m_flLastBounceTime = gpGlobals->curtime; // Reset the sound timer SetContextThink( &CPropCombineBall::WhizSoundThink, gpGlobals->curtime + 0.01, s_pWhizThinkContext ); // Deflect towards nearby enemies DeflectTowardEnemy( flSpeed, index, pEvent ); // Once more bounce ++m_nBounceCount; if ( OutOfBounces() && m_bBounceDie == false ) { StartLifetime( 0.5 ); //Hack: Stop this from being called by doing this. m_bBounceDie = true; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CPropCombineBall::AnimThink( void ) { StudioFrameAdvance(); SetContextThink( &CPropCombineBall::AnimThink, gpGlobals->curtime + 0.1f, s_pAnimThinkContext ); } //----------------------------------------------------------------------------- // // Implementation of CPropCombineBall // //----------------------------------------------------------------------------- LINK_ENTITY_TO_CLASS( func_combine_ball_spawner, CFuncCombineBallSpawner ); //----------------------------------------------------------------------------- // Save/load: //----------------------------------------------------------------------------- BEGIN_DATADESC( CFuncCombineBallSpawner ) DEFINE_KEYFIELD( m_nBallCount, FIELD_INTEGER, "ballcount" ), DEFINE_KEYFIELD( m_flMinSpeed, FIELD_FLOAT, "minspeed" ), DEFINE_KEYFIELD( m_flMaxSpeed, FIELD_FLOAT, "maxspeed" ), DEFINE_KEYFIELD( m_flBallRadius, FIELD_FLOAT, "ballradius" ), DEFINE_KEYFIELD( m_flBallRespawnTime, FIELD_FLOAT, "ballrespawntime" ), DEFINE_FIELD( m_flRadius, FIELD_FLOAT ), DEFINE_FIELD( m_nBallsRemainingInField, FIELD_INTEGER ), DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), DEFINE_UTLVECTOR( m_BallRespawnTime, FIELD_TIME ), DEFINE_FIELD( m_flDisableTime, FIELD_TIME ), DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), DEFINE_OUTPUT( m_OnBallGrabbed, "OnBallGrabbed" ), DEFINE_OUTPUT( m_OnBallReinserted, "OnBallReinserted" ), DEFINE_OUTPUT( m_OnBallHitTopSide, "OnBallHitTopSide" ), DEFINE_OUTPUT( m_OnBallHitBottomSide, "OnBallHitBottomSide" ), DEFINE_OUTPUT( m_OnLastBallGrabbed, "OnLastBallGrabbed" ), DEFINE_OUTPUT( m_OnFirstBallReinserted, "OnFirstBallReinserted" ), DEFINE_THINKFUNC( BallThink ), DEFINE_ENTITYFUNC( GrabBallTouch ), END_DATADESC() //----------------------------------------------------------------------------- // Purpose: Constructor //----------------------------------------------------------------------------- CFuncCombineBallSpawner::CFuncCombineBallSpawner() { m_flBallRespawnTime = 0.0f; m_flBallRadius = 20.0f; m_flDisableTime = 0.0f; m_bShooter = false; } //----------------------------------------------------------------------------- // Spawn a ball //----------------------------------------------------------------------------- void CFuncCombineBallSpawner::SpawnBall() { CPropCombineBall *pBall = static_cast( CreateEntityByName( "prop_combine_ball" ) ); float flRadius = m_flBallRadius; pBall->SetRadius( flRadius ); Vector vecAbsOrigin; ChoosePointInBox( &vecAbsOrigin ); Vector zaxis; MatrixGetColumn( EntityToWorldTransform(), 2, zaxis ); VectorMA( vecAbsOrigin, flRadius, zaxis, vecAbsOrigin ); pBall->SetAbsOrigin( vecAbsOrigin ); pBall->SetSpawner( this ); float flSpeed = random->RandomFloat( m_flMinSpeed, m_flMaxSpeed ); zaxis *= flSpeed; pBall->SetAbsVelocity( zaxis ); if ( HasSpawnFlags( SF_SPAWNER_POWER_SUPPLY ) ) { pBall->AddSpawnFlags( SF_COMBINE_BALL_BOUNCING_IN_SPAWNER ); } pBall->Spawn(); } void CFuncCombineBallSpawner::Precache() { BaseClass::Precache(); UTIL_PrecacheOther( "prop_combine_ball" ); } //----------------------------------------------------------------------------- // Spawn //----------------------------------------------------------------------------- void CFuncCombineBallSpawner::Spawn() { BaseClass::Spawn(); Precache(); AddEffects( EF_NODRAW ); SetModel( STRING( GetModelName() ) ); SetSolid( SOLID_BSP ); AddSolidFlags( FSOLID_NOT_SOLID ); m_nBallsRemainingInField = m_nBallCount; float flWidth = CollisionProp()->OBBSize().x; float flHeight = CollisionProp()->OBBSize().y; m_flRadius = MIN( flWidth, flHeight ) * 0.5f; if ( m_flRadius <= 0.0f && m_bShooter == false ) { Warning("Zero dimension func_combine_ball_spawner! Removing...\n"); UTIL_Remove( this ); return; } // Compute a respawn time float flDeltaT = 1.0f; if ( !( m_flMinSpeed == 0 && m_flMaxSpeed == 0 ) ) { flDeltaT = (CollisionProp()->OBBSize().z - 2 * m_flBallRadius) / ((m_flMinSpeed + m_flMaxSpeed) * 0.5f); flDeltaT /= m_nBallCount; } m_BallRespawnTime.EnsureCapacity( m_nBallCount ); for ( int i = 0; i < m_nBallCount; ++i ) { RespawnBall( (float)i * flDeltaT ); } m_bEnabled = true; if ( HasSpawnFlags( SF_SPAWNER_START_DISABLED ) ) { inputdata_t inputData; InputDisable( inputData ); } else { SetThink( &CFuncCombineBallSpawner::BallThink ); SetNextThink( gpGlobals->curtime + 0.1f ); } } //----------------------------------------------------------------------------- // Enable/disable //----------------------------------------------------------------------------- void CFuncCombineBallSpawner::InputEnable( inputdata_t &inputdata ) { if ( m_bEnabled ) return; m_bEnabled = true; m_flDisableTime = 0.0f; for ( int i = m_BallRespawnTime.Count(); --i >= 0; ) { m_BallRespawnTime[i] += gpGlobals->curtime; } SetThink( &CFuncCombineBallSpawner::BallThink ); SetNextThink( gpGlobals->curtime + 0.1f ); } void CFuncCombineBallSpawner::InputDisable( inputdata_t &inputdata ) { if ( !m_bEnabled ) return; m_flDisableTime = gpGlobals->curtime; m_bEnabled = false; for ( int i = m_BallRespawnTime.Count(); --i >= 0; ) { m_BallRespawnTime[i] -= gpGlobals->curtime; } SetThink( NULL ); } //----------------------------------------------------------------------------- // Choose a random point inside the cylinder //----------------------------------------------------------------------------- void CFuncCombineBallSpawner::ChoosePointInBox( Vector *pVecPoint ) { float flXBoundary = ( CollisionProp()->OBBSize().x != 0 ) ? m_flBallRadius / CollisionProp()->OBBSize().x : 0.0f; float flYBoundary = ( CollisionProp()->OBBSize().y != 0 ) ? m_flBallRadius / CollisionProp()->OBBSize().y : 0.0f; if ( flXBoundary > 0.5f ) { flXBoundary = 0.5f; } if ( flYBoundary > 0.5f ) { flYBoundary = 0.5f; } CollisionProp()->RandomPointInBounds( Vector( flXBoundary, flYBoundary, 0.0f ), Vector( 1.0f - flXBoundary, 1.0f - flYBoundary, 0.0f ), pVecPoint ); } //----------------------------------------------------------------------------- // Choose a random point inside the cylinder //----------------------------------------------------------------------------- void CFuncCombineBallSpawner::ChoosePointInCylinder( Vector *pVecPoint ) { float flXRange = m_flRadius / CollisionProp()->OBBSize().x; float flYRange = m_flRadius / CollisionProp()->OBBSize().y; Vector vecEndPoint1, vecEndPoint2; CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 0.0f ), &vecEndPoint1 ); CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 1.0f ), &vecEndPoint2 ); // Choose a point inside the cylinder float flDistSq; do { CollisionProp()->RandomPointInBounds( Vector( 0.5f - flXRange, 0.5f - flYRange, 0.0f ), Vector( 0.5f + flXRange, 0.5f + flYRange, 0.0f ), pVecPoint ); flDistSq = CalcDistanceSqrToLine( *pVecPoint, vecEndPoint1, vecEndPoint2 ); } while ( flDistSq > m_flRadius * m_flRadius ); } //----------------------------------------------------------------------------- // Register that a reflection occurred //----------------------------------------------------------------------------- void CFuncCombineBallSpawner::RegisterReflection( CPropCombineBall *pBall, bool bForward ) { if ( bForward ) { m_OnBallHitTopSide.FireOutput( pBall, this ); } else { m_OnBallHitBottomSide.FireOutput( pBall, this ); } } //----------------------------------------------------------------------------- // Choose a random point on the //----------------------------------------------------------------------------- void CFuncCombineBallSpawner::GetTargetEndpoint( bool bForward, Vector *pVecEndPoint ) { float flZValue = bForward ? 1.0f : 0.0f; CollisionProp()->RandomPointInBounds( Vector( 0.0f, 0.0f, flZValue ), Vector( 1.0f, 1.0f, flZValue ), pVecEndPoint ); } //----------------------------------------------------------------------------- // Fire ball grabbed output //----------------------------------------------------------------------------- void CFuncCombineBallSpawner::BallGrabbed( CBaseEntity *pCombineBall ) { m_OnBallGrabbed.FireOutput( pCombineBall, this ); --m_nBallsRemainingInField; if ( m_nBallsRemainingInField == 0 ) { m_OnLastBallGrabbed.FireOutput( pCombineBall, this ); } // Wait for another ball to touch this to re-power it up. if ( HasSpawnFlags( SF_SPAWNER_POWER_SUPPLY ) ) { AddSolidFlags( FSOLID_TRIGGER ); SetTouch( &CFuncCombineBallSpawner::GrabBallTouch ); } // Stop the ball thinking in case it was in the middle of being captured (which could re-add incorrectly) if ( pCombineBall != NULL ) { pCombineBall->SetContextThink( NULL, gpGlobals->curtime, s_pCaptureContext ); } } //----------------------------------------------------------------------------- // Fire ball grabbed output //----------------------------------------------------------------------------- void CFuncCombineBallSpawner::GrabBallTouch( CBaseEntity *pOther ) { // Safety net for two balls hitting this at once if ( m_nBallsRemainingInField >= m_nBallCount ) return; if ( pOther->GetCollisionGroup() != HL2COLLISION_GROUP_COMBINE_BALL ) return; CPropCombineBall *pBall = dynamic_cast( pOther ); Assert( pBall ); // Don't grab AR2 alt-fire if ( pBall->WasWeaponLaunched() || !pBall->VPhysicsGetObject() ) return; // Don't grab balls that are already in the field.. if ( pBall->IsInField() ) return; // Don't grab fading out balls... if ( !pBall->IsSolid() ) return; // Don't capture balls that were very recently in the field (breaks punting) if ( gpGlobals->curtime - pBall->LastCaptureTime() < 0.5f ) return; // Now we're bouncing in this spawner pBall->AddSpawnFlags( SF_COMBINE_BALL_BOUNCING_IN_SPAWNER ); // Tell the respawner we're no longer its ball pBall->NotifySpawnerOfRemoval(); pBall->SetOwnerEntity( NULL ); pBall->SetSpawner( this ); pBall->CaptureBySpawner(); ++m_nBallsRemainingInField; if ( m_nBallsRemainingInField >= m_nBallCount ) { RemoveSolidFlags( FSOLID_TRIGGER ); SetTouch( NULL ); } m_OnBallReinserted.FireOutput( pBall, this ); if ( m_nBallsRemainingInField == 1 ) { m_OnFirstBallReinserted.FireOutput( pBall, this ); } } //----------------------------------------------------------------------------- // Get a speed for the ball to insert //----------------------------------------------------------------------------- float CFuncCombineBallSpawner::GetBallSpeed( ) const { return random->RandomFloat( m_flMinSpeed, m_flMaxSpeed ); } //----------------------------------------------------------------------------- // Balls call this when they've been removed from the spawner //----------------------------------------------------------------------------- void CFuncCombineBallSpawner::RespawnBall( float flRespawnTime ) { // Insert the time in sorted order, // which by definition means to always insert at the start m_BallRespawnTime.AddToTail( gpGlobals->curtime + flRespawnTime - m_flDisableTime ); } //----------------------------------------------------------------------------- // //----------------------------------------------------------------------------- void CFuncCombineBallSpawner::RespawnBallPostExplosion( void ) { if ( m_flBallRespawnTime < 0 ) return; if ( m_flBallRespawnTime == 0.0f ) { m_BallRespawnTime.AddToTail( gpGlobals->curtime + 4.0f - m_flDisableTime ); } else { m_BallRespawnTime.AddToTail( gpGlobals->curtime + m_flBallRespawnTime - m_flDisableTime ); } } //----------------------------------------------------------------------------- // Ball think //----------------------------------------------------------------------------- void CFuncCombineBallSpawner::BallThink() { for ( int i = m_BallRespawnTime.Count(); --i >= 0; ) { if ( m_BallRespawnTime[i] < gpGlobals->curtime ) { SpawnBall(); m_BallRespawnTime.FastRemove( i ); } } // There are no more to respawn SetNextThink( gpGlobals->curtime + 0.1f ); } BEGIN_DATADESC( CPointCombineBallLauncher ) DEFINE_KEYFIELD( m_flConeDegrees, FIELD_FLOAT, "launchconenoise" ), DEFINE_KEYFIELD( m_iszBullseyeName, FIELD_STRING, "bullseyename" ), DEFINE_KEYFIELD( m_iBounces, FIELD_INTEGER, "maxballbounces" ), DEFINE_INPUTFUNC( FIELD_VOID, "LaunchBall", InputLaunchBall ), END_DATADESC() #define SF_COMBINE_BALL_LAUNCHER_ATTACH_BULLSEYE 0x00000001 #define SF_COMBINE_BALL_LAUNCHER_COLLIDE_PLAYER 0x00000002 LINK_ENTITY_TO_CLASS( point_combine_ball_launcher, CPointCombineBallLauncher ); CPointCombineBallLauncher::CPointCombineBallLauncher() { m_bShooter = true; m_flConeDegrees = 0.0f; m_iBounces = 0; } void CPointCombineBallLauncher::Spawn( void ) { m_bShooter = true; BaseClass::Spawn(); } void CPointCombineBallLauncher::InputLaunchBall ( inputdata_t &inputdata ) { SpawnBall(); } //----------------------------------------------------------------------------- // Spawn a ball //----------------------------------------------------------------------------- void CPointCombineBallLauncher::SpawnBall() { CPropCombineBall *pBall = static_cast( CreateEntityByName( "prop_combine_ball" ) ); if ( pBall == NULL ) return; float flRadius = m_flBallRadius; pBall->SetRadius( flRadius ); Vector vecAbsOrigin = GetAbsOrigin(); Vector zaxis; pBall->SetAbsOrigin( vecAbsOrigin ); pBall->SetSpawner( this ); float flSpeed = random->RandomFloat( m_flMinSpeed, m_flMaxSpeed ); Vector vDirection; QAngle qAngle = GetAbsAngles(); qAngle = qAngle + QAngle ( random->RandomFloat( -m_flConeDegrees, m_flConeDegrees ), random->RandomFloat( -m_flConeDegrees, m_flConeDegrees ), 0 ); AngleVectors( qAngle, &vDirection, NULL, NULL ); vDirection *= flSpeed; pBall->SetAbsVelocity( vDirection ); DispatchSpawn(pBall); pBall->Activate(); pBall->SetState( CPropCombineBall::STATE_LAUNCHED ); pBall->SetMaxBounces( m_iBounces ); if ( HasSpawnFlags( SF_COMBINE_BALL_LAUNCHER_COLLIDE_PLAYER ) ) { pBall->SetCollisionGroup( HL2COLLISION_GROUP_COMBINE_BALL_NPC ); } if( GetSpawnFlags() & SF_COMBINE_BALL_LAUNCHER_ATTACH_BULLSEYE ) { CNPC_Bullseye *pBullseye = static_cast( CreateEntityByName( "npc_bullseye" ) ); if( pBullseye ) { pBullseye->SetAbsOrigin( pBall->GetAbsOrigin() ); pBullseye->SetAbsAngles( QAngle( 0, 0, 0 ) ); pBullseye->KeyValue( "solid", "6" ); pBullseye->KeyValue( "targetname", STRING(m_iszBullseyeName) ); pBullseye->Spawn(); DispatchSpawn(pBullseye); pBullseye->Activate(); pBullseye->SetParent(pBall); pBullseye->SetHealth(10); } } } // ################################################################### // > FilterClass // ################################################################### class CFilterCombineBall : public CBaseFilter { DECLARE_CLASS( CFilterCombineBall, CBaseFilter ); DECLARE_DATADESC(); public: int m_iBallType; bool PassesFilterImpl( CBaseEntity *pCaller, CBaseEntity *pEntity ) { CPropCombineBall *pBall = dynamic_cast(pEntity ); if ( pBall ) { //Playtest HACK: If we have an NPC owner then we were shot from an AR2. if ( pBall->GetOwnerEntity() && pBall->GetOwnerEntity()->IsNPC() ) return false; return pBall->GetState() == m_iBallType; } return false; } }; LINK_ENTITY_TO_CLASS( filter_combineball_type, CFilterCombineBall ); BEGIN_DATADESC( CFilterCombineBall ) // Keyfields DEFINE_KEYFIELD( m_iBallType, FIELD_INTEGER, "balltype" ), END_DATADESC()