//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "ai_network.h" #include "ai_default.h" #include "ai_schedule.h" #include "ai_hull.h" #include "ai_node.h" #include "ai_task.h" #include "ai_senses.h" #include "ai_navigator.h" #include "ai_route.h" #include "entitylist.h" #include "soundenvelope.h" #include "gamerules.h" #include "ndebugoverlay.h" #include "soundflags.h" #include "trains.h" #include "globalstate.h" #include "vehicle_base.h" #include "npc_vehicledriver.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" #define DRIVER_DEBUG_PATH 1 #define DRIVER_DEBUG_PATH_SPLINE 2 //------------------------------------ // //------------------------------------ ConVar g_debug_vehicledriver( "g_debug_vehicledriver", "0", FCVAR_CHEAT ); BEGIN_DATADESC( CNPC_VehicleDriver ) DEFINE_KEYFIELD( m_iszVehicleName, FIELD_STRING, "vehicle" ), // DEFINE_FIELD( m_hVehicle, FIELD_EHANDLE ), // DEFINE_FIELD( m_pVehicleInterface, FIELD_POINTER ), DEFINE_FIELD( m_hVehicleEntity, FIELD_EHANDLE ), // DEFINE_FIELD( m_Waypoints, FIELD_???? ), // DEFINE_FIELD( m_pCurrentWaypoint, FIELD_POINTER ), // DEFINE_FIELD( m_pNextWaypoint, FIELD_POINTER ), DEFINE_FIELD( m_vecDesiredVelocity, FIELD_VECTOR ), DEFINE_FIELD( m_vecDesiredPosition, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_vecPrevPoint, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_vecPrevPrevPoint, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_vecPostPoint, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_vecPostPostPoint, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_flDistanceAlongSpline, FIELD_FLOAT ), DEFINE_KEYFIELD( m_flDriversMaxSpeed, FIELD_FLOAT, "drivermaxspeed" ), DEFINE_KEYFIELD( m_flDriversMinSpeed, FIELD_FLOAT, "driverminspeed" ), DEFINE_FIELD( m_flMaxSpeed, FIELD_FLOAT ), DEFINE_FIELD( m_flGoalSpeed, FIELD_FLOAT ), //DEFINE_KEYFIELD( m_flInitialSpeed, FIELD_FLOAT ), DEFINE_FIELD( m_flSteering, FIELD_FLOAT ), // Inputs DEFINE_INPUTFUNC( FIELD_FLOAT, "SetDriversMaxSpeed", InputSetDriversMaxSpeed ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetDriversMinSpeed", InputSetDriversMinSpeed ), DEFINE_INPUTFUNC( FIELD_VOID, "StartForward", InputStartForward ), DEFINE_INPUTFUNC( FIELD_VOID, "Stop", InputStop ), DEFINE_INPUTFUNC( FIELD_VOID, "StartFiring", InputStartFiring ), DEFINE_INPUTFUNC( FIELD_VOID, "StopFiring", InputStopFiring ), DEFINE_INPUTFUNC( FIELD_STRING, "GotoPathCorner", InputGotoPathCorner ), END_DATADESC() LINK_ENTITY_TO_CLASS( npc_vehicledriver, CNPC_VehicleDriver ); //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CNPC_VehicleDriver::CNPC_VehicleDriver( void ) { } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CNPC_VehicleDriver::~CNPC_VehicleDriver( void ) { ClearWaypoints(); } //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ void CNPC_VehicleDriver::Spawn( void ) { Precache( ); BaseClass::Spawn(); CapabilitiesClear(); CapabilitiesAdd( bits_CAP_MOVE_GROUND ); CapabilitiesAdd( bits_CAP_MOVE_SHOOT ); SetModel( "models/roller_vehicledriver.mdl" ); SetHullType(HULL_LARGE); SetHullSizeNormal(); m_iMaxHealth = m_iHealth = 1; m_flFieldOfView = VIEW_FIELD_FULL; SetSolid( SOLID_BBOX ); AddSolidFlags( FSOLID_NOT_SOLID ); SetMoveType( MOVETYPE_NONE ); AddEffects( EF_NODRAW ); m_lifeState = LIFE_ALIVE; SetCycle( 0 ); ResetSequenceInfo(); AddFlag( FL_NPC ); m_flMaxSpeed = 0; m_flGoalSpeed = m_flInitialSpeed; m_vecDesiredVelocity = vec3_origin; m_vecPrevPoint = vec3_origin; m_vecPrevPrevPoint = vec3_origin; m_vecPostPoint = vec3_origin; m_vecPostPostPoint = vec3_origin; m_vecDesiredPosition = vec3_origin; m_flSteering = 45; m_flDistanceAlongSpline = 0.2; m_pCurrentWaypoint = m_pNextWaypoint = NULL; GetNavigator()->SetPathcornerPathfinding( false ); NPCInit(); m_takedamage = DAMAGE_NO; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::Precache( void ) { PrecacheModel( "models/roller_vehicledriver.mdl" ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::Activate( void ) { BaseClass::Activate(); // Restore doesn't need to do this if ( m_hVehicleEntity ) return; // Make sure we've got a vehicle if ( m_iszVehicleName == NULL_STRING ) { Warning( "npc_vehicledriver %s has no vehicle to drive.\n", STRING(GetEntityName()) ); UTIL_Remove( this ); return; } m_hVehicleEntity = (gEntList.FindEntityByName( NULL, STRING(m_iszVehicleName) )); if ( !m_hVehicleEntity ) { Warning( "npc_vehicledriver %s couldn't find his vehicle named %s.\n", STRING(GetEntityName()), STRING(m_iszVehicleName) ); UTIL_Remove( this ); return; } m_pVehicleInterface = m_hVehicleEntity->GetServerVehicle(); Assert( m_pVehicleInterface ); if ( !m_pVehicleInterface->NPC_CanDrive() ) { Warning( "npc_vehicledriver %s doesn't know how to drive vehicle %s.\n", STRING(GetEntityName()), STRING(m_hVehicleEntity->GetEntityName()) ); UTIL_Remove( this ); return; } // We've found our vehicle. Move to it and start following it. SetAbsOrigin( m_hVehicleEntity->WorldSpaceCenter() ); m_pVehicleInterface->NPC_SetDriver( this ); RecalculateSpeeds(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::OnRestore( void ) { BaseClass::OnRestore(); if ( m_hVehicleEntity ) { m_pVehicleInterface = m_hVehicleEntity->GetServerVehicle(); Assert( m_pVehicleInterface ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::UpdateOnRemove( void ) { // Leave our vehicle if ( m_pVehicleInterface ) { m_pVehicleInterface->NPC_SetDriver( NULL ); } BaseClass::UpdateOnRemove(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::PrescheduleThink( void ) { if ( !m_hVehicleEntity ) { m_pVehicleInterface = NULL; UTIL_Remove( this ); return; } // Keep up with my vehicle SetAbsOrigin( m_hVehicleEntity->WorldSpaceCenter() ); SetAbsAngles( m_hVehicleEntity->GetAbsAngles() ); BaseClass::PrescheduleThink(); if ( m_NPCState == NPC_STATE_IDLE ) { m_pVehicleInterface->NPC_Brake(); return; } // If we've been picked up by something (dropship probably), abort. if ( m_hVehicleEntity->GetParent() ) { SetState( NPC_STATE_IDLE ); ClearWaypoints(); SetGoalEnt( NULL ); return; } DriveVehicle(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CNPC_VehicleDriver::SelectSchedule( void ) { // Vehicle driver hangs in the air inside the vehicle, so we never need to fall to ground ClearCondition( COND_FLOATING_OFF_GROUND ); if ( HasSpawnFlags(SF_VEHICLEDRIVER_INACTIVE) ) { SetState( NPC_STATE_IDLE ); return SCHED_VEHICLEDRIVER_INACTIVE; } if ( GetGoalEnt() ) return SCHED_VEHICLEDRIVER_DRIVE_PATH; switch ( m_NPCState ) { case NPC_STATE_IDLE: break; case NPC_STATE_ALERT: break; case NPC_STATE_COMBAT: { if ( HasCondition(COND_NEW_ENEMY) || HasCondition( COND_ENEMY_DEAD ) ) return BaseClass::SelectSchedule(); if ( HasCondition(COND_SEE_ENEMY) ) { // we can see the enemy if ( HasCondition(COND_CAN_RANGE_ATTACK2) ) return SCHED_RANGE_ATTACK2; if ( HasCondition(COND_CAN_RANGE_ATTACK1) ) return SCHED_RANGE_ATTACK1; // What to do here? Not necessarily easy to face enemy. //if ( HasCondition(COND_NOT_FACING_ATTACK) ) //return SCHED_COMBAT_FACE; } // We can see him, but can't shoot him. Just wait and hope he comes closer. return SCHED_VEHICLEDRIVER_COMBAT_WAIT; } break; } return BaseClass::SelectSchedule(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CNPC_VehicleDriver::RangeAttack1Conditions( float flDot, float flDist ) { // Vehicle not ready to fire again yet? if ( m_pVehicleInterface->Weapon_PrimaryCanFireAt() > gpGlobals->curtime ) return 0; // Check weapon range float flMinRange, flMaxRange; m_pVehicleInterface->Weapon_PrimaryRanges( &flMinRange, &flMaxRange ); if ( flDist < flMinRange ) return COND_TOO_CLOSE_TO_ATTACK; if ( flDist > flMaxRange ) return COND_TOO_FAR_TO_ATTACK; // Don't shoot backwards Vector vecForward; Vector vecToTarget = (GetEnemy()->GetAbsOrigin() - GetAbsOrigin()); VectorNormalize(vecToTarget); m_hVehicleEntity->GetVectors( &vecForward, NULL, NULL ); float flForwardDot = DotProduct( vecForward, vecToTarget ); if ( flForwardDot < 0 && fabs(flDot) < 0.5 ) return COND_NOT_FACING_ATTACK; return COND_CAN_RANGE_ATTACK1; } //========================================================= // RangeAttack2Conditions //========================================================= int CNPC_VehicleDriver::RangeAttack2Conditions( float flDot, float flDist ) { // Vehicle not ready to fire again yet? if ( m_pVehicleInterface->Weapon_SecondaryCanFireAt() > gpGlobals->curtime ) return 0; // Check weapon range float flMinRange, flMaxRange; m_pVehicleInterface->Weapon_SecondaryRanges( &flMinRange, &flMaxRange ); if ( flDist < flMinRange ) return COND_TOO_CLOSE_TO_ATTACK; if ( flDist > flMaxRange ) return COND_TOO_FAR_TO_ATTACK; return COND_CAN_RANGE_ATTACK2; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CNPC_VehicleDriver::TranslateSchedule( int scheduleType ) { switch ( scheduleType ) { case SCHED_COMBAT_FACE: { // Vehicles can't rotate, so don't try and face return TranslateSchedule( SCHED_CHASE_ENEMY ); } break; case SCHED_ALERT_FACE: { // Vehicles can't rotate, so don't try and face return SCHED_ALERT_STAND; } break; case SCHED_CHASE_ENEMY_FAILED: case SCHED_FAIL: { return SCHED_FAIL; } break; } return BaseClass::TranslateSchedule(scheduleType); } //----------------------------------------------------------------------------- // Purpose: // Input : *pTask - //----------------------------------------------------------------------------- void CNPC_VehicleDriver::StartTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_RUN_PATH: case TASK_WALK_PATH: TaskComplete(); break; case TASK_FACE_IDEAL: case TASK_FACE_ENEMY: { // Vehicle ignores face commands, since it can't rotate on the spot. TaskComplete(); } break; case TASK_VEHICLEDRIVER_GET_PATH: { if ( !GetGoalEnt() ) { TaskFail( FAIL_NO_TARGET ); return; } CheckForTeleport(); if ( g_debug_vehicledriver.GetInt() & DRIVER_DEBUG_PATH ) { NDebugOverlay::Box( GetGoalEnt()->GetAbsOrigin(), -Vector(50,50,50), Vector(50,50,50), 255,255,255, true, 5); } AI_NavGoal_t goal( GOALTYPE_PATHCORNER, GetGoalEnt()->GetLocalOrigin(), ACT_WALK, AIN_DEF_TOLERANCE, AIN_YAW_TO_DEST); if ( !GetNavigator()->SetGoal( goal ) ) { TaskFail( FAIL_NO_ROUTE ); return; } TaskComplete(); } break; case TASK_WAIT_FOR_MOVEMENT: { if (GetNavigator()->GetGoalType() == GOALTYPE_NONE) { TaskComplete(); GetNavigator()->StopMoving(); // Stop moving } else if (!GetNavigator()->IsGoalActive()) { SetIdealActivity( GetStoppedActivity() ); } else { // Check validity of goal type ValidateNavGoal(); } } break; default: BaseClass::StartTask( pTask ); break; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::RunTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_RANGE_ATTACK1: { // Vehicle driver has no animations, so fire a burst at the target CBaseEntity *pEnemy = GetEnemy(); if ( pEnemy ) { // TODO: Get a bodytarget from the firing point of the gun in the vehicle Vector vecTarget = GetEnemy()->BodyTarget( GetAbsOrigin(), false ); m_pVehicleInterface->NPC_AimPrimaryWeapon( vecTarget ); m_pVehicleInterface->NPC_PrimaryFire(); TaskComplete(); } else { TaskFail(FAIL_NO_ENEMY); return; } } break; case TASK_RANGE_ATTACK2: { // Vehicle driver has no animations, so fire a burst at the target CBaseEntity *pEnemy = GetEnemy(); if ( pEnemy ) { // TODO: Get a bodytarget from the firing point of the gun in the vehicle Vector vecTarget = GetEnemy()->BodyTarget( GetAbsOrigin(), false ); m_pVehicleInterface->NPC_AimSecondaryWeapon( vecTarget ); m_pVehicleInterface->NPC_SecondaryFire(); TaskComplete(); } else { TaskFail(FAIL_NO_ENEMY); return; } } break; case TASK_WAIT_FOR_MOVEMENT: { BaseClass::RunTask( pTask ); if ( HasCondition(COND_SEE_ENEMY) ) { // we can see the enemy if ( HasCondition(COND_CAN_RANGE_ATTACK2) ) { ChainRunTask( TASK_RANGE_ATTACK2, pTask->flTaskData ); } if ( HasCondition(COND_CAN_RANGE_ATTACK1) ) { ChainRunTask( TASK_RANGE_ATTACK1, pTask->flTaskData ); } } } break; default: BaseClass::RunTask( pTask ); break; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::GatherEnemyConditions( CBaseEntity *pEnemy ) { BaseClass::GatherEnemyConditions(pEnemy); } //----------------------------------------------------------------------------- // Purpose: Overridden because if the player is a criminal, we hate them. //----------------------------------------------------------------------------- Disposition_t CNPC_VehicleDriver::IRelationType(CBaseEntity *pTarget) { // If it's the player and they are a criminal, we hate them. if ( pTarget && pTarget->Classify() == CLASS_PLAYER) { if (GlobalEntity_GetState("gordon_precriminal") == GLOBAL_ON) { return(D_NU); } } return(BaseClass::IRelationType(pTarget)); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CNPC_VehicleDriver::OverrideMove( float flInterval ) { if ( !m_hVehicleEntity ) return true; // If we don't have a maxspeed, we've been stopped, so abort early // Or we've been picked up by something (dropship probably). if ( !m_flMaxSpeed || m_hVehicleEntity->GetParent() ) { m_pVehicleInterface->NPC_Brake(); return true; } // ----------------------------------------------------------------- // If I have a route, keep it updated and move toward target // ------------------------------------------------------------------ if (GetNavigator()->IsGoalActive()) { if ( OverridePathMove( flInterval ) ) return true; } return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::CalculatePostPoints( void ) { m_vecPostPoint = m_vecDesiredPosition; m_vecPostPostPoint = m_vecDesiredPosition; // If we have a waypoint beyond our current, use it instead. if ( !GetNavigator()->CurWaypointIsGoal() ) { AI_Waypoint_t *pCurWaypoint = GetNavigator()->GetPath()->GetCurWaypoint(); m_vecPostPoint = pCurWaypoint->GetNext()->GetPos(); if ( pCurWaypoint->GetNext()->GetNext() ) { m_vecPostPostPoint = pCurWaypoint->GetNext()->GetNext()->GetPos(); } else { m_vecPostPostPoint = m_vecPostPoint; } } } //----------------------------------------------------------------------------- // Purpose: Destroy our current waypoints //----------------------------------------------------------------------------- void CNPC_VehicleDriver::ClearWaypoints( void ) { m_vecDesiredPosition = vec3_origin; if ( m_pCurrentWaypoint ) { delete m_pCurrentWaypoint; m_pCurrentWaypoint = NULL; } if ( m_pNextWaypoint ) { delete m_pNextWaypoint; m_pNextWaypoint = NULL; } } //----------------------------------------------------------------------------- // Purpose: We've hit a waypoint. Handle it, and return true if this is the // end of the path. //----------------------------------------------------------------------------- bool CNPC_VehicleDriver::WaypointReached( void ) { // We reached our current waypoint. m_vecPrevPrevPoint = m_vecPrevPoint; m_vecPrevPoint = GetAbsOrigin(); // If we've got to our goal, we're done here. if ( GetNavigator()->CurWaypointIsGoal() ) { // Necessary for InPass outputs to be fired, is a no-op otherwise GetNavigator()->AdvancePath(); // Stop pathing ClearWaypoints(); TaskComplete(); SetGoalEnt( NULL ); return true; } AI_Waypoint_t *pCurWaypoint = GetNavigator()->GetPath()->GetCurWaypoint(); if ( !pCurWaypoint ) return false; // Check to see if the waypoint wants us to change speed if ( pCurWaypoint->Flags() & bits_WP_TO_PATHCORNER ) { CBaseEntity *pEntity = pCurWaypoint->hPathCorner; if ( pEntity ) { if ( pEntity->m_flSpeed > 0 ) { if ( pEntity->m_flSpeed <= 1.0 ) { m_flDriversMaxSpeed = pEntity->m_flSpeed; RecalculateSpeeds(); } else { Warning("path_track %s tried to tell the npc_vehicledriver to set speed to %.3f. npc_vehicledriver only accepts values between 0 and 1.\n", STRING(pEntity->GetEntityName()), pEntity->m_flSpeed ); } } } } // Get the waypoints for the next part of the path GetNavigator()->AdvancePath(); if ( !GetNavigator()->GetPath()->GetCurWaypoint() ) { ClearWaypoints(); TaskComplete(); SetGoalEnt( NULL ); return true; } m_vecDesiredPosition = GetNavigator()->GetCurWaypointPos(); CalculatePostPoints(); // Move to the next waypoint delete m_pCurrentWaypoint; m_pCurrentWaypoint = m_pNextWaypoint; m_Waypoints[1] = new CVehicleWaypoint( m_vecPrevPoint, m_vecDesiredPosition, m_vecPostPoint, m_vecPostPostPoint ); m_pNextWaypoint = m_Waypoints[1]; // Drop the spline marker back m_flDistanceAlongSpline = MAX( 0, m_flDistanceAlongSpline - 1.0 ); CheckForTeleport(); return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CNPC_VehicleDriver::OverridePathMove( float flInterval ) { // Setup our initial path data if we've just started running a path if ( !m_pCurrentWaypoint ) { m_vecPrevPoint = GetAbsOrigin(); m_vecPrevPrevPoint = GetAbsOrigin(); m_vecDesiredPosition = GetNavigator()->GetCurWaypointPos(); CalculatePostPoints(); // Init our two waypoints m_Waypoints[0] = new CVehicleWaypoint( m_vecPrevPrevPoint, m_vecPrevPoint, m_vecDesiredPosition, m_vecPostPoint ); m_Waypoints[1] = new CVehicleWaypoint( m_vecPrevPoint, m_vecDesiredPosition, m_vecPostPoint, m_vecPostPostPoint ); m_pCurrentWaypoint = m_Waypoints[0]; m_pNextWaypoint = m_Waypoints[1]; m_flDistanceAlongSpline = 0.2; } // Have we reached our target? See if we've passed the current waypoint's plane. Vector vecAbsMins, vecAbsMaxs; CollisionProp()->WorldSpaceAABB( &vecAbsMins, &vecAbsMaxs ); if ( BoxOnPlaneSide( vecAbsMins, vecAbsMaxs, &m_pCurrentWaypoint->planeWaypoint ) == 3 ) { if ( WaypointReached() ) return true; } // Did we bypass it and reach the next one already? if ( m_pNextWaypoint && BoxOnPlaneSide( vecAbsMins, vecAbsMaxs, &m_pNextWaypoint->planeWaypoint ) == 3 ) { if ( WaypointReached() ) return true; } // We may have just teleported, so check to make sure we have a waypoint if ( !m_pCurrentWaypoint || !m_pNextWaypoint ) return false; // Figure out which spline we're trucking along CVehicleWaypoint *pCurrentSplineBeingTraversed = m_pCurrentWaypoint; if ( m_flDistanceAlongSpline > 1 ) { pCurrentSplineBeingTraversed = m_pNextWaypoint; } // Get our current speed, and check it against the length of the spline to know how far to advance our marker AngularImpulse angVel; Vector vecVelocity; IPhysicsObject *pVehiclePhysics = m_hVehicleEntity->VPhysicsGetObject(); if( !pVehiclePhysics ) { // I think my vehicle has been destroyed. return false; } pVehiclePhysics->GetVelocity( &vecVelocity, &angVel ); float flSpeed = vecVelocity.Length(); float flIncTime = gpGlobals->curtime - GetLastThink(); float flIncrement = flIncTime * (flSpeed / pCurrentSplineBeingTraversed->GetLength()); // Now advance our point along the spline m_flDistanceAlongSpline = clamp( m_flDistanceAlongSpline + flIncrement, 0, 2); if ( m_flDistanceAlongSpline > 1 ) { // We crossed the spline boundary pCurrentSplineBeingTraversed = m_pNextWaypoint; } Vector vSplinePoint = pCurrentSplineBeingTraversed->GetPointAt( m_flDistanceAlongSpline > 1 ? m_flDistanceAlongSpline-1 : m_flDistanceAlongSpline ); Vector vSplineTangent = pCurrentSplineBeingTraversed->GetTangentAt( m_flDistanceAlongSpline > 1 ? m_flDistanceAlongSpline-1 : m_flDistanceAlongSpline ); // Now that we've got the target spline point & tangent, use it to decide what our desired velocity is. // If we're close to the tangent, just use the tangent. Otherwise, Lerp towards it. Vector vecToDesired = (vSplinePoint - GetAbsOrigin()); float flDistToDesired = VectorNormalize( vecToDesired ); float flTangentLength = VectorNormalize( vSplineTangent ); if ( flDistToDesired > (flTangentLength * 0.75) ) { m_vecDesiredVelocity = vecToDesired * flTangentLength; } else { VectorLerp( vSplineTangent, vecToDesired * flTangentLength, (flDistToDesired / (flTangentLength * 0.5)), m_vecDesiredVelocity ); } // Decrease speed according to the turn we're trying to make Vector vecRight; m_hVehicleEntity->GetVectors( NULL, &vecRight, NULL ); Vector vecNormVel = m_vecDesiredVelocity; VectorNormalize( vecNormVel ); float flDotRight = DotProduct( vecRight, vecNormVel ); flSpeed = (1.0 - fabs(flDotRight)); // Don't go slower than we've been told to go if ( flSpeed < m_flDriversMinSpeed ) { flSpeed = m_flDriversMinSpeed; } m_vecDesiredVelocity = vecNormVel * (flSpeed * m_flMaxSpeed); // Bunch o'debug if ( g_debug_vehicledriver.GetInt() & DRIVER_DEBUG_PATH ) { NDebugOverlay::Box( m_vecPrevPrevPoint, -Vector(15,15,15), Vector(15,15,15), 192,0,0, true, 0.1); NDebugOverlay::Box( m_vecPrevPoint, -Vector(20,20,20), Vector(20,20,20), 255,0,0, true, 0.1); NDebugOverlay::Box( m_vecPostPoint, -Vector(20,20,20), Vector(20,20,20), 0,192,0, true, 0.1); NDebugOverlay::Box( m_vecPostPostPoint, -Vector(20,20,20), Vector(20,20,20), 0,128,0, true, 0.1); NDebugOverlay::Box( vSplinePoint, -Vector(10,10,10), Vector(10,10,10), 0,0,255, true, 0.1); NDebugOverlay::Line( vSplinePoint, vSplinePoint + (vSplineTangent * 40), 0,0,255, true, 0.1); //NDebugOverlay::HorzArrow( pCurrentSplineBeingTraversed->splinePoints[0], pCurrentSplineBeingTraversed->splinePoints[1], 30, 255,255,255,0, false, 0.1f ); //NDebugOverlay::HorzArrow( pCurrentSplineBeingTraversed->splinePoints[1], pCurrentSplineBeingTraversed->splinePoints[2], 20, 255,255,255,0, false, 0.1f ); //NDebugOverlay::HorzArrow( pCurrentSplineBeingTraversed->splinePoints[2], pCurrentSplineBeingTraversed->splinePoints[3], 10, 255,255,255,0, false, 0.1f ); // Draw the plane we're checking against for waypoint passing Vector vecPlaneRight; CrossProduct( m_pCurrentWaypoint->planeWaypoint.normal, Vector(0,0,1), vecPlaneRight ); Vector vecPlane = m_pCurrentWaypoint->splinePoints[2]; NDebugOverlay::Line( vecPlane + (vecPlaneRight * -100), vecPlane + (vecPlaneRight * 100), 255,0,0, true, 0.1); // Draw the next plane too CrossProduct( m_pNextWaypoint->planeWaypoint.normal, Vector(0,0,1), vecPlaneRight ); vecPlane = m_pNextWaypoint->splinePoints[2]; NDebugOverlay::Line( vecPlane + (vecPlaneRight * -100), vecPlane + (vecPlaneRight * 100), 192,0,0, true, 0.1); } if ( g_debug_vehicledriver.GetInt() & DRIVER_DEBUG_PATH_SPLINE ) { for ( int i = 0; i < 10; i++ ) { Vector vecTarget = m_pCurrentWaypoint->GetPointAt( 0.1 * i ); Vector vecTangent = m_pCurrentWaypoint->GetTangentAt( 0.1 * i ); VectorNormalize(vecTangent); NDebugOverlay::Box( vecTarget, -Vector(10,10,10), Vector(10,10,10), 255,0,0, true, 0.1 ); NDebugOverlay::Line( vecTarget, vecTarget + (vecTangent * 10), 255,255,0, true, 0.1); } } return true; } //----------------------------------------------------------------------------- // Purpose: This takes the current place the NPC's trying to get to, figures out // what keys to press to get the vehicle to go there, and then sends // them to the vehicle. //----------------------------------------------------------------------------- void CNPC_VehicleDriver::DriveVehicle( void ) { AngularImpulse angVel; Vector vecVelocity; IPhysicsObject *pVehiclePhysics = m_hVehicleEntity->VPhysicsGetObject(); if ( !pVehiclePhysics ) return; pVehiclePhysics->GetVelocity( &vecVelocity, &angVel ); float flSpeed = VectorNormalize( vecVelocity ); // If we have no target position to drive to, brake to a halt if ( !m_flMaxSpeed || m_vecDesiredPosition == vec3_origin ) { if ( flSpeed > 1 ) { m_pVehicleInterface->NPC_Brake(); } return; } if ( g_debug_vehicledriver.GetInt() & DRIVER_DEBUG_PATH ) { NDebugOverlay::Box(m_vecDesiredPosition, -Vector(20,20,20), Vector(20,20,20), 0,255,0, true, 0.1); NDebugOverlay::Line(GetAbsOrigin(), GetAbsOrigin() + m_vecDesiredVelocity, 0,255,0, true, 0.1); } m_flGoalSpeed = VectorNormalize(m_vecDesiredVelocity); // Is our target in front or behind us? Vector vecForward, vecRight; m_hVehicleEntity->GetVectors( &vecForward, &vecRight, NULL ); float flDot = DotProduct( vecForward, m_vecDesiredVelocity ); bool bBehind = ( flDot < 0 ); float flVelDot = DotProduct( vecVelocity, m_vecDesiredVelocity ); bool bGoingWrongWay = ( flVelDot < 0 ); // Figure out whether we should accelerate / decelerate if ( bGoingWrongWay || (flSpeed < m_flGoalSpeed) ) { // If it's behind us, go backwards not forwards if ( bBehind ) { m_pVehicleInterface->NPC_ThrottleReverse(); } else { m_pVehicleInterface->NPC_ThrottleForward(); } } else { // Brake if we're go significantly too fast if ( (flSpeed - 200) > m_flGoalSpeed ) { m_pVehicleInterface->NPC_Brake(); } else { m_pVehicleInterface->NPC_ThrottleCenter(); } } // Do we need to turn? float flDotRight = DotProduct( vecRight, m_vecDesiredVelocity ); if ( bBehind ) { // If we're driving backwards, flip our turning flDotRight *= -1; } // Map it to the vehicle's steering flDotRight *= (m_flSteering / 90); if ( flDotRight < 0 ) { // Turn left m_pVehicleInterface->NPC_TurnLeft( -flDotRight ); } else if ( flDotRight > 0 ) { // Turn right m_pVehicleInterface->NPC_TurnRight( flDotRight ); } else { m_pVehicleInterface->NPC_TurnCenter(); } } //----------------------------------------------------------------------------- // Purpose: Check to see if we should teleport to the current path corner //----------------------------------------------------------------------------- void CNPC_VehicleDriver::CheckForTeleport( void ) { if ( !GetGoalEnt() ) return; CPathTrack *pTrack = dynamic_cast( GetGoalEnt() ); if ( !pTrack ) return; // Does it have the teleport flag set? if ( pTrack->HasSpawnFlags( SF_PATH_TELEPORT ) ) { AddEffects( EF_NOINTERP ); // Teleport the vehicle to the pathcorner Vector vecMins, vecMaxs; vecMins = m_hVehicleEntity->CollisionProp()->OBBMins(); vecMaxs = m_hVehicleEntity->CollisionProp()->OBBMaxs(); Vector vecTarget = pTrack->GetAbsOrigin() - (vecMins + vecMaxs) * 0.5; vecTarget.z += ((vecMaxs.z - vecMins.z) * 0.5) + 8; // Safety buffer // Orient it to face the next point QAngle vecAngles = pTrack->GetAbsAngles(); Vector vecToTarget = vec3_origin; if ( pTrack->GetNext() ) { vecToTarget = (pTrack->GetNext()->GetAbsOrigin() - pTrack->GetAbsOrigin()); VectorNormalize( vecToTarget ); // Vehicles are rotated 90 degrees VectorAngles( vecToTarget, vecAngles ); vecAngles[YAW] -= 90; } m_hVehicleEntity->Teleport( &vecTarget, &vecAngles, &vec3_origin ); // Teleport the driver SetAbsOrigin( m_hVehicleEntity->WorldSpaceCenter() ); SetAbsAngles( m_hVehicleEntity->GetAbsAngles() ); m_vecPrevPoint = pTrack->GetAbsOrigin(); // Move to the next waypoint, we've reached this one if ( GetNavigator()->GetPath() ) { WaypointReached(); } // Clear our waypoints, because the next waypoint is certainly invalid now. ClearWaypoints(); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- float CNPC_VehicleDriver::GetDefaultNavGoalTolerance() { return 48; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::RecalculateSpeeds( void ) { // Get data from the vehicle const vehicleparams_t *pParams = m_pVehicleInterface->GetVehicleParams(); if ( pParams ) { m_flMaxSpeed = pParams->engine.maxSpeed * m_flDriversMaxSpeed; m_flSteering = pParams->steering.degreesSlow; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::InputSetDriversMaxSpeed( inputdata_t &inputdata ) { m_flDriversMaxSpeed = inputdata.value.Float(); RecalculateSpeeds(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::InputSetDriversMinSpeed( inputdata_t &inputdata ) { m_flDriversMinSpeed = inputdata.value.Float(); RecalculateSpeeds(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::InputStartForward( inputdata_t &inputdata ) { CLEARBITS( m_spawnflags, SF_VEHICLEDRIVER_INACTIVE ); if ( m_NPCState == NPC_STATE_IDLE ) { SetState( NPC_STATE_ALERT ); } SetCondition( COND_PROVOKED ); RecalculateSpeeds(); } //----------------------------------------------------------------------------- // Purpose: Tell the driver to stop moving //----------------------------------------------------------------------------- void CNPC_VehicleDriver::InputStop( inputdata_t &inputdata ) { m_flMaxSpeed = 0; } //----------------------------------------------------------------------------- // Purpose: Tell the driver to start firing at targets //----------------------------------------------------------------------------- void CNPC_VehicleDriver::InputStartFiring( inputdata_t &inputdata ) { CLEARBITS( m_spawnflags, SF_VEHICLEDRIVER_INACTIVE ); SetCondition( COND_PROVOKED ); float flMinRange, flMaxRange; // If the vehicle has a weapon, set our capability if ( m_pVehicleInterface->NPC_HasPrimaryWeapon() ) { CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1 ); m_pVehicleInterface->Weapon_PrimaryRanges( &flMinRange, &flMaxRange ); // Ensure the look distances is long enough if ( m_flDistTooFar < flMaxRange || GetSenses()->GetDistLook() < flMaxRange ) { m_flDistTooFar = flMaxRange; SetDistLook( flMaxRange ); } } if ( m_pVehicleInterface->NPC_HasSecondaryWeapon() ) { CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK2 ); m_pVehicleInterface->Weapon_SecondaryRanges( &flMinRange, &flMaxRange ); // Ensure the look distances is long enough if ( m_flDistTooFar < flMaxRange || GetSenses()->GetDistLook() < flMaxRange ) { m_flDistTooFar = flMaxRange; SetDistLook( flMaxRange ); } } } //----------------------------------------------------------------------------- // Purpose: Tell the driver to stop firing at targets //----------------------------------------------------------------------------- void CNPC_VehicleDriver::InputStopFiring( inputdata_t &inputdata ) { // If the vehicle has a weapon, set our capability CapabilitiesRemove( bits_CAP_INNATE_RANGE_ATTACK1 ); CapabilitiesRemove( bits_CAP_INNATE_RANGE_ATTACK2 ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_VehicleDriver::InputGotoPathCorner( inputdata_t &inputdata ) { string_t iszPathName = inputdata.value.StringID(); if ( iszPathName != NULL_STRING ) { CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, iszPathName ); if ( !pEntity ) { Warning("npc_vehicledriver %s couldn't find entity named %s\n", STRING(GetEntityName()), STRING(iszPathName) ); return; } ClearWaypoints(); // Drive to the point SetGoalEnt( pEntity ); if ( m_NPCState == NPC_STATE_IDLE ) { SetState( NPC_STATE_ALERT ); } SetCondition( COND_PROVOKED ); // Force him to start forward InputStartForward( inputdata ); } } //----------------------------------------------------------------------------- // // Schedules // //----------------------------------------------------------------------------- AI_BEGIN_CUSTOM_NPC( npc_vehicledriver, CNPC_VehicleDriver ) //Tasks DECLARE_TASK( TASK_VEHICLEDRIVER_GET_PATH ) // Schedules DEFINE_SCHEDULE ( SCHED_VEHICLEDRIVER_INACTIVE, " Tasks" " TASK_WAIT_INDEFINITE 0" "" " Interrupts" " COND_PROVOKED" ) DEFINE_SCHEDULE ( SCHED_VEHICLEDRIVER_COMBAT_WAIT, " Tasks" " TASK_WAIT 5" "" " Interrupts" " COND_NEW_ENEMY" " COND_LIGHT_DAMAGE" " COND_HEAVY_DAMAGE" " COND_PROVOKED" " COND_CAN_RANGE_ATTACK1" " COND_CAN_RANGE_ATTACK2" ) DEFINE_SCHEDULE ( SCHED_VEHICLEDRIVER_DRIVE_PATH, " Tasks" " TASK_VEHICLEDRIVER_GET_PATH 0" " TASK_WALK_PATH 9999" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_WAIT_PVS 0" "" " Interrupts" " COND_NEW_ENEMY" " COND_PROVOKED" ) AI_END_CUSTOM_NPC()