//========= Copyright 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" #include "vehicle_crane.h" #include "saverestore_utlvector.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" extern ConVar g_debug_vehicledriver; //========================================================= // Custom schedules //========================================================= enum { SCHED_CRANE_RANGE_ATTACK1 = LAST_VEHICLEDRIVER_SCHED, SCHED_CRANE_FIND_LARGE_OBJECT, SCHED_CRANE_PICKUP_OBJECT, SCHED_CRANE_FORCED_GO, SCHED_CRANE_CHASE_ENEMY, SCHED_CRANE_FORCED_DROP, }; //========================================================= // Custom tasks //========================================================= enum { TASK_CRANE_GET_POSITION_OVER_ENEMY = LAST_VEHICLEDRIVER_TASK, TASK_CRANE_GET_POSITION_OVER_LASTPOSITION, TASK_CRANE_GET_POSITION_OVER_OBJECT, TASK_CRANE_TURN_MAGNET_OFF, TASK_CRANE_FIND_OBJECT_TO_PICKUP, TASK_CRANE_DROP_MAGNET, TASK_END_FORCED_DROP, }; //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- class CNPC_CraneDriver : public CNPC_VehicleDriver { DECLARE_CLASS( CNPC_CraneDriver, CNPC_VehicleDriver ); public: DECLARE_DATADESC(); DEFINE_CUSTOM_AI; void Spawn( void ); void Activate( void ); // AI int RangeAttack1Conditions( float flDot, float flDist ); int TranslateSchedule( int scheduleType ); int SelectSchedule( void ); void StartTask( const Task_t *pTask ); void RunTask( const Task_t *pTask ); void SetDesiredPosition( const Vector &vecPosition ); // Driving void DriveVehicle( void ); bool OverrideMove( float flInterval ); // Inputs void InputForcePickup( inputdata_t &inputdata ); void InputForceDrop( inputdata_t &inputdata ); protected: CHandle m_hCrane; EHANDLE m_hPickupTarget; float m_flDistanceToTarget; CUtlVector< EHANDLE > m_PreviouslyPickedUpObjects; bool m_bForcedPickup; bool m_bForcedDropoff; float m_flDropWait; float m_flReleasePause; float m_flReleaseAt; // Outputs COutputEvent m_OnPickedUpObject; COutputEvent m_OnDroppedObject; COutputEvent m_OnPausingBeforeDrop; }; BEGIN_DATADESC( CNPC_CraneDriver ) // Inputs DEFINE_INPUTFUNC( FIELD_STRING, "ForcePickup", InputForcePickup ), DEFINE_INPUTFUNC( FIELD_STRING, "ForceDrop", InputForceDrop ), //DEFINE_FIELD( m_hCrane, FIELD_EHANDLE ), DEFINE_FIELD( m_hPickupTarget, FIELD_EHANDLE ), DEFINE_FIELD( m_flDistanceToTarget, FIELD_FLOAT ), DEFINE_UTLVECTOR( m_PreviouslyPickedUpObjects, FIELD_EHANDLE ), DEFINE_FIELD( m_bForcedPickup, FIELD_BOOLEAN ), DEFINE_FIELD( m_bForcedDropoff, FIELD_BOOLEAN ), DEFINE_FIELD( m_flDropWait, FIELD_FLOAT ), DEFINE_KEYFIELD( m_flReleasePause, FIELD_FLOAT, "releasepause" ), DEFINE_FIELD( m_flReleaseAt, FIELD_FLOAT ), // Outputs DEFINE_OUTPUT( m_OnPickedUpObject, "OnPickedUpObject" ), DEFINE_OUTPUT( m_OnDroppedObject, "OnDroppedObject" ), DEFINE_OUTPUT( m_OnPausingBeforeDrop, "OnPausingBeforeDrop" ), END_DATADESC() LINK_ENTITY_TO_CLASS( npc_cranedriver, CNPC_CraneDriver ); //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ void CNPC_CraneDriver::Spawn( void ) { BaseClass::Spawn(); CapabilitiesClear(); CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1 ); m_flDistTooFar = 2048.0; SetDistLook( 2048 ); m_PreviouslyPickedUpObjects.Purge(); m_hPickupTarget = NULL; m_bForcedPickup = false; m_bForcedDropoff = false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_CraneDriver::Activate( void ) { BaseClass::Activate(); m_hCrane = dynamic_cast((CBaseEntity*)m_hVehicleEntity); if ( !m_hCrane ) { Warning( "npc_cranedriver %s couldn't find his crane named %s.\n", STRING(GetEntityName()), STRING(m_iszVehicleName) ); UTIL_Remove( this ); return; } } //----------------------------------------------------------------------------- // Purpose: // Input : // Output : //----------------------------------------------------------------------------- int CNPC_CraneDriver::RangeAttack1Conditions( float flDot, float flDist ) { if ( !HasCondition( COND_SEE_ENEMY ) ) return COND_NONE; // Do our distance check in 2D Vector2D vecOrigin2D( m_hCrane->GetAbsOrigin().x, m_hCrane->GetAbsOrigin().y ); Vector2D vecEnemy2D( GetEnemy()->GetAbsOrigin().x, GetEnemy()->GetAbsOrigin().y ); flDist = (vecOrigin2D - vecEnemy2D).Length(); // Maximum & Minimum size of the crane's reach if ( flDist > MAX_CRANE_FLAT_REACH ) return COND_TOO_FAR_TO_ATTACK; // Crane can't reach any closer than this if ( flDist < MIN_CRANE_FLAT_REACH ) return COND_TOO_CLOSE_TO_ATTACK; return COND_CAN_RANGE_ATTACK1; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CNPC_CraneDriver::SelectSchedule( void ) { if ( HasSpawnFlags(SF_VEHICLEDRIVER_INACTIVE) ) return BaseClass::SelectSchedule(); // If we've got an object to pickup, so go get it if ( m_hPickupTarget ) { // Only clear the pickup target if we managed to pick something up if ( m_hCrane->GetTotalMassOnCrane() > 0 ) { if ( m_bForcedPickup ) { m_OnPickedUpObject.FireOutput( m_hPickupTarget, this ); } // Remember what we dropped so we go try something else if we can. m_PreviouslyPickedUpObjects.AddToTail( m_hPickupTarget ); m_hPickupTarget = NULL; } else { if ( m_NPCState == NPC_STATE_IDLE ) { SetIdealState( NPC_STATE_ALERT ); } return SCHED_CRANE_PICKUP_OBJECT; } } // If we're currently being forced to pickup something, do only that if ( m_bForcedPickup ) { if ( m_hPickupTarget ) return SCHED_CRANE_PICKUP_OBJECT; // We've picked up our target, we're waiting to be told where to put it return SCHED_IDLE_STAND; } // If we've been told to drop something off, do that if ( m_bForcedDropoff ) return SCHED_CRANE_FORCED_DROP; switch ( m_NPCState ) { case NPC_STATE_IDLE: break; case NPC_STATE_ALERT: break; case NPC_STATE_COMBAT: if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) ) { // Do we have anything on the crane? If not, look for something if ( m_hCrane->GetTotalMassOnCrane() == 0 ) return SCHED_CRANE_FIND_LARGE_OBJECT; // We've got something on the crane, so try and drop it on the enemy return SCHED_CRANE_RANGE_ATTACK1; } // We can't attack him, so if we don't have anything on the crane, grab something if ( m_hCrane->GetTotalMassOnCrane() == 0 ) return SCHED_CRANE_FIND_LARGE_OBJECT; } return BaseClass::SelectSchedule(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CNPC_CraneDriver::TranslateSchedule( int scheduleType ) { switch ( scheduleType ) { case SCHED_COMBAT_FACE: // Vehicles can't rotate, so don't try and face return TranslateSchedule( SCHED_CHASE_ENEMY ); case SCHED_ALERT_FACE: // Vehicles can't rotate, so don't try and face return SCHED_ALERT_STAND; case SCHED_FORCED_GO: return SCHED_CRANE_FORCED_GO; case SCHED_CHASE_ENEMY: return SCHED_CRANE_CHASE_ENEMY; } return BaseClass::TranslateSchedule(scheduleType); } //----------------------------------------------------------------------------- // Purpose: // Input : *pTask - //----------------------------------------------------------------------------- void CNPC_CraneDriver::StartTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_WAIT_FOR_MOVEMENT: break; case TASK_CRANE_GET_POSITION_OVER_ENEMY: { if ( !GetEnemy() ) { TaskFail(FAIL_NO_ROUTE); return; } SetDesiredPosition( GetEnemy()->GetAbsOrigin() ); TaskComplete(); } break; case TASK_CRANE_GET_POSITION_OVER_OBJECT: { if ( !m_hPickupTarget ) { TaskFail("No object to pickup!"); return; } SetDesiredPosition( m_hPickupTarget->GetAbsOrigin() ); TaskComplete(); } break; case TASK_CRANE_GET_POSITION_OVER_LASTPOSITION: { SetDesiredPosition( m_vecLastPosition ); TaskComplete(); } break; case TASK_CRANE_TURN_MAGNET_OFF: { // If we picked up something, and we were being forced to pick something up, fire our output if ( m_hCrane->GetTotalMassOnCrane() > 0 && m_bForcedDropoff ) { // Are we supposed to pause first? if ( m_flReleasePause ) { m_flReleaseAt = gpGlobals->curtime + m_flReleasePause; m_OnPausingBeforeDrop.FireOutput( this, this ); return; } m_OnDroppedObject.FireOutput( this, this ); } m_hCrane->TurnMagnetOff(); TaskComplete(); } break; case TASK_END_FORCED_DROP: { m_bForcedDropoff = false; TaskComplete(); } break; case TASK_CRANE_FIND_OBJECT_TO_PICKUP: { Vector2D vecOrigin2D( m_hCrane->GetAbsOrigin().x, m_hCrane->GetAbsOrigin().y ); // Find a large physics object within our reach to pickup float flLargestMass = 0; CBaseEntity *pLargestEntity = NULL; CBaseEntity *pList[1024]; Vector delta( m_flDistTooFar, m_flDistTooFar, m_flDistTooFar*2 ); int count = UTIL_EntitiesInBox( pList, 1024, m_hCrane->GetAbsOrigin() - delta, m_hCrane->GetAbsOrigin() + delta, 0 ); for ( int i = 0; i < count; i++ ) { if ( !pList[i] ) continue; // Ignore the crane & the magnet if ( pList[i] == m_hCrane || pList[i] == m_hCrane->GetMagnet() ) continue; if ( m_PreviouslyPickedUpObjects.Find( pList[i] ) != m_PreviouslyPickedUpObjects.InvalidIndex() ) continue; // Get the VPhysics object IPhysicsObject *pPhysics = pList[i]->VPhysicsGetObject(); if ( pPhysics && pList[i]->GetMoveType() == MOVETYPE_VPHYSICS ) { float flMass = pPhysics->GetMass(); if ( flMass > flLargestMass && (flMass < MAXIMUM_CRANE_PICKUP_MASS) && (flMass > MINIMUM_CRANE_PICKUP_MASS) ) { // Biggest one we've found so far // Now make sure it's within our reach // Do our distance check in 2D Vector2D vecOrigin2D( m_hCrane->GetAbsOrigin().x, m_hCrane->GetAbsOrigin().y ); Vector2D vecEnemy2D( pList[i]->GetAbsOrigin().x, pList[i]->GetAbsOrigin().y ); float flDist = (vecOrigin2D - vecEnemy2D).Length(); // Maximum & Minimum size of the crane's reach if ( flDist > MAX_CRANE_FLAT_REACH ) continue; if ( flDist < MIN_CRANE_FLAT_REACH ) continue; flLargestMass = flMass; pLargestEntity = pList[i]; } } } // If we didn't find anything new, clear our list of targets if ( !pLargestEntity ) { m_PreviouslyPickedUpObjects.Purge(); } if ( !pLargestEntity ) { TaskFail("Couldn't find anything to pick up!"); return; } m_hPickupTarget = pLargestEntity; TaskComplete(); } break; case TASK_CRANE_DROP_MAGNET: { // Drop the magnet, but only end the task once the magnet's back up m_pVehicleInterface->NPC_SecondaryFire(); // Don't check to see if drop's finished until this time is up. // This is necessary because the crane won't start dropping this // frame, and our cranedriver will think it's finished immediately. m_flDropWait = gpGlobals->curtime + 0.5; } break; default: BaseClass::StartTask( pTask ); break; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_CraneDriver::RunTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_WAIT_FOR_MOVEMENT: { // Is the magnet over the target, and are we not moving too fast? AngularImpulse angVel; Vector vecVelocity; CBaseEntity *pMagnet = m_hCrane->GetMagnet(); IPhysicsObject *pVehiclePhysics = pMagnet->VPhysicsGetObject(); pVehiclePhysics->GetVelocity( &vecVelocity, &angVel ); float flVelocity = 100; float flDistance = 90; // More accurate on forced drops if ( m_bForcedPickup || m_bForcedDropoff ) { flVelocity = 10; flDistance = 30; } if ( m_flDistanceToTarget < flDistance && m_hCrane->GetTurnRate() < 0.1 && vecVelocity.Length() < flVelocity ) { TaskComplete(); } } break; case TASK_CRANE_DROP_MAGNET: { // Wait for the magnet to get back up if ( m_flDropWait < gpGlobals->curtime && !m_hCrane->IsDropping() ) { TaskComplete(); } } break; case TASK_CRANE_TURN_MAGNET_OFF: { // We're waiting for the pause length before dropping whatever's on our magnet if ( gpGlobals->curtime > m_flReleaseAt ) { if ( m_bForcedDropoff ) { m_OnDroppedObject.FireOutput( this, this ); } m_hCrane->TurnMagnetOff(); TaskComplete(); } } break; default: BaseClass::RunTask( pTask ); break; } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CNPC_CraneDriver::OverrideMove( float flInterval ) { return true; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_CraneDriver::SetDesiredPosition( const Vector &vecPosition ) { m_vecDesiredPosition = vecPosition; m_flDistanceToTarget = 999; } //----------------------------------------------------------------------------- // 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_CraneDriver::DriveVehicle( void ) { // No targets? if ( !GetEnemy() && m_vecDesiredPosition == vec3_origin ) return; Vector vecTarget = m_vecDesiredPosition; // Track our targets if ( m_hPickupTarget ) { vecTarget = m_hPickupTarget->GetAbsOrigin(); } else if ( !m_bForcedPickup && !m_bForcedDropoff && GetEnemy() ) { vecTarget = GetEnemy()->GetAbsOrigin(); } // Move the crane over the target // Use the crane type as a targeting point Vector vecCraneTip = m_hCrane->GetCraneTipPosition(); Vector2D vecCraneTip2D( vecCraneTip.x, vecCraneTip.y ); Vector2D vecTarget2D( vecTarget.x, vecTarget.y ); Vector2D vecOrigin2D( m_hCrane->GetAbsOrigin().x, m_hCrane->GetAbsOrigin().y ); if ( g_debug_vehicledriver.GetInt() ) { NDebugOverlay::Box( vecTarget, -Vector(50,50,50), Vector(50,50,50), 0,255,0, true, 0.1 ); NDebugOverlay::Box( vecCraneTip, -Vector(2,2,5000), Vector(2,2,5), 0,255,0, true, 0.1 ); NDebugOverlay::Box( vecTarget, -Vector(2,2,5), Vector(2,2,5000), 0,255,0, true, 0.1 ); } // Store off the distance to our target m_flDistanceToTarget = (vecTarget2D - vecCraneTip2D).Length(); // First determine whether we need to extend / retract the arm float flDistToTarget = (vecOrigin2D - vecTarget2D).LengthSqr(); float flDistToCurrent = (vecOrigin2D - vecCraneTip2D).LengthSqr(); float flDelta = fabs(flDistToTarget - flDistToCurrent); // Slow down as we get closer, but do it based upon our current extension rate float flMinDelta = 50 + (50 * fabs(m_hCrane->GetExtensionRate() / CRANE_EXTENSION_RATE_MAX)); flMinDelta *= flMinDelta; if ( flDelta > flMinDelta ) { if ( flDistToCurrent > flDistToTarget ) { // Retract m_pVehicleInterface->NPC_ThrottleReverse(); } else if ( flDistToCurrent < flDistToTarget ) { // Extend m_pVehicleInterface->NPC_ThrottleForward(); } } else { m_pVehicleInterface->NPC_ThrottleCenter(); } // Then figure out if we need to rotate. Do it all in 2D space. Vector vecRight, vecForward; m_hCrane->GetVectors( &vecForward, &vecRight, NULL ); vecRight.z = 0; vecForward.z = 0; VectorNormalize( vecRight ); VectorNormalize( vecForward ); Vector vecToTarget = ( vecTarget - m_hCrane->GetAbsOrigin() ); vecToTarget.z = 0; VectorNormalize( vecToTarget ); float flDotRight = DotProduct( vecRight, vecToTarget ); float flDotForward = DotProduct( vecForward, vecToTarget ); // Start slowing if we're going to hit the point soon float flTurnInDeg = RAD2DEG( acos(flDotForward) ); float flSpeed = m_hCrane->GetMaxTurnRate() * (flTurnInDeg / 15.0); flSpeed = MIN( m_hCrane->GetMaxTurnRate(), flSpeed ); if ( fabs(flSpeed) < 0.05 ) { // We're approaching the target, so stop turning m_pVehicleInterface->NPC_TurnCenter(); } else { if ( flDotRight < 0 ) { // Turn right m_pVehicleInterface->NPC_TurnRight( flSpeed ); } else if ( flDotRight > 0 ) { // Turn left m_pVehicleInterface->NPC_TurnLeft( flSpeed ); } } } //----------------------------------------------------------------------------- // Purpose: Force the driver to pickup a specific entity // Input : &inputdata - //----------------------------------------------------------------------------- void CNPC_CraneDriver::InputForcePickup( inputdata_t &inputdata ) { string_t iszPickupName = inputdata.value.StringID(); if ( iszPickupName != NULL_STRING ) { // Turn the magnet off now to drop anything we might have already on the magnet m_hCrane->TurnMagnetOff(); m_hPickupTarget = gEntList.FindEntityByName( NULL, iszPickupName, NULL, inputdata.pActivator, inputdata.pCaller ); m_bForcedPickup = true; m_bForcedDropoff = false; SetCondition( COND_PROVOKED ); CLEARBITS( m_spawnflags, SF_VEHICLEDRIVER_INACTIVE ); } } //----------------------------------------------------------------------------- // Purpose: Force the driver to drop his held entity at a specific point // Input : &inputdata - //----------------------------------------------------------------------------- void CNPC_CraneDriver::InputForceDrop( inputdata_t &inputdata ) { string_t iszDropName = inputdata.value.StringID(); if ( iszDropName != NULL_STRING ) { CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, iszDropName, NULL, inputdata.pActivator, inputdata.pCaller ); if ( !pEntity ) { Warning("Crane couldn't find entity named %s\n", STRING(iszDropName) ); return; } m_bForcedPickup = false; m_bForcedDropoff = true; SetDesiredPosition( pEntity->GetAbsOrigin() ); SetCondition( COND_PROVOKED ); CLEARBITS( m_spawnflags, SF_VEHICLEDRIVER_INACTIVE ); } } //----------------------------------------------------------------------------- // // Schedules // //----------------------------------------------------------------------------- AI_BEGIN_CUSTOM_NPC( npc_cranedriver, CNPC_CraneDriver ) //Tasks DECLARE_TASK( TASK_CRANE_GET_POSITION_OVER_ENEMY ) DECLARE_TASK( TASK_CRANE_GET_POSITION_OVER_LASTPOSITION ) DECLARE_TASK( TASK_CRANE_GET_POSITION_OVER_OBJECT ) DECLARE_TASK( TASK_CRANE_TURN_MAGNET_OFF ) DECLARE_TASK( TASK_END_FORCED_DROP ) DECLARE_TASK( TASK_CRANE_FIND_OBJECT_TO_PICKUP ) DECLARE_TASK( TASK_CRANE_DROP_MAGNET ) //Schedules //================================================== // SCHED_ANTLION_CHASE_ENEMY_BURROW //================================================== DEFINE_SCHEDULE ( SCHED_CRANE_RANGE_ATTACK1, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY" " TASK_CRANE_GET_POSITION_OVER_ENEMY 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_CRANE_TURN_MAGNET_OFF 0" " " " Interrupts" " COND_ENEMY_DEAD" " COND_NEW_ENEMY" " COND_ENEMY_OCCLUDED" " COND_ENEMY_TOO_FAR" " COND_PROVOKED" ) DEFINE_SCHEDULE ( SCHED_CRANE_FIND_LARGE_OBJECT, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY" " TASK_CRANE_FIND_OBJECT_TO_PICKUP 0" " " " Interrupts" " COND_ENEMY_DEAD" " COND_NEW_ENEMY" " COND_ENEMY_OCCLUDED" " COND_ENEMY_TOO_FAR" ) DEFINE_SCHEDULE ( SCHED_CRANE_PICKUP_OBJECT, " Tasks" " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY" " TASK_CRANE_GET_POSITION_OVER_OBJECT 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_CRANE_DROP_MAGNET 0" " " " Interrupts" " COND_ENEMY_DEAD" " COND_NEW_ENEMY" " COND_ENEMY_OCCLUDED" " COND_ENEMY_TOO_FAR" " COND_PROVOKED" ) DEFINE_SCHEDULE ( SCHED_CRANE_FORCED_GO, " Tasks" " TASK_CRANE_GET_POSITION_OVER_LASTPOSITION 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_CRANE_TURN_MAGNET_OFF 0" " TASK_WAIT 2" " " " Interrupts" ) DEFINE_SCHEDULE ( SCHED_CRANE_CHASE_ENEMY, " Tasks" " TASK_CRANE_GET_POSITION_OVER_ENEMY 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_WAIT 5" " " " Interrupts" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" " COND_ENEMY_UNREACHABLE" " COND_CAN_RANGE_ATTACK1" " COND_TOO_CLOSE_TO_ATTACK" " COND_TASK_FAILED" " COND_LOST_ENEMY" " COND_PROVOKED" ) DEFINE_SCHEDULE ( SCHED_CRANE_FORCED_DROP, " Tasks" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_CRANE_TURN_MAGNET_OFF 0" " TASK_END_FORCED_DROP 0" " TASK_WAIT 2" " " " Interrupts" ) AI_END_CUSTOM_NPC()