//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Companion NPCs riding in cars // //============================================================================= #include "cbase.h" #include "ai_speech.h" #include "ai_pathfinder.h" #include "ai_waypoint.h" #include "ai_navigator.h" #include "ai_navgoaltype.h" #include "ai_memory.h" #include "ai_behavior_passenger_companion.h" #include "ai_squadslot.h" #include "npc_playercompanion.h" #include "ai_route.h" #include "saverestore_utlvector.h" #include "cplane.h" #include "util_shared.h" #include "sceneentity.h" bool SphereWithinPlayerFOV( CBasePlayer *pPlayer, const Vector &vecCenter, float flRadius ); #define PASSENGER_NEAR_VEHICLE_THRESHOLD 64.0f #define MIN_OVERTURNED_DURATION 1.0f // seconds #define MIN_FAILED_EXIT_ATTEMPTS 4 #define MIN_OVERTURNED_WARN_DURATION 4.0f // seconds ConVar passenger_collision_response_threshold( "passenger_collision_response_threshold", "250.0" ); ConVar passenger_debug_entry( "passenger_debug_entry", "0" ); ConVar passenger_use_leaning("passenger_use_leaning", "1" ); extern ConVar passenger_debug_transition; // Custom activities Activity ACT_PASSENGER_IDLE_AIM; Activity ACT_PASSENGER_RELOAD; Activity ACT_PASSENGER_OVERTURNED; Activity ACT_PASSENGER_IMPACT; Activity ACT_PASSENGER_IMPACT_WEAPON; Activity ACT_PASSENGER_POINT; Activity ACT_PASSENGER_POINT_BEHIND; Activity ACT_PASSENGER_IDLE_READY; Activity ACT_PASSENGER_GESTURE_JOSTLE_LARGE; Activity ACT_PASSENGER_GESTURE_JOSTLE_SMALL; Activity ACT_PASSENGER_GESTURE_JOSTLE_LARGE_STIMULATED; Activity ACT_PASSENGER_GESTURE_JOSTLE_SMALL_STIMULATED; Activity ACT_PASSENGER_COWER_IN; Activity ACT_PASSENGER_COWER_LOOP; Activity ACT_PASSENGER_COWER_OUT; Activity ACT_PASSENGER_IDLE_FIDGET; BEGIN_DATADESC( CAI_PassengerBehaviorCompanion ) DEFINE_EMBEDDED( m_VehicleMonitor ), DEFINE_UTLVECTOR( m_FailedEntryPositions, FIELD_EMBEDDED ), DEFINE_FIELD( m_flOverturnedDuration, FIELD_FLOAT ), DEFINE_FIELD( m_flUnseenDuration, FIELD_FLOAT ), DEFINE_FIELD( m_nExitAttempts, FIELD_INTEGER ), DEFINE_FIELD( m_flNextOverturnWarning, FIELD_TIME ), DEFINE_FIELD( m_flEnterBeginTime, FIELD_TIME ), DEFINE_FIELD( m_hCompanion, FIELD_EHANDLE ), DEFINE_FIELD( m_flNextJostleTime, FIELD_TIME ), DEFINE_FIELD( m_nVisibleEnemies, FIELD_INTEGER ), DEFINE_FIELD( m_flLastLateralLean, FIELD_FLOAT ), DEFINE_FIELD( m_flEntraceUpdateTime, FIELD_TIME ), DEFINE_FIELD( m_flNextEnterAttempt, FIELD_TIME ), DEFINE_FIELD( m_flNextFidgetTime, FIELD_TIME ), END_DATADESC(); BEGIN_SIMPLE_DATADESC( FailPosition_t ) DEFINE_FIELD( vecPosition, FIELD_VECTOR ), DEFINE_FIELD( flTime, FIELD_TIME ), END_DATADESC(); CAI_PassengerBehaviorCompanion::CAI_PassengerBehaviorCompanion( void ) : m_flUnseenDuration( 0.0f ), m_flNextOverturnWarning( 0.0f ), m_flOverturnedDuration( 0.0f ), m_nExitAttempts( 0 ), m_flNextEnterAttempt( 0.0f ), m_flLastLateralLean( 0.0f ), m_flNextJostleTime( 0.0f ) { memset( &m_vehicleState, 0, sizeof( m_vehicleState ) ); m_VehicleMonitor.ClearMark(); } void CAI_PassengerBehaviorCompanion::Enable( CPropJeepEpisodic *pVehicle, bool bImmediateEnter /*= false*/ ) { BaseClass::Enable( pVehicle ); // Store this up for quick reference later on m_hCompanion = dynamic_cast(GetOuter()); // See if we want to sit in the vehicle immediately if ( bImmediateEnter ) { // Find the seat and sit in it if ( ReserveEntryPoint( VEHICLE_SEAT_ANY ) ) { // Attach AttachToVehicle(); // This will slam us into the right position and clean up FinishEnterVehicle(); GetOuter()->IncrementInterpolationFrame(); // Start our schedule immediately ClearSchedule( "Immediate entry to vehicle" ); } } } //----------------------------------------------------------------------------- // Set up the shot regulator based on the equipped weapon //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::OnUpdateShotRegulator( void ) { if ( GetVehicleSpeed() > 250 ) { // Default values GetOuter()->GetShotRegulator()->SetBurstInterval( 0.1f, 0.5f ); GetOuter()->GetShotRegulator()->SetBurstShotCountRange( 1, 4 ); GetOuter()->GetShotRegulator()->SetRestInterval( 0.25f, 1.0f ); } else { BaseClass::OnUpdateShotRegulator(); } } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::IsValidEnemy( CBaseEntity *pEntity ) { // The target must be much closer in the vehicle float flDistSqr = ( pEntity->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); if ( flDistSqr > Square( (40*12) ) && pEntity->Classify() != CLASS_BULLSEYE ) return false; // Determine if the target is going to move past us? return BaseClass::IsValidEnemy( pEntity ); } //----------------------------------------------------------------------------- // Purpose: Returns the speed the vehicle is moving at // Output : units per second //----------------------------------------------------------------------------- float CAI_PassengerBehaviorCompanion::GetVehicleSpeed( void ) { if ( m_hVehicle == NULL ) { Assert(0); return -1.0f; } Vector vecVelocity; m_hVehicle->GetVelocity( &vecVelocity, NULL ); // Get our speed return vecVelocity.Length(); } //----------------------------------------------------------------------------- // Purpose: Detect oncoming collisions //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::GatherVehicleCollisionConditions( const Vector &localVelocity ) { // Look for walls in front of us if ( localVelocity.y > passenger_collision_response_threshold.GetFloat() ) { // Detect an upcoming collision Vector vForward; m_hVehicle->GetVectors( &vForward, NULL, NULL ); // Use a smaller bounding box to make it detect mostly head-on impacts Vector mins, maxs; mins.Init( -24, -24, 32 ); maxs.Init( 24, 24, 64 ); float dt = 0.6f; // Seconds float distance = localVelocity.y * dt; // Find our angular velocity as a vector Vector vecAngularVelocity; vecAngularVelocity.z = 0.0f; SinCos( DEG2RAD( m_vehicleState.m_vecLastAngles.z * dt ), &vecAngularVelocity.y, &vecAngularVelocity.x ); Vector vecOffset; VectorRotate( vecAngularVelocity, m_hVehicle->GetAbsAngles() + QAngle( 0, 90, 0 ), vecOffset ); vForward += vecOffset; VectorNormalize( vForward ); // Trace ahead of us to see what's there CTraceFilterNoNPCsOrPlayer filter( m_hVehicle, COLLISION_GROUP_NONE ); // We don't care about NPCs or the player (certainly if they're in the vehicle!) trace_t tr; UTIL_TraceHull( m_hVehicle->GetAbsOrigin(), m_hVehicle->GetAbsOrigin() + ( vForward * distance ), mins, maxs, MASK_SOLID, &filter, &tr ); bool bWarnCollision = true; if ( tr.DidHit() ) { // We need to see how "head-on" to the surface we are float impactDot = DotProduct( tr.plane.normal, vForward ); // Don't warn over grazing blows or slopes if ( impactDot < -0.9f && tr.plane.normal.z < 0.75f ) { // Make sure this is a worthwhile thing to warn about if ( tr.m_pEnt ) { // If it's physical and moveable, then ignore it because we'll probably smash or move it IPhysicsObject *pObject = tr.m_pEnt->VPhysicsGetObject(); if ( pObject && pObject->IsMoveable() ) { bWarnCollision = false; } } // Note that we should say something to the player about it if ( bWarnCollision ) { SetCondition( COND_PASSENGER_WARN_COLLISION ); } } } } if ( passenger_use_leaning.GetBool() ) { // Calculate how our body is leaning CalculateBodyLean(); } } //----------------------------------------------------------------------------- // Purpose: Speak various lines about the state of the vehicle //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::SpeakVehicleConditions( void ) { Assert( m_hVehicle != NULL ); if ( m_hVehicle == NULL ) return; // Speak if we just hit something if ( HasCondition( COND_PASSENGER_HARD_IMPACT ) ) { SpeakIfAllowed( TLK_PASSENGER_IMPACT ); } // Speak if we're overturned if ( HasCondition( COND_PASSENGER_OVERTURNED ) ) { SpeakIfAllowed( TLK_PASSENGER_OVERTURNED ); } // Speak if we're about to hit something if ( HasCondition( COND_PASSENGER_WARN_COLLISION ) ) { // Make Alyx look at the impending impact Vector vecForward; m_hVehicle->GetVectors( &vecForward, NULL, NULL ); Vector vecLookPos = m_hVehicle->WorldSpaceCenter() + ( vecForward * 64.0f ); GetOuter()->AddLookTarget( vecLookPos, 1.0f, 1.0f ); SpeakIfAllowed( TLK_PASSENGER_WARN_COLLISION ); ClearCondition( COND_PASSENGER_WARN_COLLISION ); } // Speak if the player is driving like a madman if ( HasCondition( COND_PASSENGER_ERRATIC_DRIVING ) ) { SpeakIfAllowed( TLK_PASSENGER_ERRATIC_DRIVING ); } // The vehicle has come to a halt if ( HasCondition( COND_PASSENGER_VEHICLE_STOPPED ) ) { float flDist = ( WorldSpaceCenter() - m_hVehicle->WorldSpaceCenter() ).Length(); CFmtStrN<128> modifiers( "vehicle_distance:%f", flDist ); SpeakIfAllowed( TLK_PASSENGER_VEHICLE_STOPPED, modifiers ); } // The vehicle has started to move if ( HasCondition( COND_PASSENGER_VEHICLE_STARTED ) ) { float flDist = ( WorldSpaceCenter() - m_hVehicle->WorldSpaceCenter() ).Length(); CFmtStrN<128> modifiers( "vehicle_distance:%f", flDist ); SpeakIfAllowed( TLK_PASSENGER_VEHICLE_STARTED, modifiers ); } // Player got in if ( HasCondition( COND_PASSENGER_PLAYER_EXITED_VEHICLE ) ) { CPropJeepEpisodic *pJalopy = dynamic_cast(m_hVehicle.Get()); if( pJalopy != NULL && pJalopy->NumRadarContacts() > 0 ) { SpeakIfAllowed( TLK_PASSENGER_PLAYER_EXITED, "radar_has_targets" ); } else { SpeakIfAllowed( TLK_PASSENGER_PLAYER_EXITED ); } } // Player got out if ( HasCondition( COND_PASSENGER_PLAYER_ENTERED_VEHICLE ) ) { SpeakIfAllowed( TLK_PASSENGER_PLAYER_ENTERED ); } } //----------------------------------------------------------------------------- // Purpose: Whether or not we should jostle at this moment // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::CanPlayJostle( bool bLargeJostle ) { // We've been told to suppress the jostle if ( m_flNextJostleTime > gpGlobals->curtime ) return false; // Can't do this if we're at a high readiness level if ( m_hCompanion && m_hCompanion->ShouldBeAiming() ) return false; // Can't do this when we're upside-down if ( HasCondition( COND_PASSENGER_OVERTURNED ) ) return false; // Allow our normal impact code to handle this one instead if ( HasCondition( COND_PASSENGER_HARD_IMPACT ) || IsCurSchedule( SCHED_PASSENGER_IMPACT ) ) return false; if ( bLargeJostle ) { // Don't bother under certain circumstances if ( IsCurSchedule( SCHED_PASSENGER_COWER ) || IsCurSchedule( SCHED_PASSENGER_FIDGET ) ) return false; } else { // Don't interrupt a larger gesture if ( GetOuter()->IsPlayingGesture( ACT_PASSENGER_GESTURE_JOSTLE_LARGE ) || GetOuter()->IsPlayingGesture( ACT_PASSENGER_GESTURE_JOSTLE_LARGE_STIMULATED ) ) return false; } return true; } //----------------------------------------------------------------------------- // Purpose: Gather conditions we can comment on or react to while riding in the vehicle //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::GatherVehicleStateConditions( void ) { // Gather the base class BaseClass::GatherVehicleStateConditions(); // See if we're going to collide with anything soon GatherVehicleCollisionConditions( m_vehicleState.m_vecLastLocalVelocity ); // Say anything we're meant to through the response rules SpeakVehicleConditions(); } //----------------------------------------------------------------------------- // Purpose: Handles exit failure notifications //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::OnExitVehicleFailed( void ) { m_nExitAttempts++; } //----------------------------------------------------------------------------- // Purpose: Track how long we've been overturned //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::UpdateStuckStatus( void ) { if ( m_hVehicle == NULL ) return; // Always clear this to start out with ClearCondition( COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE ); // If we can't exit the vehicle, then don't bother with these checks if ( m_hVehicle->NPC_CanExitVehicle( GetOuter(), true ) == false ) return; bool bVisibleToPlayer = false; bool bPlayerInVehicle = false; CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); if ( pPlayer ) { bVisibleToPlayer = pPlayer->FInViewCone( GetOuter()->GetAbsOrigin() ); bPlayerInVehicle = pPlayer->IsInAVehicle(); } // If we're not overturned, just reset our counter if ( m_vehicleState.m_bWasOverturned == false ) { m_flOverturnedDuration = 0.0f; m_flUnseenDuration = 0.0f; } else { // Add up the time since we last checked m_flOverturnedDuration += ( gpGlobals->curtime - GetLastThink() ); } // Warn about being stuck upside-down if it's been long enough if ( m_flOverturnedDuration > MIN_OVERTURNED_WARN_DURATION && m_flNextOverturnWarning < gpGlobals->curtime ) { SetCondition( COND_PASSENGER_WARN_OVERTURNED ); } // If the player can see us or is still in the vehicle, we never exit if ( bVisibleToPlayer || bPlayerInVehicle ) { // Reset our timer m_flUnseenDuration = 0.0f; return; } // Add up the time since we last checked m_flUnseenDuration += ( gpGlobals->curtime - GetLastThink() ); // If we've been overturned for long enough or tried to exit one too many times if ( m_vehicleState.m_bWasOverturned ) { if ( m_flUnseenDuration > MIN_OVERTURNED_DURATION ) { SetCondition( COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE ); } } else if ( m_nExitAttempts >= MIN_FAILED_EXIT_ATTEMPTS ) { // The player can't be looking at us SetCondition( COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE ); } } //----------------------------------------------------------------------------- // Purpose: Gather conditions for our use in making decisions //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::GatherConditions( void ) { // Code below relies on these conditions being set first! BaseClass::GatherConditions(); // We're not enabled if ( IsEnabled() == false ) return; // In-car conditions if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) { // If we're jostling, then note that if ( HasCondition( COND_PASSENGER_ERRATIC_DRIVING ) ) { if ( CanPlayJostle( true ) ) { // Add the gesture to be played. If it's already playing, the underlying function will simply opt-out int nSequence = GetOuter()->AddGesture( GetOuter()->NPC_TranslateActivity( ACT_PASSENGER_GESTURE_JOSTLE_LARGE ), true ); GetOuter()->SetNextAttack( gpGlobals->curtime + ( GetOuter()->SequenceDuration( nSequence ) * 2.0f ) ); GetOuter()->GetShotRegulator()->FireNoEarlierThan( GetOuter()->GetNextAttack() ); // Push out our fidget into the future so that we don't act unnaturally over bumpy terrain ExtendFidgetDelay( random->RandomFloat( 1.5f, 3.0f ) ); } } else if ( HasCondition( COND_PASSENGER_JOSTLE_SMALL ) ) { if ( CanPlayJostle( false ) ) { // Add the gesture to be played. If it's already playing, the underlying function will simply opt-out GetOuter()->AddGesture( GetOuter()->NPC_TranslateActivity( ACT_PASSENGER_GESTURE_JOSTLE_SMALL ), true ); // Push out our fidget into the future so that we don't act unnaturally over bumpy terrain ExtendFidgetDelay( random->RandomFloat( 1.5f, 3.0f ) ); } } // See if we're upside-down UpdateStuckStatus(); // See if we're able to fidget if ( CanFidget() ) { SetCondition( COND_PASSENGER_CAN_FIDGET ); } } // Clear this out ClearCondition( COND_PASSENGER_CAN_ENTER_IMMEDIATELY ); // Make sure a vehicle doesn't stray from its mark if ( IsCurSchedule( SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE ) ) { if ( m_VehicleMonitor.TargetMoved( m_hVehicle ) ) { SetCondition( COND_PASSENGER_VEHICLE_MOVED_FROM_MARK ); } // If we can get in the car right away, set us up to do so int nNearestSequence; if ( CanEnterVehicleImmediately( &nNearestSequence, &m_vecTargetPosition, &m_vecTargetAngles ) ) { SetTransitionSequence( nNearestSequence ); SetCondition( COND_PASSENGER_ENTERING ); SetCondition( COND_PASSENGER_CAN_ENTER_IMMEDIATELY ); } } // Clear the number for now m_nVisibleEnemies = 0; AIEnemiesIter_t iter; for( AI_EnemyInfo_t *pEMemory = GetEnemies()->GetFirst(&iter); pEMemory != NULL; pEMemory = GetEnemies()->GetNext(&iter) ) { if( GetOuter()->IRelationType( pEMemory->hEnemy ) == D_HT ) { if( pEMemory->timeLastSeen == gpGlobals->curtime ) { m_nVisibleEnemies++; } } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::AimGun( void ) { // If there is no aiming target, return to center if ( GetEnemy() == NULL ) { GetOuter()->RelaxAim(); return; } // Otherwise try and shoot down the barrel Vector vecForward, vecRight, vecUp; GetOuter()->GetVectors( &vecForward, &vecRight, &vecUp ); Vector vecTorso = GetAbsOrigin() + ( vecUp * 48.0f ); Vector vecShootDir = GetOuter()->GetShootEnemyDir( vecTorso, false ); Vector vecDirToEnemy = GetEnemy()->GetAbsOrigin() - vecTorso; VectorNormalize( vecDirToEnemy ); bool bRightSide = ( DotProduct( vecDirToEnemy, vecRight ) > 0.0f ); float flTargetDot = ( bRightSide ) ? -0.7f : 0.0f; if ( DotProduct( vecForward, vecDirToEnemy ) <= flTargetDot ) { // Don't aim at something that's outside our reach GetOuter()->RelaxAim(); } else { // Aim at it GetOuter()->SetAim( vecShootDir ); } } //----------------------------------------------------------------------------- // Purpose: Allow us to deny selecting a schedule if we're not in a state to do so //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::CanSelectSchedule( void ) { if ( BaseClass::CanSelectSchedule() == false ) return false; // We're in a period where we're allowing our base class to override us if ( m_flNextEnterAttempt > gpGlobals->curtime ) return false; return true; } //----------------------------------------------------------------------------- // Purpose: Deal with enter/exit of the vehicle //----------------------------------------------------------------------------- int CAI_PassengerBehaviorCompanion::SelectTransitionSchedule( void ) { // Attempt to instantly enter the vehicle if ( HasCondition( COND_PASSENGER_CAN_ENTER_IMMEDIATELY ) ) { // Snap to position and begin to animate into the seat EnterVehicleImmediately(); return SCHED_PASSENGER_ENTER_VEHICLE_IMMEDIATELY; } // Entering schedule if ( HasCondition( COND_PASSENGER_ENTERING ) || m_PassengerIntent == PASSENGER_INTENT_ENTER ) { if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) { ClearCondition( COND_PASSENGER_ENTERING ); m_PassengerIntent = PASSENGER_INTENT_NONE; return SCHED_NONE; } // Don't attempt to enter for a period of time if ( m_flNextEnterAttempt > gpGlobals->curtime ) return SCHED_NONE; ClearCondition( COND_PASSENGER_ENTERING ); // Failing that, run to the right place return SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE; } return BaseClass::SelectTransitionSchedule(); } //----------------------------------------------------------------------------- // Purpose: Select schedules when we're riding in the car //----------------------------------------------------------------------------- int CAI_PassengerBehaviorCompanion::SelectScheduleInsideVehicle( void ) { // Overturned if ( HasCondition( COND_PASSENGER_OVERTURNED ) ) return SCHED_PASSENGER_OVERTURNED; if ( HasCondition( COND_PASSENGER_HARD_IMPACT ) ) { // Push out our fidget into the future so that we don't act unnaturally over bumpy terrain ExtendFidgetDelay( random->RandomFloat( 1.5f, 3.0f ) ); m_flNextJostleTime = gpGlobals->curtime + random->RandomFloat( 2.5f, 4.0f ); return SCHED_PASSENGER_IMPACT; } // Look for exiting the vehicle if ( HasCondition( COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE ) ) return SCHED_PASSENGER_EXIT_STUCK_VEHICLE; // Cower if we're about to get nailed if ( HasCondition( COND_HEAR_DANGER ) && IsCurSchedule( SCHED_PASSENGER_COWER ) == false ) { SpeakIfAllowed( TLK_DANGER ); return SCHED_PASSENGER_COWER; } // Fire on targets if ( GetEnemy() ) { // Limit how long we'll keep an enemy if there are many on screen if ( HasCondition( COND_NEW_ENEMY ) && m_nVisibleEnemies > 1 ) { GetEnemies()->SetTimeValidEnemy( GetEnemy(), random->RandomFloat( 0.5f, 1.0f ) ); } // Always face GetOuter()->AddLookTarget( GetEnemy(), 1.0f, 2.0f ); if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) && ( GetOuter()->GetShotRegulator()->IsInRestInterval() == false ) ) return SCHED_PASSENGER_RANGE_ATTACK1; } // Reload when we have the chance if ( HasCondition( COND_LOW_PRIMARY_AMMO ) && HasCondition( COND_SEE_ENEMY ) == false ) return SCHED_PASSENGER_RELOAD; // Say an overturned line if ( HasCondition( COND_PASSENGER_WARN_OVERTURNED ) ) { SpeakIfAllowed( TLK_PASSENGER_REQUEST_UPRIGHT ); m_flNextOverturnWarning = gpGlobals->curtime + random->RandomFloat( 5.0f, 10.0f ); ClearCondition( COND_PASSENGER_WARN_OVERTURNED ); } // Should we fidget? if ( HasCondition( COND_PASSENGER_CAN_FIDGET ) ) { ExtendFidgetDelay( random->RandomFloat( 6.0f, 12.0f ) ); return SCHED_PASSENGER_FIDGET; } return SCHED_NONE; } //----------------------------------------------------------------------------- // Purpose: Select schedules while we're outside the car //----------------------------------------------------------------------------- int CAI_PassengerBehaviorCompanion::SelectScheduleOutsideVehicle( void ) { // FIXME: How can we get in here? Assert( m_hVehicle ); if ( m_hVehicle == NULL ) return SCHED_NONE; // Handle our mark moving if ( HasCondition( COND_PASSENGER_VEHICLE_MOVED_FROM_MARK ) ) { // Reset our mark m_VehicleMonitor.SetMark( m_hVehicle, 36.0f ); ClearCondition( COND_PASSENGER_VEHICLE_MOVED_FROM_MARK ); } // If we want to get in, the try to do so if ( m_PassengerIntent == PASSENGER_INTENT_ENTER ) { // If we're not attempting to enter the vehicle again, just fall to the base class if ( m_flNextEnterAttempt > gpGlobals->curtime ) return BaseClass::SelectSchedule(); // Otherwise try and enter thec car return SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE; } // This means that we're outside the vehicle with no intent to enter, which should have disabled us! Disable(); Assert( 0 ); return SCHED_NONE; } //----------------------------------------------------------------------------- // Purpose: // Input : *pPlayer - // &vecCenter - // flRadius - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool SphereWithinPlayerFOV( CBasePlayer *pPlayer, const Vector &vecCenter, float flRadius ) { // TODO: For safety sake, we might want to do a more fully qualified test against the frustum using the bbox // If the player can see us, then we can't enter immediately anyway if ( pPlayer == NULL ) return false; // Find the length to the point Vector los = ( vecCenter - pPlayer->EyePosition() ); float flLength = VectorNormalize( los ); // Get the player's forward direction Vector vecPlayerForward; pPlayer->EyeVectors( &vecPlayerForward, NULL, NULL ); // This is the additional number of degrees to add to account for our distance float flArcAddition = atan2( flRadius, flLength ); // Find if the sphere is within our FOV float flDot = DotProduct( los, vecPlayerForward ); float flPlayerFOV = cos( DEG2RAD( pPlayer->GetFOV() / 2.0f ) ); return ( flDot > (flPlayerFOV-flArcAddition) ); } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::CanEnterVehicleImmediately( int *pResultSequence, Vector *pResultPos, QAngle *pResultAngles ) { // Must wait a short time before trying to do this (otherwise we stack up on the player!) if ( ( gpGlobals->curtime - m_flEnterBeginTime ) < 0.5f ) return false; // Vehicle can't be moving too quickly if ( GetVehicleSpeed() > 150 ) return false; // If the player can see us, then we can't enter immediately anyway CBasePlayer *pPlayer = AI_GetSinglePlayer(); if ( pPlayer == NULL ) return false; Vector vecPosition = GetOuter()->WorldSpaceCenter(); float flRadius = GetOuter()->CollisionProp()->BoundingRadius2D(); if ( SphereWithinPlayerFOV( pPlayer, vecPosition, flRadius ) ) return false; // Reserve an entry point if ( ReserveEntryPoint( VEHICLE_SEAT_ANY ) == false ) return false; // Get a list of all our animations const PassengerSeatAnims_t *pEntryAnims = m_hVehicle->GetServerVehicle()->NPC_GetPassengerSeatAnims( GetOuter(), PASSENGER_SEAT_ENTRY ); if ( pEntryAnims == NULL ) return -1; // Get the ultimate position we'll end up at Vector vecStartPos, vecEndPos; QAngle vecStartAngles; if ( m_hVehicle->GetServerVehicle()->NPC_GetPassengerSeatPosition( GetOuter(), &vecEndPos, NULL ) == false ) return -1; // Categorize the passenger in terms of being on the left or right side of the vehicle Vector vecRight; m_hVehicle->GetVectors( NULL, &vecRight, NULL ); CPlane lateralPlane; lateralPlane.InitializePlane( vecRight, m_hVehicle->WorldSpaceCenter() ); bool bPlaneSide = lateralPlane.PointInFront( GetOuter()->GetAbsOrigin() ); Vector vecPassengerOffset = ( GetOuter()->WorldSpaceCenter() - GetOuter()->GetAbsOrigin() ); const CPassengerSeatTransition *pTransition; float flNearestDistSqr = FLT_MAX; float flSeatDistSqr; int nNearestSequence = -1; int nSequence; Vector vecNearestPos; QAngle vecNearestAngles; // Test each animation (sorted by priority) for the best match for ( int i = 0; i < pEntryAnims->Count(); i++ ) { // Find the activity for this animation name pTransition = &pEntryAnims->Element(i); nSequence = GetOuter()->LookupSequence( STRING( pTransition->GetAnimationName() ) ); if ( nSequence == -1 ) continue; // Test this entry for validity if ( GetEntryPoint( nSequence, &vecStartPos, &vecStartAngles ) == false ) continue; // See if the passenger would be visible if standing at this position if ( SphereWithinPlayerFOV( pPlayer, (vecStartPos+vecPassengerOffset), flRadius ) ) continue; // Otherwise distance is the deciding factor flSeatDistSqr = ( vecStartPos - GetOuter()->GetAbsOrigin() ).LengthSqr(); // We must be within a certain distance to the vehicle if ( flSeatDistSqr > Square( 25*12 ) ) continue; // We cannot cross between the plane which splits the vehicle laterally in half down the middle // This avoids cases where the character magically ends up on one side of the vehicle after they were // clearly just on the other side. if ( lateralPlane.PointInFront( vecStartPos ) != bPlaneSide ) continue; // Closer, take it if ( flSeatDistSqr < flNearestDistSqr ) { flNearestDistSqr = flSeatDistSqr; nNearestSequence = nSequence; vecNearestPos = vecStartPos; vecNearestAngles = vecStartAngles; } } // Fail if we didn't find anything if ( nNearestSequence == -1 ) return false; // Return the results if ( pResultSequence ) { *pResultSequence = nNearestSequence; } if ( pResultPos ) { *pResultPos = vecNearestPos; } if ( pResultAngles ) { *pResultAngles = vecNearestAngles; } return true; } //----------------------------------------------------------------------------- // Purpose: Put us into the vehicle immediately // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::EnterVehicleImmediately( void ) { // Now play the animation GetOuter()->SetIdealActivity( ACT_SCRIPT_CUSTOM_MOVE ); GetOuter()->GetNavigator()->ClearGoal(); // Put us there and get going (no interpolation!) GetOuter()->Teleport( &m_vecTargetPosition, &m_vecTargetAngles, &vec3_origin ); GetOuter()->IncrementInterpolationFrame(); } //----------------------------------------------------------------------------- // Purpose: Overrides the schedule selection // Output : int - Schedule to play //----------------------------------------------------------------------------- int CAI_PassengerBehaviorCompanion::SelectSchedule( void ) { // First, keep track of our transition state (enter/exit) int nSched = SelectTransitionSchedule(); if ( nSched != SCHED_NONE ) return nSched; // Handle schedules based on our passenger state if ( GetPassengerState() == PASSENGER_STATE_OUTSIDE ) { nSched = SelectScheduleOutsideVehicle(); if ( nSched != SCHED_NONE ) return nSched; } else if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) { nSched = SelectScheduleInsideVehicle(); if ( nSched != SCHED_NONE ) return nSched; } return BaseClass::SelectSchedule(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CAI_PassengerBehaviorCompanion::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode ) { switch( failedTask ) { case TASK_GET_PATH_TO_VEHICLE_ENTRY_POINT: { // This is not allowed! if ( GetPassengerState() != PASSENGER_STATE_OUTSIDE ) { Assert( 0 ); return SCHED_FAIL; } // If we're not close enough, then get nearer the target if ( UTIL_DistApprox( m_hVehicle->GetAbsOrigin(), GetOuter()->GetAbsOrigin() ) > PASSENGER_NEAR_VEHICLE_THRESHOLD ) return SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE_FAILED; } // Fall through case TASK_GET_PATH_TO_NEAR_VEHICLE: m_flNextEnterAttempt = gpGlobals->curtime + 3.0f; break; } return BaseClass::SelectFailSchedule( failedSchedule, failedTask, taskFailCode ); } //----------------------------------------------------------------------------- // Purpose: Start to enter the vehicle //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::EnterVehicle( void ) { BaseClass::EnterVehicle(); m_nExitAttempts = 0; m_VehicleMonitor.SetMark( m_hVehicle, 8.0f ); m_flEnterBeginTime = gpGlobals->curtime; // Remove this flag because we're sitting so close we always think we're going to hit the player // FIXME: We need to store this state so we don't incorrectly restore it later GetOuter()->CapabilitiesRemove( bits_CAP_NO_HIT_PLAYER ); // Discard enemies quickly GetOuter()->GetEnemies()->SetEnemyDiscardTime( 2.0f ); SpeakIfAllowed( TLK_PASSENGER_BEGIN_ENTRANCE ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::FinishEnterVehicle( void ) { BaseClass::FinishEnterVehicle(); // We succeeded ResetVehicleEntryFailedState(); // Push this out into the future so we don't always fidget immediately in the vehicle ExtendFidgetDelay( random->RandomFloat( 4.0, 15.0f ) ); SpeakIfAllowed( TLK_PASSENGER_FINISH_ENTRANCE ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::ExitVehicle( void ) { BaseClass::ExitVehicle(); SpeakIfAllowed( TLK_PASSENGER_BEGIN_EXIT ); } //----------------------------------------------------------------------------- // Purpose: Vehicle has been completely exited //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::FinishExitVehicle( void ) { BaseClass::FinishExitVehicle(); m_nExitAttempts = 0; m_VehicleMonitor.ClearMark(); // FIXME: We need to store this state so we don't incorrectly restore it later GetOuter()->CapabilitiesAdd( bits_CAP_NO_HIT_PLAYER ); // FIXME: Restore this properly GetOuter()->GetEnemies()->SetEnemyDiscardTime( AI_DEF_ENEMY_DISCARD_TIME ); SpeakIfAllowed( TLK_PASSENGER_FINISH_EXIT ); } //----------------------------------------------------------------------------- // Purpose: Tries to build a route to the entry point of the target vehicle. // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::FindPathToVehicleEntryPoint( void ) { // Set our custom move name // bool bFindNearest = ( GetOuter()->m_NPCState == NPC_STATE_COMBAT || GetOuter()->m_NPCState == NPC_STATE_ALERT ); bool bFindNearest = true; // For the sake of quick gameplay, just make Alyx move directly! int nSequence = FindEntrySequence( bFindNearest ); if ( nSequence == -1 ) return false; // We have to do this specially because the activities are not named SetTransitionSequence( nSequence ); // Get the entry position Vector vecEntryPoint; QAngle vecEntryAngles; if ( GetEntryPoint( m_nTransitionSequence, &vecEntryPoint, &vecEntryAngles ) == false ) { MarkVehicleEntryFailed( vecEntryPoint ); return false; } // If we're already close enough, just succeed float flDistToGoalSqr = ( GetOuter()->GetAbsOrigin() - vecEntryPoint ).LengthSqr(); if ( flDistToGoalSqr < Square(3*12) ) return true; // Setup our goal AI_NavGoal_t goal( GOALTYPE_LOCATION ); // goal.arrivalActivity = ACT_SCRIPT_CUSTOM_MOVE; goal.dest = vecEntryPoint; // See if we need a radial route around the car, to our goal if ( UseRadialRouteToEntryPoint( vecEntryPoint ) ) { // Find the bounding radius of the vehicle Vector vecCenterPoint = m_hVehicle->WorldSpaceCenter(); vecCenterPoint.z = vecEntryPoint.z; bool bClockwise; float flArc = GetArcToEntryPoint( vecCenterPoint, vecEntryPoint, bClockwise ); float flRadius = m_hVehicle->CollisionProp()->BoundingRadius2D(); // Try and set a radial route if ( GetOuter()->GetNavigator()->SetRadialGoal( vecEntryPoint, vecCenterPoint, flRadius, flArc, 64.0f, bClockwise ) == false ) { // Try the opposite way flArc = 360.0f - flArc; // Try the opposite way around if ( GetOuter()->GetNavigator()->SetRadialGoal( vecEntryPoint, vecCenterPoint, flRadius, flArc, 64.0f, !bClockwise ) == false ) { // Try and set a direct route as a last resort if ( GetOuter()->GetNavigator()->SetGoal( goal ) == false ) return false; } } // We found a goal GetOuter()->GetNavigator()->SetArrivalDirection( vecEntryAngles ); GetOuter()->GetNavigator()->SetArrivalSpeed( 64.0f ); return true; } else { // Try and set a direct route if ( GetOuter()->GetNavigator()->SetGoal( goal ) ) { GetOuter()->GetNavigator()->SetArrivalDirection( vecEntryAngles ); GetOuter()->GetNavigator()->SetArrivalSpeed( 64.0f ); return true; } } // We failed, so remember it MarkVehicleEntryFailed( vecEntryPoint ); return false; } //----------------------------------------------------------------------------- // Purpose: Tests the route and position to see if it's valid // Input : &vecTestPos - position to test // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::CanExitAtPosition( const Vector &vecTestPos ) { CBasePlayer *pPlayer = AI_GetSinglePlayer(); if ( pPlayer == NULL ) return false; // Can't be in our potential view if ( pPlayer->FInViewCone( vecTestPos ) ) return false; // NOTE: There's no reason to do this since this is only called from a node's reported position // Find the exact ground at this position //Vector vecGroundPos; //if ( FindGroundAtPosition( vecTestPos, 16.0f, 64.0f, &vecGroundPos ) == false ) // return false; // Get the ultimate position we'll end up at Vector vecStartPos; if ( m_hVehicle->GetServerVehicle()->NPC_GetPassengerSeatPosition( GetOuter(), &vecStartPos, NULL ) == false ) return false; // See if we can move from where we are to that position in space if ( IsValidTransitionPoint( vecStartPos, vecTestPos ) == false ) return false; // Trace down to the ground // FIXME: This piece of code is redundant and happening in IsValidTransitionPoint() as well /* Vector vecGroundPos; if ( FindGroundAtPosition( vecTestPos, GetOuter()->StepHeight(), 64.0f, &vecGroundPos ) == false ) return false; */ // Try and sweep a box through space and make sure it's clear of obstructions /* trace_t tr; CTraceFilterVehicleTransition skipFilter( GetOuter(), m_hVehicle, COLLISION_GROUP_NONE ); // These are very approximated (and magical) numbers to allow passengers greater head room and leg room when transitioning Vector vecMins = GetOuter()->GetHullMins() + Vector( 0, 0, GetOuter()->StepHeight()*2.0f ); // FIXME: Vector vecMaxs = GetOuter()->GetHullMaxs() - Vector( 0, 0, GetOuter()->StepHeight() ); UTIL_TraceHull( GetOuter()->GetAbsOrigin(), vecGroundPos, vecMins, vecMaxs, MASK_NPCSOLID, &skipFilter, &tr ); // If we're blocked, we can't get out there if ( tr.fraction < 1.0f || tr.allsolid || tr.startsolid ) { if ( passenger_debug_transition.GetBool() ) { NDebugOverlay::SweptBox( GetOuter()->GetAbsOrigin(), vecGroundPos, vecMins, GetOuter()->GetHullMaxs(), vec3_angle, 255, 0, 0, 64, 2.0f ); } return false; } */ return true; } #define NUM_EXIT_ITERATIONS 8 //----------------------------------------------------------------------------- // Purpose: Find a position we can use to exit the vehicle via teleportation // Input : *vecResult - safe place to exit to // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::GetStuckExitPos( Vector *vecResult ) { // Get our right direction Vector vecVehicleRight; m_hVehicle->GetVectors( NULL, &vecVehicleRight, NULL ); // Get the vehicle's rough horizontal bounds float flVehicleRadius = m_hVehicle->CollisionProp()->BoundingRadius2D(); // Use the vehicle's center as our hub Vector vecCenter = m_hVehicle->WorldSpaceCenter(); // Angle whose tan is: y/x float flCurAngle = atan2f( vecVehicleRight.y, vecVehicleRight.x ); float flAngleIncr = (M_PI*2.0f)/(float)NUM_EXIT_ITERATIONS; Vector vecTestPos; // Test a number of discrete exit routes for ( int i = 0; i <= NUM_EXIT_ITERATIONS-1; i++ ) { // Get our position SinCos( flCurAngle, &vecTestPos.y, &vecTestPos.x ); vecTestPos.z = 0.0f; vecTestPos *= flVehicleRadius; vecTestPos += vecCenter; // Now find the nearest node and use that int nNearNode = GetOuter()->GetPathfinder()->NearestNodeToPoint( vecTestPos ); if ( nNearNode != NO_NODE ) { Vector vecNodePos = g_pBigAINet->GetNodePosition( GetOuter()->GetHullType(), nNearNode ); // Test the position if ( CanExitAtPosition( vecNodePos ) ) { // Take the result *vecResult = vecNodePos; return true; } // Move to the next iteration flCurAngle += flAngleIncr; } } // None found return false; } //----------------------------------------------------------------------------- // Purpose: Attempt to get out of an overturned vehicle when the player isn't looking // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::ExitStuckVehicle( void ) { // Try and find an exit position Vector vecExitPos; if ( GetStuckExitPos( &vecExitPos ) == false ) return false; // Detach from the parent GetOuter()->SetParent( NULL ); // Do all necessary clean-up FinishExitVehicle(); // Teleport to the destination // TODO: Make sure that the player can't see this! GetOuter()->Teleport( &vecExitPos, &vec3_angle, &vec3_origin ); GetOuter()->IncrementInterpolationFrame(); return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::StartTask( const Task_t *pTask ) { // We need to override these so we never face if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) { if ( pTask->iTask == TASK_FACE_TARGET || pTask->iTask == TASK_FACE_ENEMY || pTask->iTask == TASK_FACE_IDEAL || pTask->iTask == TASK_FACE_HINTNODE || pTask->iTask == TASK_FACE_LASTPOSITION || pTask->iTask == TASK_FACE_PATH || pTask->iTask == TASK_FACE_PLAYER || pTask->iTask == TASK_FACE_REASONABLE || pTask->iTask == TASK_FACE_SAVEPOSITION || pTask->iTask == TASK_FACE_SCRIPT ) { return TaskComplete(); } } switch ( pTask->iTask ) { case TASK_RUN_TO_VEHICLE_ENTRANCE: { // Get a move on! GetOuter()->GetNavigator()->SetMovementActivity( ACT_RUN ); } break; case TASK_GET_PATH_TO_VEHICLE_ENTRY_POINT: { if ( GetPassengerState() != PASSENGER_STATE_OUTSIDE ) { Assert( 0 ); TaskFail( "Trying to run while inside a vehicle!\n"); return; } // Reserve an entry point if ( ReserveEntryPoint( VEHICLE_SEAT_ANY ) == false ) { TaskFail( "No valid entry point!\n" ); return; } // Find where we're going if ( FindPathToVehicleEntryPoint() ) { TaskComplete(); return; } // We didn't find a path TaskFail( "TASK_GET_PATH_TO_VEHICLE_ENTRY_POINT: Unable to run to entry point" ); } break; case TASK_GET_PATH_TO_TARGET: { GetOuter()->SetTarget( m_hVehicle ); BaseClass::StartTask( pTask ); } break; case TASK_GET_PATH_TO_NEAR_VEHICLE: { if ( m_hVehicle == NULL ) { TaskFail("Lost vehicle pointer\n"); return; } // Find the passenger offset we're going for Vector vecRight; m_hVehicle->GetVectors( NULL, &vecRight, NULL ); Vector vecTargetOffset = vecRight * 64.0f; // Try and find a path near there AI_NavGoal_t goal( GOALTYPE_TARGETENT, vecTargetOffset, AIN_DEF_ACTIVITY, 64.0f, AIN_UPDATE_TARGET_POS, m_hVehicle ); GetOuter()->SetTarget( m_hVehicle ); if ( GetOuter()->GetNavigator()->SetGoal( goal ) ) { TaskComplete(); return; } TaskFail( "Unable to find path to get closer to vehicle!\n" ); return; } break; case TASK_PASSENGER_RELOAD: { GetOuter()->SetIdealActivity( ACT_PASSENGER_RELOAD ); return; } break; case TASK_PASSENGER_EXIT_STUCK_VEHICLE: { if ( ExitStuckVehicle() ) { TaskComplete(); return; } TaskFail("Unable to exit overturned vehicle!\n"); } break; case TASK_PASSENGER_OVERTURNED: { // Go into our overturned animation if ( GetOuter()->GetActivity() != ACT_PASSENGER_OVERTURNED ) { GetOuter()->SetActivity( ACT_RESET ); GetOuter()->SetActivity( ACT_PASSENGER_OVERTURNED ); } TaskComplete(); } break; case TASK_PASSENGER_IMPACT: { // Stomp anything currently playing on top of us, this has to take priority GetOuter()->RemoveAllGestures(); // Go into our impact animation GetOuter()->ResetIdealActivity( ACT_PASSENGER_IMPACT ); // Delay for twice the duration of our impact animation int nSequence = GetOuter()->SelectWeightedSequence( ACT_PASSENGER_IMPACT ); float flSeqDuration = GetOuter()->SequenceDuration( nSequence ); float flStunTime = flSeqDuration + random->RandomFloat( 1.0f, 2.0f ); GetOuter()->SetNextAttack( gpGlobals->curtime + flStunTime ); ExtendFidgetDelay( flStunTime ); } break; default: BaseClass::StartTask( pTask ); break; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::IsCurTaskContinuousMove( void ) { const Task_t *pCurTask = GetCurTask(); if ( pCurTask && pCurTask->iTask == TASK_RUN_TO_VEHICLE_ENTRANCE ) return true; return BaseClass::IsCurTaskContinuousMove(); } //----------------------------------------------------------------------------- // Purpose: Update our path if we're running towards the vehicle (since it can move) //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::UpdateVehicleEntrancePath( void ) { // If it's too soon to check again, don't bother if ( m_flEntraceUpdateTime > gpGlobals->curtime ) return true; // Find out if we need to update if ( m_VehicleMonitor.TargetMoved2D( m_hVehicle ) == false ) { m_flEntraceUpdateTime = gpGlobals->curtime + 0.5f; return true; } // Don't attempt again for some amount of time m_flEntraceUpdateTime = gpGlobals->curtime + 1.0f; int nSequence = FindEntrySequence( true ); if ( nSequence == -1 ) return false; SetTransitionSequence( nSequence ); // Get the entry position Vector vecEntryPoint; QAngle vecEntryAngles; if ( GetEntryPoint( m_nTransitionSequence, &vecEntryPoint, &vecEntryAngles ) == false ) return false; // Move the entry point forward in time a bit to predict where it'll be Vector vecVehicleSpeed = m_hVehicle->GetSmoothedVelocity(); // Tack on the smoothed velocity vecEntryPoint += vecVehicleSpeed; // one second // Update our entry point if ( GetOuter()->GetNavigator()->UpdateGoalPos( vecEntryPoint ) == false ) return false; // Reset the goal angles GetNavigator()->SetArrivalDirection( vecEntryAngles ); return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::RunTask( const Task_t *pTask ) { switch ( pTask->iTask ) { case TASK_PASSENGER_RELOAD: { if ( GetOuter()->IsSequenceFinished() ) { TaskComplete(); } } break; case TASK_PASSENGER_IMPACT: { if ( GetOuter()->IsSequenceFinished() ) { TaskComplete(); return; } } break; case TASK_RUN_TO_VEHICLE_ENTRANCE: { // Update our entrance point if we can if ( UpdateVehicleEntrancePath() == false ) { TaskFail("Unable to find entrance to vehicle"); break; } // See if we're close enough to our goal if ( GetOuter ()->GetNavigator()->IsGoalActive() == false ) { // See if we're close enough now to enter the vehicle Vector vecEntryPoint; GetEntryPoint( m_nTransitionSequence, &vecEntryPoint ); if ( ( vecEntryPoint - GetAbsOrigin() ).Length2DSqr() < Square( 36.0f ) ) { if ( GetNavigator()->GetArrivalActivity() != ACT_INVALID ) { SetActivity( GetNavigator()->GetArrivalActivity() ); } TaskComplete(); } else { TaskFail( "Unable to navigate to vehicle" ); } } // Keep merrily going! } break; default: BaseClass::RunTask( pTask ); break; } } //----------------------------------------------------------------------------- // Purpose: Add custom interrupt conditions //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::BuildScheduleTestBits( void ) { // Always break on being able to exit if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) { GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE ) ); GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_PASSENGER_HARD_IMPACT) ); if ( IsCurSchedule( SCHED_PASSENGER_OVERTURNED ) == false ) { GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_PASSENGER_OVERTURNED ) ); } // Append the ability to break on fidgeting if ( IsCurSchedule( SCHED_PASSENGER_IDLE ) ) { GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_PASSENGER_CAN_FIDGET ) ); } // Add this so we're prompt about exiting the vehicle when able to if ( m_PassengerIntent == PASSENGER_INTENT_EXIT ) { GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_PASSENGER_VEHICLE_STOPPED ) ); } } BaseClass::BuildScheduleTestBits(); } //----------------------------------------------------------------------------- // Purpose: Determines if the passenger should take a radial route to the goal // Input : &vecEntryPoint - Point of entry // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::UseRadialRouteToEntryPoint( const Vector &vecEntryPoint ) { // Get the center position of the vehicle we'll radiate around Vector vecCenterPos = m_hVehicle->WorldSpaceCenter(); vecCenterPos.z = vecEntryPoint.z; // Find out if we need to go around the vehicle float flDistToVehicleCenter = ( vecCenterPos - GetOuter()->GetAbsOrigin() ).Length(); float flDistToGoal = ( vecEntryPoint - GetOuter()->GetAbsOrigin() ).Length(); if ( flDistToGoal > flDistToVehicleCenter ) return true; return false; } //----------------------------------------------------------------------------- // Purpose: Find the arc in degrees to reach our goal position // Input : &vecCenterPoint - Point around which the arc rotates // &vecEntryPoint - Point we're trying to reach // &bClockwise - If we should move clockwise or not to get there // Output : float - degrees around arc to follow //----------------------------------------------------------------------------- float CAI_PassengerBehaviorCompanion::GetArcToEntryPoint( const Vector &vecCenterPoint, const Vector &vecEntryPoint, bool &bClockwise ) { // We want the entry point to be at the same level as the center to make this a two dimensional problem Vector vecEntryPointAdjusted = vecEntryPoint; vecEntryPointAdjusted.z = vecCenterPoint.z; // Direction from vehicle center to passenger Vector vecVehicleToPassenger = ( GetOuter()->GetAbsOrigin() - vecCenterPoint ); VectorNormalize( vecVehicleToPassenger ); // Direction from vehicle center to entry point Vector vecVehicleToEntry = ( vecEntryPointAdjusted - vecCenterPoint ); VectorNormalize( vecVehicleToEntry ); float flVehicleToPassengerYaw = UTIL_VecToYaw( vecVehicleToPassenger ); float flVehicleToEntryYaw = UTIL_VecToYaw( vecVehicleToEntry ); float flArcDist = UTIL_AngleDistance( flVehicleToEntryYaw, flVehicleToPassengerYaw ); bClockwise = ( flArcDist < 0.0f ); return fabs( flArcDist ); } //----------------------------------------------------------------------------- // Purpose: Removes all failed points //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::ResetVehicleEntryFailedState( void ) { m_FailedEntryPositions.RemoveAll(); } //----------------------------------------------------------------------------- // Purpose: Adds a failed position to the list and marks when it occurred // Input : &vecPosition - Position that failed //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::MarkVehicleEntryFailed( const Vector &vecPosition ) { FailPosition_t failPos; failPos.flTime = gpGlobals->curtime; failPos.vecPosition = vecPosition; m_FailedEntryPositions.AddToTail( failPos ); // Show this as failed if ( passenger_debug_entry.GetBool() ) { NDebugOverlay::Box( vecPosition, -Vector(8,8,8), Vector(8,8,8), 255, 0, 0, 0, 2.0f ); } } //----------------------------------------------------------------------------- // Purpose: See if a vector is near enough to a previously failed position // Input : &vecPosition - position to test // Output : Returns true if the point is near enough another to be considered equivalent //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::PointIsWithinEntryFailureRadius( const Vector &vecPosition ) { // Test this point against our known failed points and reject it if it's too near for ( int i = 0; i < m_FailedEntryPositions.Count(); i++ ) { // If our time has expired, kill the position if ( ( gpGlobals->curtime - m_FailedEntryPositions[i].flTime ) > 3.0f ) { // Show that we've cleared it if ( passenger_debug_entry.GetBool() ) { NDebugOverlay::Box( m_FailedEntryPositions[i].vecPosition, -Vector(12,12,12), Vector(12,12,12), 255, 255, 0, 0, 2.0f ); } m_FailedEntryPositions.Remove( i ); continue; } // See if this position is too near our last failed attempt if ( ( vecPosition - m_FailedEntryPositions[i].vecPosition ).LengthSqr() < Square(3*12) ) { // Show that this was denied if ( passenger_debug_entry.GetBool() ) { NDebugOverlay::Box( m_FailedEntryPositions[i].vecPosition, -Vector(12,12,12), Vector(12,12,12), 255, 0, 0, 128, 2.0f ); } return true; } } return false; } //----------------------------------------------------------------------------- // Purpose: Find the proper sequence to use (weighted by priority or distance from current position) // to enter the vehicle. // Input : bNearest - Use distance as the criteria for a "best" sequence. Otherwise the order of the // seats is their priority. // Output : int - sequence index //----------------------------------------------------------------------------- int CAI_PassengerBehaviorCompanion::FindEntrySequence( bool bNearest /*= false*/ ) { // Get a list of all our animations const PassengerSeatAnims_t *pEntryAnims = m_hVehicle->GetServerVehicle()->NPC_GetPassengerSeatAnims( GetOuter(), PASSENGER_SEAT_ENTRY ); if ( pEntryAnims == NULL ) return -1; // Get the ultimate position we'll end up at Vector vecStartPos, vecEndPos; if ( m_hVehicle->GetServerVehicle()->NPC_GetPassengerSeatPosition( GetOuter(), &vecEndPos, NULL ) == false ) return -1; const CPassengerSeatTransition *pTransition; float flNearestDistSqr = FLT_MAX; float flSeatDistSqr; int nNearestSequence = -1; int nSequence; // Test each animation (sorted by priority) for the best match for ( int i = 0; i < pEntryAnims->Count(); i++ ) { // Find the activity for this animation name pTransition = &pEntryAnims->Element(i); nSequence = GetOuter()->LookupSequence( STRING( pTransition->GetAnimationName() ) ); if ( nSequence == -1 ) continue; // Test this entry for validity if ( GetEntryPoint( nSequence, &vecStartPos ) == false ) continue; // See if this entry position is in our list of known unreachable places if ( PointIsWithinEntryFailureRadius( vecStartPos ) ) continue; // Check to see if we can use this if ( IsValidTransitionPoint( vecStartPos, vecEndPos ) ) { // If we're just looking for the first, we're done if ( bNearest == false ) return nSequence; // Otherwise distance is the deciding factor flSeatDistSqr = ( vecStartPos - GetOuter()->GetAbsOrigin() ).LengthSqr(); // Closer, take it if ( flSeatDistSqr < flNearestDistSqr ) { flNearestDistSqr = flSeatDistSqr; nNearestSequence = nSequence; } } } return nNearestSequence; } //----------------------------------------------------------------------------- // Purpose: Override certain animations //----------------------------------------------------------------------------- Activity CAI_PassengerBehaviorCompanion::NPC_TranslateActivity( Activity activity ) { Activity newActivity = BaseClass::NPC_TranslateActivity( activity ); // Handle animations from inside the vehicle if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) { // Alter idle depending on the vehicle's state if ( newActivity == ACT_IDLE ) { // Always play the overturned animation if ( m_vehicleState.m_bWasOverturned ) return ACT_PASSENGER_OVERTURNED; } } return newActivity; } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::CanExitVehicle( void ) { if ( BaseClass::CanExitVehicle() == false ) return false; // If we're tipped too much, we can't exit Vector vecUp; GetOuter()->GetVectors( NULL, NULL, &vecUp ); if ( DotProduct( vecUp, Vector(0,0,1) ) < DOT_45DEGREE ) return false; return true; } //----------------------------------------------------------------------------- // Purpose: NPC needs to get to their marks, so do so with urgent navigation //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::IsNavigationUrgent( void ) { // If we're running to the vehicle, do so urgently if ( GetPassengerState() == PASSENGER_STATE_OUTSIDE && m_PassengerIntent == PASSENGER_INTENT_ENTER ) return true; return BaseClass::IsNavigationUrgent(); } //----------------------------------------------------------------------------- // Purpose: Calculate our body lean based on our delta velocity //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::CalculateBodyLean( void ) { // Calculate our lateral displacement from a perfectly centered start float flLateralDisp = SimpleSplineRemapVal( m_vehicleState.m_vecLastAngles.z, 100.0f, -100.0f, -1.0f, 1.0f ); flLateralDisp = clamp( flLateralDisp, -1.0f, 1.0f ); // FIXME: Framerate dependent! m_flLastLateralLean = ( m_flLastLateralLean * 0.2f ) + ( flLateralDisp * 0.8f ); // Here we can make Alyx do something different on an "extreme" lean condition if ( fabs( m_flLastLateralLean ) > 0.75f ) { // Large lean, make us react? } // Set these parameters GetOuter()->SetPoseParameter( "vehicle_lean", m_flLastLateralLean ); } //----------------------------------------------------------------------------- // Purpose: Whether or not we're allowed to fidget //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::CanFidget( void ) { // Can't fidget again too quickly if ( m_flNextFidgetTime > gpGlobals->curtime ) return false; // FIXME: Really we want to check our readiness level at this point if ( GetOuter()->GetEnemy() != NULL ) return false; // Don't fidget unless we're at low readiness if ( m_hCompanion && ( m_hCompanion->GetReadinessLevel() > AIRL_RELAXED ) ) return false; // Don't fidget while we're in a script if ( GetOuter()->IsInAScript() || GetOuter()->GetIdealState() == NPC_STATE_SCRIPT || IsRunningScriptedScene( GetOuter() ) ) return false; // If we're upside down, don't bother if ( HasCondition( COND_PASSENGER_OVERTURNED ) ) return false; // Must be visible to the player CBasePlayer *pPlayer = AI_GetSinglePlayer(); if ( pPlayer && pPlayer->FInViewCone( GetOuter()->EyePosition() ) == false ) return false; return true; } //----------------------------------------------------------------------------- // Purpose: Extends the fidget delay by the time specified // Input : flDuration - in seconds //----------------------------------------------------------------------------- void CAI_PassengerBehaviorCompanion::ExtendFidgetDelay( float flDuration ) { // If we're already expired, just set this as the next time if ( m_flNextFidgetTime < gpGlobals->curtime ) { m_flNextFidgetTime = gpGlobals->curtime + flDuration; } else { // Otherwise bump the delay farther into the future m_flNextFidgetTime += flDuration; } } //----------------------------------------------------------------------------- // Purpose: We never want to be marked as crouching when inside a vehicle //----------------------------------------------------------------------------- bool CAI_PassengerBehaviorCompanion::IsCrouching( void ) { return false; } AI_BEGIN_CUSTOM_SCHEDULE_PROVIDER( CAI_PassengerBehaviorCompanion ) { DECLARE_ACTIVITY( ACT_PASSENGER_IDLE_AIM ) DECLARE_ACTIVITY( ACT_PASSENGER_RELOAD ) DECLARE_ACTIVITY( ACT_PASSENGER_OVERTURNED ) DECLARE_ACTIVITY( ACT_PASSENGER_IMPACT ) DECLARE_ACTIVITY( ACT_PASSENGER_IMPACT_WEAPON ) DECLARE_ACTIVITY( ACT_PASSENGER_POINT ) DECLARE_ACTIVITY( ACT_PASSENGER_POINT_BEHIND ) DECLARE_ACTIVITY( ACT_PASSENGER_IDLE_READY ) DECLARE_ACTIVITY( ACT_PASSENGER_GESTURE_JOSTLE_LARGE ) DECLARE_ACTIVITY( ACT_PASSENGER_GESTURE_JOSTLE_SMALL ) DECLARE_ACTIVITY( ACT_PASSENGER_GESTURE_JOSTLE_LARGE_STIMULATED ) DECLARE_ACTIVITY( ACT_PASSENGER_GESTURE_JOSTLE_SMALL_STIMULATED ) DECLARE_ACTIVITY( ACT_PASSENGER_COWER_IN ) DECLARE_ACTIVITY( ACT_PASSENGER_COWER_LOOP ) DECLARE_ACTIVITY( ACT_PASSENGER_COWER_OUT ) DECLARE_ACTIVITY( ACT_PASSENGER_IDLE_FIDGET ) DECLARE_TASK( TASK_GET_PATH_TO_VEHICLE_ENTRY_POINT ) DECLARE_TASK( TASK_GET_PATH_TO_NEAR_VEHICLE ) DECLARE_TASK( TASK_PASSENGER_RELOAD ) DECLARE_TASK( TASK_PASSENGER_EXIT_STUCK_VEHICLE ) DECLARE_TASK( TASK_PASSENGER_OVERTURNED ) DECLARE_TASK( TASK_PASSENGER_IMPACT ) DECLARE_TASK( TASK_RUN_TO_VEHICLE_ENTRANCE ) DECLARE_CONDITION( COND_PASSENGER_VEHICLE_MOVED_FROM_MARK ) DECLARE_CONDITION( COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE ) DECLARE_CONDITION( COND_PASSENGER_WARN_OVERTURNED ) DECLARE_CONDITION( COND_PASSENGER_WARN_COLLISION ) DECLARE_CONDITION( COND_PASSENGER_CAN_FIDGET ) DECLARE_CONDITION( COND_PASSENGER_CAN_ENTER_IMMEDIATELY ) DEFINE_SCHEDULE ( SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE_FAILED" " TASK_STOP_MOVING 0" " TASK_SET_TOLERANCE_DISTANCE 36" // 3 ft " TASK_SET_ROUTE_SEARCH_TIME 5" " TASK_GET_PATH_TO_VEHICLE_ENTRY_POINT 0" " TASK_RUN_TO_VEHICLE_ENTRANCE 0" " TASK_SET_SCHEDULE SCHEDULE:SCHED_PASSENGER_ENTER_VEHICLE" "" " Interrupts" " COND_PASSENGER_CAN_ENTER_IMMEDIATELY" " COND_PASSENGER_CANCEL_ENTER" ) DEFINE_SCHEDULE ( SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE_FAILED, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_PASSENGER_ENTER_VEHICLE_PAUSE" " TASK_STOP_MOVING 0" " TASK_SET_TOLERANCE_DISTANCE 36" " TASK_SET_ROUTE_SEARCH_TIME 3" " TASK_GET_PATH_TO_NEAR_VEHICLE 0" " TASK_RUN_PATH 0" " TASK_WAIT_FOR_MOVEMENT 0" "" " Interrupts" " COND_PASSENGER_CANCEL_ENTER" ) DEFINE_SCHEDULE ( SCHED_PASSENGER_ENTER_VEHICLE_PAUSE, " Tasks" " TASK_STOP_MOVING 1" " TASK_FACE_TARGET 0" " TASK_WAIT 2" "" " Interrupts" " COND_LIGHT_DAMAGE" " COND_NEW_ENEMY" " COND_PASSENGER_CANCEL_ENTER" ) DEFINE_SCHEDULE ( SCHED_PASSENGER_RANGE_ATTACK1, " Tasks" " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack " TASK_RANGE_ATTACK1 0" "" " Interrupts" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" " COND_ENEMY_OCCLUDED" " COND_NO_PRIMARY_AMMO" " COND_HEAR_DANGER" " COND_WEAPON_BLOCKED_BY_FRIEND" " COND_WEAPON_SIGHT_OCCLUDED" ) DEFINE_SCHEDULE ( SCHED_PASSENGER_EXIT_STUCK_VEHICLE, " Tasks" " TASK_PASSENGER_EXIT_STUCK_VEHICLE 0" "" " Interrupts" ) DEFINE_SCHEDULE ( SCHED_PASSENGER_RELOAD, " Tasks" " TASK_PASSENGER_RELOAD 0" "" " Interrupts" ) DEFINE_SCHEDULE ( SCHED_PASSENGER_OVERTURNED, " Tasks" " TASK_PASSENGER_OVERTURNED 0" "" " Interrupts" ) DEFINE_SCHEDULE ( SCHED_PASSENGER_IMPACT, " Tasks" " TASK_PASSENGER_IMPACT 0" "" " Interrupts" ) DEFINE_SCHEDULE ( SCHED_PASSENGER_ENTER_VEHICLE_IMMEDIATELY, " Tasks" " TASK_PASSENGER_ATTACH_TO_VEHICLE 0" " TASK_PASSENGER_ENTER_VEHICLE 0" "" " Interrupts" " COND_NO_CUSTOM_INTERRUPTS" ) DEFINE_SCHEDULE ( SCHED_PASSENGER_COWER, " Tasks" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_PASSENGER_COWER_IN" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_PASSENGER_COWER_LOOP" " TASK_WAIT_UNTIL_NO_DANGER_SOUND 0" " TASK_WAIT 2" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_PASSENGER_COWER_OUT" "" " Interrupts" " COND_NO_CUSTOM_INTERRUPTS" ) DEFINE_SCHEDULE ( SCHED_PASSENGER_FIDGET, " Tasks" " TASK_PLAY_SEQUENCE ACTIVITY:ACT_PASSENGER_IDLE_FIDGET" "" " Interrupts" " COND_NO_CUSTOM_INTERRUPTS" ) AI_END_CUSTOM_SCHEDULE_PROVIDER() }