//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // //=============================================================================// #include "cbase.h" #include "ai_behavior_actbusy.h" #include "ai_navigator.h" #include "ai_hint.h" #include "ai_behavior_follow.h" #include "KeyValues.h" #include "filesystem.h" #include "eventqueue.h" #include "ai_playerally.h" #include "SoundEmitterSystem/isoundemittersystembase.h" #include "entityblocker.h" #include "npcevent.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" #define ACTBUSY_SEE_ENTITY_TIMEOUT 1.0f #define ACTBUSY_COMBAT_PLAYER_MAX_DIST 720.0f // NPCs in combat actbusy should try to stay within this distance of the player. ConVar ai_actbusy_search_time( "ai_actbusy_search_time","10.0" ); ConVar ai_debug_actbusy( "ai_debug_actbusy", "0", FCVAR_CHEAT, "Used to debug actbusy behavior. Usage:\n\ 1: Constantly draw lines from NPCs to the actbusy nodes they've chosen to actbusy at.\n\ 2: Whenever an NPC makes a decision to use an actbusy, show which actbusy they've chosen.\n\ 3: Selected NPCs (with npc_select) will report why they're not choosing actbusy nodes.\n\ 4: Display debug output of actbusy logic.\n\ 5: Display safe zone volumes and info.\n\ "); // Anim events static int AE_ACTBUSY_WEAPON_FIRE_ON; static int AE_ACTBUSY_WEAPON_FIRE_OFF; BEGIN_DATADESC( CAI_ActBusyBehavior ) DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), DEFINE_FIELD( m_bForceActBusy, FIELD_BOOLEAN ), DEFINE_CUSTOM_FIELD( m_ForcedActivity, ActivityDataOps() ), DEFINE_FIELD( m_bTeleportToBusy, FIELD_BOOLEAN ), DEFINE_FIELD( m_bUseNearestBusy, FIELD_BOOLEAN ), DEFINE_FIELD( m_bLeaving, FIELD_BOOLEAN ), DEFINE_FIELD( m_bVisibleOnly, FIELD_BOOLEAN ), DEFINE_FIELD( m_bUseRenderBoundsForCollision, FIELD_BOOLEAN ), DEFINE_FIELD( m_flForcedMaxTime, FIELD_FLOAT ), DEFINE_FIELD( m_bBusy, FIELD_BOOLEAN ), DEFINE_FIELD( m_bMovingToBusy, FIELD_BOOLEAN ), DEFINE_FIELD( m_bNeedsToPlayExitAnim, FIELD_BOOLEAN ), DEFINE_FIELD( m_flNextBusySearchTime, FIELD_TIME ), DEFINE_FIELD( m_flEndBusyAt, FIELD_TIME ), DEFINE_FIELD( m_flBusySearchRange, FIELD_FLOAT ), DEFINE_FIELD( m_bInQueue, FIELD_BOOLEAN ), DEFINE_FIELD( m_iCurrentBusyAnim, FIELD_INTEGER ), DEFINE_FIELD( m_hActBusyGoal, FIELD_EHANDLE ), DEFINE_FIELD( m_bNeedToSetBounds, FIELD_BOOLEAN ), DEFINE_FIELD( m_hSeeEntity, FIELD_EHANDLE ), DEFINE_FIELD( m_fTimeLastSawSeeEntity, FIELD_TIME ), DEFINE_FIELD( m_bExitedBusyToDueLostSeeEntity, FIELD_BOOLEAN ), DEFINE_FIELD( m_bExitedBusyToDueSeeEnemy, FIELD_BOOLEAN ), DEFINE_FIELD( m_iNumConsecutivePathFailures, FIELD_INTEGER ), DEFINE_FIELD( m_bAutoFireWeapon, FIELD_BOOLEAN ), DEFINE_FIELD( m_flDeferUntil, FIELD_TIME ), DEFINE_FIELD( m_iNumEnemiesInSafeZone, FIELD_INTEGER ), END_DATADESC(); LINK_BEHAVIOR_TO_CLASSNAME( CAI_ActBusyBehavior ); enum { ACTBUSY_SIGHT_METHOD_FULL = 0, // LOS and Viewcone ACTBUSY_SIGHT_METHOD_LOS_ONLY, }; //============================================================================= //----------------------------------------------------------------------------- // Purpose: Gamesystem that parses the act busy anim file //----------------------------------------------------------------------------- class CActBusyAnimData : public CAutoGameSystem { public: CActBusyAnimData( void ) : CAutoGameSystem( "CActBusyAnimData" ) { } // Inherited from IAutoServerSystem virtual void LevelInitPostEntity( void ); virtual void LevelShutdownPostEntity( void ); // Read in the data from the anim data file void ParseAnimDataFile( void ); // Parse a keyvalues section into an act busy anim bool ParseActBusyFromKV( busyanim_t *pAnim, KeyValues *pSection ); // Purpose: Returns the index of the busyanim data for the specified activity or sequence int FindBusyAnim( Activity iActivity, const char *pSequence ); busyanim_t *GetBusyAnim( int iIndex ) { return &m_ActBusyAnims[iIndex]; } protected: CUtlVector m_ActBusyAnims; }; CActBusyAnimData g_ActBusyAnimDataSystem; //----------------------------------------------------------------------------- // Inherited from IAutoServerSystem //----------------------------------------------------------------------------- void CActBusyAnimData::LevelInitPostEntity( void ) { ParseAnimDataFile(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CActBusyAnimData::LevelShutdownPostEntity( void ) { m_ActBusyAnims.Purge(); } //----------------------------------------------------------------------------- // Clear out the stats + their history //----------------------------------------------------------------------------- void CActBusyAnimData::ParseAnimDataFile( void ) { KeyValues *pKVAnimData = new KeyValues( "ActBusyAnimDatafile" ); if ( pKVAnimData->LoadFromFile( filesystem, "scripts/actbusy.txt" ) ) { // Now try and parse out each act busy anim KeyValues *pKVAnim = pKVAnimData->GetFirstSubKey(); while ( pKVAnim ) { // Create a new anim and add it to our list int index = m_ActBusyAnims.AddToTail(); busyanim_t *pAnim = &m_ActBusyAnims[index]; if ( !ParseActBusyFromKV( pAnim, pKVAnim ) ) { m_ActBusyAnims.Remove( index ); } pKVAnim = pKVAnim->GetNextKey(); } } pKVAnimData->deleteThis(); } //----------------------------------------------------------------------------- // Purpose: Parse a keyvalues section into the prop //----------------------------------------------------------------------------- bool CActBusyAnimData::ParseActBusyFromKV( busyanim_t *pAnim, KeyValues *pSection ) { pAnim->iszName = AllocPooledString( pSection->GetName() ); // Activities pAnim->iActivities[BA_BUSY] = (Activity)CAI_BaseNPC::GetActivityID( pSection->GetString( "busy_anim", "ACT_INVALID" ) ); pAnim->iActivities[BA_ENTRY] = (Activity)CAI_BaseNPC::GetActivityID( pSection->GetString( "entry_anim", "ACT_INVALID" ) ); pAnim->iActivities[BA_EXIT] = (Activity)CAI_BaseNPC::GetActivityID( pSection->GetString( "exit_anim", "ACT_INVALID" ) ); // Sequences pAnim->iszSequences[BA_BUSY] = AllocPooledString( pSection->GetString( "busy_sequence", NULL ) ); pAnim->iszSequences[BA_ENTRY] = AllocPooledString( pSection->GetString( "entry_sequence", NULL ) ); pAnim->iszSequences[BA_EXIT] = AllocPooledString( pSection->GetString( "exit_sequence", NULL ) ); // Sounds pAnim->iszSounds[BA_BUSY] = AllocPooledString( pSection->GetString( "busy_sound", NULL ) ); pAnim->iszSounds[BA_ENTRY] = AllocPooledString( pSection->GetString( "entry_sound", NULL ) ); pAnim->iszSounds[BA_EXIT] = AllocPooledString( pSection->GetString( "exit_sound", NULL ) ); // Times pAnim->flMinTime = pSection->GetFloat( "min_time", 10.0 ); pAnim->flMaxTime = pSection->GetFloat( "max_time", 20.0 ); pAnim->bUseAutomovement = pSection->GetInt( "use_automovement", 0 ) != 0; const char *sInterrupt = pSection->GetString( "interrupts", "BA_INT_DANGER" ); if ( !strcmp( sInterrupt, "BA_INT_PLAYER" ) ) { pAnim->iBusyInterruptType = BA_INT_PLAYER; } else if ( !strcmp( sInterrupt, "BA_INT_DANGER" ) ) { pAnim->iBusyInterruptType = BA_INT_DANGER; } else if ( !strcmp( sInterrupt, "BA_INT_AMBUSH" ) ) { pAnim->iBusyInterruptType = BA_INT_AMBUSH; } else if ( !strcmp( sInterrupt, "BA_INT_COMBAT" ) ) { pAnim->iBusyInterruptType = BA_INT_COMBAT; } else if ( !strcmp( sInterrupt, "BA_INT_ZOMBIESLUMP" )) { pAnim->iBusyInterruptType = BA_INT_ZOMBIESLUMP; } else if ( !strcmp( sInterrupt, "BA_INT_SIEGE_DEFENSE" )) { pAnim->iBusyInterruptType = BA_INT_SIEGE_DEFENSE; } else { pAnim->iBusyInterruptType = BA_INT_NONE; } return true; } //----------------------------------------------------------------------------- // Purpose: Returns the busyanim data for the specified activity //----------------------------------------------------------------------------- int CActBusyAnimData::FindBusyAnim( Activity iActivity, const char *pSequence ) { int iCount = m_ActBusyAnims.Count(); for ( int i = 0; i < iCount; i++ ) { busyanim_t *pBusyAnim = &m_ActBusyAnims[i]; Assert( pBusyAnim ); if ( pSequence && pBusyAnim->iszName != NULL_STRING && !Q_stricmp( STRING(pBusyAnim->iszName), pSequence ) ) return i; if ( iActivity != ACT_INVALID && pBusyAnim->iActivities[BA_BUSY] == iActivity ) return i; } if ( pSequence ) { Warning("Specified '%s' as a busy anim name, and it's not in the act busy anim list.\n", pSequence ); } else if ( iActivity != ACT_INVALID ) { Warning("Tried to use Activity %d as a busy anim, and it's not in the act busy anim list.\n", iActivity ); } return -1; } //============================================================================================================= //============================================================================================================= //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CAI_ActBusyBehavior::CAI_ActBusyBehavior() { // Disabled by default. Enabled by map entity. m_bEnabled = false; m_bUseRenderBoundsForCollision = false; m_flDeferUntil = 0; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::Enable( CAI_ActBusyGoal *pGoal, float flRange, bool bVisibleOnly ) { NotifyBusyEnding(); if ( pGoal ) { m_hActBusyGoal = pGoal; } m_bEnabled = true; m_bBusy = false; m_bMovingToBusy = false; m_bNeedsToPlayExitAnim = false; m_bLeaving = false; m_flNextBusySearchTime = gpGlobals->curtime + ai_actbusy_search_time.GetFloat(); m_flEndBusyAt = 0; m_bVisibleOnly = bVisibleOnly; m_bInQueue = dynamic_cast(m_hActBusyGoal.Get()) != NULL; m_ForcedActivity = ACT_INVALID; m_hSeeEntity = NULL; m_bExitedBusyToDueLostSeeEntity = false; m_bExitedBusyToDueSeeEnemy = false; m_iNumConsecutivePathFailures = 0; SetBusySearchRange( flRange ); if ( ai_debug_actbusy.GetInt() == 4 ) { Msg("ACTBUSY: behavior enabled on NPC %s (%s)\n", GetOuter()->GetClassname(), GetOuter()->GetDebugName() ); } if( IsCombatActBusy() ) { CollectSafeZoneVolumes( pGoal ); } // Robin: Due to ai goal entities delaying their EnableGoal call on each // of their target Actors, NPCs that are spawned with active actbusies // will have their SelectSchedule() called before their behavior has been // enabled. To fix this, if we're enabled while in a schedule that can be // overridden, immediately act busy. if ( IsCurScheduleOverridable() ) { // Force search time to be now. m_flNextBusySearchTime = gpGlobals->curtime; GetOuter()->ClearSchedule( "Enabling act busy" ); } ClearCondition( COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::OnRestore() { if ( m_bEnabled && m_hActBusyGoal != NULL && IsCombatActBusy() ) { CollectSafeZoneVolumes( m_hActBusyGoal ); } BaseClass::OnRestore(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::SetBusySearchRange( float flRange ) { m_flBusySearchRange = flRange; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::Disable( void ) { if ( ai_debug_actbusy.GetInt() == 4 ) { Msg("ACTBUSY: behavior disabled on NPC %s (%s)\n", GetOuter()->GetClassname(), GetOuter()->GetDebugName() ); } if ( m_bEnabled ) { SetCondition( COND_PROVOKED ); } StopBusying(); m_bEnabled = false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::ForceActBusy( CAI_ActBusyGoal *pGoal, CAI_Hint *pHintNode, float flMaxTime, bool bVisibleOnly, bool bTeleportToBusy, bool bUseNearestBusy, CBaseEntity *pSeeEntity, Activity activity ) { Assert( !m_bLeaving ); if ( m_bNeedsToPlayExitAnim ) { // If we hit this, the mapmaker's told this NPC to actbusy somewhere while it's still in an actbusy. // Right now, we don't support this. We could support it with a bit of work. if ( HasAnimForActBusy( m_iCurrentBusyAnim, BA_EXIT ) ) { Warning("ACTBUSY: %s(%s) was told to actbusy while inside an actbusy that needs to exit first. IGNORING.\n", GetOuter()->GetDebugName(), GetOuter()->GetClassname() ); return; } } if ( ai_debug_actbusy.GetInt() == 4 ) { Msg("ACTBUSY: ForceActBusy on NPC %s (%s): ", GetOuter()->GetClassname(), GetOuter()->GetDebugName() ); if ( pHintNode ) { Msg("Hintnode %s", pHintNode->GetDebugName()); } else { Msg("No Hintnode specified"); } Msg("\n"); } Enable( pGoal, m_flBusySearchRange, bVisibleOnly ); m_bForceActBusy = true; m_flForcedMaxTime = flMaxTime; m_bTeleportToBusy = bTeleportToBusy; m_bUseNearestBusy = bUseNearestBusy; m_ForcedActivity = activity; m_hSeeEntity = pSeeEntity; if ( pHintNode ) { if ( GetHintNode() && GetHintNode() != pHintNode ) { GetHintNode()->Unlock(); } if ( pHintNode->Lock( GetOuter() ) ) { SetHintNode( pHintNode ); } } SetCondition( COND_PROVOKED ); } //----------------------------------------------------------------------------- // Purpose: Force the NPC to find an exit node and leave //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::ForceActBusyLeave( bool bVisibleOnly ) { if ( ai_debug_actbusy.GetInt() == 4 ) { Msg("ACTBUSY: ForceActBusyLeave on NPC %s (%s)\n", GetOuter()->GetClassname(), GetOuter()->GetDebugName() ); } Enable( NULL, m_flBusySearchRange, bVisibleOnly ); m_bForceActBusy = true; m_bLeaving = true; m_ForcedActivity = ACT_INVALID; m_hSeeEntity = NULL; SetCondition( COND_PROVOKED ); } //----------------------------------------------------------------------------- // Purpose: Break the NPC out of the current busy state, but don't disable busying //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::StopBusying( void ) { if ( !GetOuter() ) return; // Make sure we turn this off unconditionally! m_bAutoFireWeapon = false; if ( ai_debug_actbusy.GetInt() == 4 ) { Msg("ACTBUSY: StopBusying on NPC %s (%s)\n", GetOuter()->GetClassname(), GetOuter()->GetDebugName() ); } if ( m_bBusy || m_bMovingToBusy ) { SetCondition( COND_PROVOKED ); } m_flEndBusyAt = gpGlobals->curtime; m_bForceActBusy = false; m_bTeleportToBusy = false; m_bUseNearestBusy = false; m_bLeaving = false; m_bMovingToBusy = false; m_ForcedActivity = ACT_INVALID; m_hSeeEntity = NULL; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::IsStopBusying() { return IsCurSchedule(SCHED_ACTBUSY_STOP_BUSYING); } //----------------------------------------------------------------------------- // Purpose: Find a general purpose, suitable Hint Node for me to Act Busy. //----------------------------------------------------------------------------- CAI_Hint *CAI_ActBusyBehavior::FindActBusyHintNode() { Assert( !IsCombatActBusy() ); int iBits = bits_HINT_NODE_USE_GROUP; if ( m_bVisibleOnly ) { iBits |= bits_HINT_NODE_VISIBLE; } if ( ai_debug_actbusy.GetInt() == 3 && GetOuter()->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT ) { iBits |= bits_HINT_NODE_REPORT_FAILURES; } if ( m_bUseNearestBusy ) { iBits |= bits_HINT_NODE_NEAREST; } else { iBits |= bits_HINT_NODE_RANDOM; } CAI_Hint *pNode = CAI_HintManager::FindHint( GetOuter(), HINT_WORLD_WORK_POSITION, iBits, m_flBusySearchRange ); return pNode; } //----------------------------------------------------------------------------- // Purpose: Find a node for me to combat act busy. // // Right now, all of this work assumes the actbusier is a player ally and // wants to fight near the player a'la Alyx in ep2_outland_10. //----------------------------------------------------------------------------- CAI_Hint *CAI_ActBusyBehavior::FindCombatActBusyHintNode() { Assert( IsCombatActBusy() ); CBasePlayer *pPlayer = AI_GetSinglePlayer(); if( !pPlayer ) return NULL; CHintCriteria criteria; // Ok, find a hint node THAT: // -Is in my hint group // -Is Visible (if specified by designer) // -Is Closest to me (if specified by designer) // -The player can see // -Is within the accepted max dist from player int iBits = bits_HINT_NODE_USE_GROUP; if ( m_bVisibleOnly ) iBits |= bits_HINT_NODE_VISIBLE; if ( ai_debug_actbusy.GetInt() == 3 && GetOuter()->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT ) iBits |= bits_HINT_NODE_REPORT_FAILURES; if ( m_bUseNearestBusy ) iBits |= bits_HINT_NODE_NEAREST; else iBits |= bits_HINT_NODE_RANDOM; iBits |= bits_HAS_EYEPOSITION_LOS_TO_PLAYER; criteria.AddHintType( HINT_WORLD_WORK_POSITION ); criteria.SetFlag( iBits ); criteria.AddIncludePosition( pPlayer->GetAbsOrigin(), ACTBUSY_COMBAT_PLAYER_MAX_DIST ); CAI_Hint *pNode = CAI_HintManager::FindHint( GetOuter(), criteria ); return pNode; } //----------------------------------------------------------------------------- // Purpose: Find a suitable combat actbusy node to teleport to. That is, find // one that the player is not going to see me appear at. //----------------------------------------------------------------------------- CAI_Hint *CAI_ActBusyBehavior::FindCombatActBusyTeleportHintNode() { Assert( IsCombatActBusy() ); CBasePlayer *pPlayer = AI_GetSinglePlayer(); if( !pPlayer ) return NULL; CHintCriteria criteria; // Ok, find a hint node THAT: // -Is in my hint group // -The player CAN NOT see so that they don't see me teleport int iBits = bits_HINT_NODE_USE_GROUP; if ( ai_debug_actbusy.GetInt() == 3 && GetOuter()->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT ) iBits |= bits_HINT_NODE_REPORT_FAILURES; iBits |= bits_HINT_NODE_RANDOM; iBits |= bits_HINT_NODE_NOT_VISIBLE_TO_PLAYER; criteria.AddHintType( HINT_WORLD_WORK_POSITION ); criteria.SetFlag( iBits ); criteria.AddIncludePosition( pPlayer->GetAbsOrigin(), ACTBUSY_COMBAT_PLAYER_MAX_DIST * 1.1f ); CAI_Hint *pNode = CAI_HintManager::FindHint( GetOuter(), criteria ); return pNode; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::FValidateHintType( CAI_Hint *pHint ) { if ( pHint->HintType() != HINT_WORLD_WORK_POSITION && pHint->HintType() != HINT_NPC_EXIT_POINT ) return false; // If the node doesn't want to be teleported to, we need to check for clear const char *pSequenceOrActivity = STRING(pHint->HintActivityName()); const char *cSpace = strchr( pSequenceOrActivity, ' ' ); if ( cSpace ) { if ( !Q_strncmp( cSpace+1, "teleport", 8 ) ) { // Node is a teleport node, so it's good return true; } } // Check for clearance trace_t tr; AI_TraceHull( pHint->GetAbsOrigin(), pHint->GetAbsOrigin(), GetOuter()->WorldAlignMins(), GetOuter()->WorldAlignMaxs(), MASK_SOLID, GetOuter(), COLLISION_GROUP_NONE, &tr ); if ( tr.fraction == 1.0 ) return true; // Report failures if ( ai_debug_actbusy.GetInt() == 3 && GetOuter()->m_debugOverlays & OVERLAY_NPC_SELECTED_BIT ) { NDebugOverlay::Text( pHint->GetAbsOrigin(), "Node isn't clear.", false, 60 ); NDebugOverlay::Box( pHint->GetAbsOrigin(), GetOuter()->WorldAlignMins(), GetOuter()->WorldAlignMaxs(), 255,0,0, 8, 2.0 ); } return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::CanSelectSchedule( void ) { // Always active when we're busy if ( m_bBusy || m_bForceActBusy || m_bNeedsToPlayExitAnim ) return true; if ( !m_bEnabled ) return false; if ( m_flDeferUntil > gpGlobals->curtime ) return false; if ( CountEnemiesInSafeZone() > 0 ) { // I have enemies left in the safe zone. Actbusy isn't appropriate. // I should be off fighting them. return false; } if ( !IsCurScheduleOverridable() ) return false; // Don't select actbusy if we're not going to search for a node anyway return (m_flNextBusySearchTime < gpGlobals->curtime); } //----------------------------------------------------------------------------- // Purpose: Return true if the current schedule is one that ActBusy is allowed to override //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::IsCurScheduleOverridable( void ) { if( IsCombatActBusy() ) { // The whole point of a combat actbusy is that it can run in any state (including combat) // the only exception is SCRIPT (sjb) return (GetOuter()->GetState() != NPC_STATE_SCRIPT); } // Act busies are not valid inside of a vehicle if ( GetOuter()->IsInAVehicle() ) return false; // Only if we're about to idle (or SCHED_NONE to catch newly spawned guys) return ( IsCurSchedule( SCHED_IDLE_STAND ) || IsCurSchedule( SCHED_NONE ) ); } //----------------------------------------------------------------------------- // Purpose: // Input : *pSound - // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::ShouldIgnoreSound( CSound *pSound ) { // If we're busy in an actbusy anim that's an ambush, stay mum as long as we can if ( m_bBusy ) { busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim ); if( pBusyAnim && pBusyAnim->iBusyInterruptType == BA_INT_ZOMBIESLUMP ) { // Slumped zombies are deaf. return true; } if ( pBusyAnim && ( pBusyAnim->iBusyInterruptType == BA_INT_AMBUSH ) || ( pBusyAnim->iBusyInterruptType == BA_INT_COMBAT ) ) { /* // Robin: First version ignored sounds in front of the NPC. Vector vecToSound = (pSound->GetSoundReactOrigin() - GetAbsOrigin()); vecToSound.z = 0; VectorNormalize( vecToSound ); Vector facingDir = GetOuter()->EyeDirection2D(); if ( DotProduct( vecToSound, facingDir ) > 0 ) return true; */ // Ignore sounds that aren't visible if ( !GetOuter()->FVisible( pSound->GetSoundReactOrigin() ) ) return true; } } return BaseClass::ShouldIgnoreSound( pSound ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::OnFriendDamaged( CBaseCombatCharacter *pSquadmate, CBaseEntity *pAttacker ) { if( IsCombatActBusy() && pSquadmate->IsPlayer() && IsInSafeZone( pAttacker ) ) { SetCondition( COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE ); // Break the actbusy, if we're running it. m_flDeferUntil = gpGlobals->curtime + 4.0f; // Stop actbusying and go deal with that enemy!! } BaseClass::OnFriendDamaged( pSquadmate, pAttacker ); } //----------------------------------------------------------------------------- // Purpose: Count the number of enemies of mine that are inside my safe zone // volume. // // NOTE: We keep this count to prevent the NPC re-entering combat // actbusy whilst too many enemies are present in the safe zone. // This count does not automatically alert the NPC that there are // enemies in the safe zone. // You must set COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE to let // the NPC know. //----------------------------------------------------------------------------- int CAI_ActBusyBehavior::CountEnemiesInSafeZone() { if( !IsCombatActBusy() ) { return 0; } // Grovel the AI list and count the enemies in the zone. By enemies, I mean // anyone that I would fight if I saw. CAI_BaseNPC ** ppAIs = g_AI_Manager.AccessAIs(); int nAIs = g_AI_Manager.NumAIs(); int count = 0; for ( int i = 0; i < nAIs; i++ ) { if( GetOuter()->IRelationType(ppAIs[i]) < D_LI ) { if( IsInSafeZone(ppAIs[i]) ) { count++; } } } return count; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CAI_ActBusyBehavior::OnTakeDamage_Alive( const CTakeDamageInfo &info ) { if( IsCombatActBusy() && info.GetAttacker() && IsInSafeZone( info.GetAttacker() ) ) { SetCondition( COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE ); // Break the actbusy, if we're running it. m_flDeferUntil = gpGlobals->curtime + 4.0f; // Stop actbusying and go deal with that enemy!! } return BaseClass::OnTakeDamage_Alive( info ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::GatherConditions( void ) { // Clear this condition before we call up, since the look sensing code will run and // set this condition if it is relevant. if( !IsCurSchedule(SCHED_ACTBUSY_BUSY, false) ) { // Only clear this condition when we aren't busying. We want it to be sticky // during that time so that schedule selection works properly (sjb) ClearCondition( COND_ACTBUSY_ENEMY_TOO_CLOSE ); } BaseClass::GatherConditions(); bool bCheckLOS = true; bool bCheckFOV = true; if( m_hActBusyGoal && m_hActBusyGoal->m_iSightMethod == ACTBUSY_SIGHT_METHOD_LOS_ONLY ) { bCheckFOV = false; } // If we have a see entity, make sure we can still see it if ( m_hSeeEntity && m_bBusy ) { if ( (!bCheckFOV||GetOuter()->FInViewCone(m_hSeeEntity)) && GetOuter()->QuerySeeEntity(m_hSeeEntity) && (!bCheckLOS||GetOuter()->FVisible(m_hSeeEntity)) ) { m_fTimeLastSawSeeEntity = gpGlobals->curtime; ClearCondition( COND_ACTBUSY_LOST_SEE_ENTITY ); } else if( m_hActBusyGoal ) { float fDelta = gpGlobals->curtime - m_fTimeLastSawSeeEntity; if( fDelta >= m_hActBusyGoal->m_flSeeEntityTimeout ) { SetCondition( COND_ACTBUSY_LOST_SEE_ENTITY ); m_hActBusyGoal->NPCLostSeeEntity( GetOuter() ); if( IsCombatActBusy() && (GetOuter()->Classify() == CLASS_PLAYER_ALLY_VITAL && m_hSeeEntity->IsPlayer()) ) { // Defer any actbusying for several seconds. This serves as a heuristic for waiting // for the player to settle after moving out of the room. This helps Alyx pick a more // pertinent Actbusy near the player's new location. m_flDeferUntil = gpGlobals->curtime + 4.0f; } } } } else { ClearCondition( COND_ACTBUSY_LOST_SEE_ENTITY ); } // If we're busy, ignore sounds depending on our actbusy break rules if ( m_bBusy ) { busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim ); if ( pBusyAnim ) { switch( pBusyAnim->iBusyInterruptType ) { case BA_INT_DANGER: break; case BA_INT_AMBUSH: break; case BA_INT_ZOMBIESLUMP: { ClearCondition( COND_HEAR_PLAYER ); ClearCondition( COND_SEE_ENEMY ); ClearCondition( COND_NEW_ENEMY ); CBasePlayer *pPlayer = UTIL_PlayerByIndex(1); if( pPlayer ) { float flDist = pPlayer->GetAbsOrigin().DistTo( GetAbsOrigin() ); if( flDist <= 60 ) { StopBusying(); } } } break; case BA_INT_COMBAT: // Ignore the player unless he shoots at us ClearCondition( COND_HEAR_PLAYER ); ClearCondition( COND_SEE_ENEMY ); ClearCondition( COND_NEW_ENEMY ); break; case BA_INT_PLAYER: // Clear all but player. ClearCondition( COND_HEAR_DANGER ); ClearCondition( COND_HEAR_COMBAT ); ClearCondition( COND_HEAR_WORLD ); ClearCondition( COND_HEAR_BULLET_IMPACT ); break; case BA_INT_SIEGE_DEFENSE: ClearCondition( COND_HEAR_PLAYER ); ClearCondition( COND_SEE_ENEMY ); ClearCondition( COND_NEW_ENEMY ); ClearCondition( COND_HEAR_COMBAT ); ClearCondition( COND_HEAR_WORLD ); ClearCondition( COND_HEAR_BULLET_IMPACT ); break; case BA_INT_NONE: // Clear all ClearCondition( COND_HEAR_DANGER ); ClearCondition( COND_HEAR_COMBAT ); ClearCondition( COND_HEAR_WORLD ); ClearCondition( COND_HEAR_BULLET_IMPACT ); ClearCondition( COND_HEAR_PLAYER ); break; default: break; } } } if( m_bAutoFireWeapon && random->RandomInt(0, 5) <= 3 ) { CBaseCombatWeapon *pWeapon = GetOuter()->GetActiveWeapon(); if( pWeapon ) { pWeapon->Operator_ForceNPCFire( GetOuter(), false ); } } if( ai_debug_actbusy.GetInt() == 5 ) { // Visualize them there Actbusy safe volumes for( int i = 0 ; i < m_SafeZones.Count() ; i++ ) { busysafezone_t *pSafeZone = &m_SafeZones[i]; Vector vecBoxOrigin = (pSafeZone->vecMins + pSafeZone->vecMaxs) * 0.5f; Vector vecBoxMins = vecBoxOrigin - pSafeZone->vecMins; Vector vecBoxMaxs = vecBoxOrigin - pSafeZone->vecMaxs; NDebugOverlay::Box( vecBoxOrigin, vecBoxMins, vecBoxMaxs, 255, 0, 255, 64, 0.2f ); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::EndScheduleSelection( void ) { NotifyBusyEnding(); CheckAndCleanupOnExit(); } //----------------------------------------------------------------------------- // Purpose: // Input : nActivity - //----------------------------------------------------------------------------- Activity CAI_ActBusyBehavior::NPC_TranslateActivity( Activity nActivity ) { // Find out what the base class wants to do with the activity Activity nNewActivity = BaseClass::NPC_TranslateActivity( nActivity ); if( nActivity == ACT_RUN ) { // FIXME: Forcing STIMULATED here is illegal if the entity doesn't support it as an activity CAI_PlayerAlly *pAlly = dynamic_cast(GetOuter()); if ( pAlly ) return ACT_RUN_STIMULATED; } // Else stay with the base class' decision. return nNewActivity; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::HandleAnimEvent( animevent_t *pEvent ) { int nEvent = pEvent->Event(); if( nEvent == AE_ACTBUSY_WEAPON_FIRE_ON ) { m_bAutoFireWeapon = true; return; } else if( nEvent == AE_ACTBUSY_WEAPON_FIRE_OFF ) { m_bAutoFireWeapon = false; return; } return BaseClass::HandleAnimEvent( pEvent ); } //----------------------------------------------------------------------------- // Purpose: Actbusy's ending, ensure we haven't left NPC in broken state. //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::CheckAndCleanupOnExit( void ) { if ( m_bNeedsToPlayExitAnim && !GetOuter()->IsMarkedForDeletion() && GetOuter()->IsAlive() ) { Warning("NPC %s(%s) left actbusy without playing exit anim.\n", GetOuter()->GetDebugName(), GetOuter()->GetClassname() ); m_bNeedsToPlayExitAnim = false; } GetOuter()->RemoveFlag( FL_FLY ); // If we're supposed to use render bounds while inside the busy anim, restore normal now if ( m_bUseRenderBoundsForCollision ) { GetOuter()->SetHullSizeNormal( true ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::BuildScheduleTestBits( void ) { BaseClass::BuildScheduleTestBits(); // When going to an actbusy, we can't be interrupted during the entry anim if ( IsCurSchedule(SCHED_ACTBUSY_START_BUSYING) ) { if ( GetOuter()->GetTask()->iTask == TASK_ACTBUSY_PLAY_ENTRY ) return; GetOuter()->SetCustomInterruptCondition( COND_PROVOKED ); if( IsCombatActBusy() ) { GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal(COND_ACTBUSY_ENEMY_TOO_CLOSE) ); } } // If we're in a queue, or leaving, we have no extra conditions if ( m_bInQueue || IsCurSchedule( SCHED_ACTBUSY_LEAVE ) ) return; // If we're not busy, or we're exiting a busy, we have no extra conditions if ( !m_bBusy || IsCurSchedule( SCHED_ACTBUSY_STOP_BUSYING ) ) return; busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim ); if ( pBusyAnim ) { switch( pBusyAnim->iBusyInterruptType ) { case BA_INT_ZOMBIESLUMP: { GetOuter()->SetCustomInterruptCondition( COND_LIGHT_DAMAGE ); GetOuter()->SetCustomInterruptCondition( COND_HEAVY_DAMAGE ); } break; case BA_INT_SIEGE_DEFENSE: { GetOuter()->SetCustomInterruptCondition( COND_HEAR_DANGER ); GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal(COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE) ); GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal(COND_ACTBUSY_ENEMY_TOO_CLOSE) ); } break; case BA_INT_AMBUSH: case BA_INT_DANGER: { GetOuter()->SetCustomInterruptCondition( COND_LIGHT_DAMAGE ); GetOuter()->SetCustomInterruptCondition( COND_HEAVY_DAMAGE ); GetOuter()->SetCustomInterruptCondition( COND_HEAR_DANGER ); GetOuter()->SetCustomInterruptCondition( COND_HEAR_COMBAT ); GetOuter()->SetCustomInterruptCondition( COND_HEAR_BULLET_IMPACT ); GetOuter()->SetCustomInterruptCondition( COND_NEW_ENEMY ); GetOuter()->SetCustomInterruptCondition( COND_SEE_ENEMY ); GetOuter()->SetCustomInterruptCondition( COND_PLAYER_ADDED_TO_SQUAD ); GetOuter()->SetCustomInterruptCondition( COND_RECEIVED_ORDERS ); break; } case BA_INT_PLAYER: { GetOuter()->SetCustomInterruptCondition( COND_LIGHT_DAMAGE ); GetOuter()->SetCustomInterruptCondition( COND_HEAVY_DAMAGE ); GetOuter()->SetCustomInterruptCondition( COND_HEAR_DANGER ); GetOuter()->SetCustomInterruptCondition( COND_HEAR_COMBAT ); GetOuter()->SetCustomInterruptCondition( COND_HEAR_BULLET_IMPACT ); GetOuter()->SetCustomInterruptCondition( COND_NEW_ENEMY ); GetOuter()->SetCustomInterruptCondition( COND_PLAYER_ADDED_TO_SQUAD ); GetOuter()->SetCustomInterruptCondition( COND_RECEIVED_ORDERS ); // The player can interrupt us GetOuter()->SetCustomInterruptCondition( COND_SEE_PLAYER ); break; } case BA_INT_COMBAT: { GetOuter()->SetCustomInterruptCondition( COND_LIGHT_DAMAGE ); GetOuter()->SetCustomInterruptCondition( COND_HEAVY_DAMAGE ); GetOuter()->SetCustomInterruptCondition( COND_HEAR_DANGER ); break; } case BA_INT_NONE: break; default: break; } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CAI_ActBusyBehavior::SelectScheduleForLeaving( void ) { // Are we already near an exit node? if ( GetHintNode() ) { if ( GetHintNode()->HintType() == HINT_NPC_EXIT_POINT ) { // Are we near it? If so, we're done. If not, move to it. if ( UTIL_DistApprox( GetHintNode()->GetAbsOrigin(), GetAbsOrigin() ) < 64 ) { if ( !GetOuter()->IsMarkedForDeletion() ) { CBaseEntity *pOwner = GetOuter()->GetOwnerEntity(); if ( pOwner ) { pOwner->DeathNotice( GetOuter() ); GetOuter()->SetOwnerEntity( NULL ); } GetOuter()->SetThink( &CBaseEntity::SUB_Remove); //SUB_Remove) ; //GetOuter()->SUB_Remove ); GetOuter()->SetNextThink( gpGlobals->curtime + 0.1 ); if ( m_hActBusyGoal ) { m_hActBusyGoal->NPCLeft( GetOuter() ); } } return SCHED_IDLE_STAND; } return SCHED_ACTBUSY_LEAVE; } else { // Clear the node, it's no use to us GetHintNode()->NPCStoppedUsing( GetOuter() ); GetHintNode()->Unlock(); SetHintNode( NULL ); } } // Find an exit node CHintCriteria hintCriteria; hintCriteria.SetHintType( HINT_NPC_EXIT_POINT ); hintCriteria.SetFlag( bits_HINT_NODE_RANDOM | bits_HINT_NODE_CLEAR | bits_HINT_NODE_USE_GROUP ); CAI_Hint *pNode = CAI_HintManager::FindHintRandom( GetOuter(), GetOuter()->GetAbsOrigin(), hintCriteria ); if ( pNode ) { SetHintNode( pNode ); return SCHED_ACTBUSY_LEAVE; } // We've been told to leave, but we can't find an exit node. What to do? return SCHED_IDLE_STAND; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CAI_ActBusyBehavior::SelectScheduleWhileNotBusy( int iBase ) { // Randomly act busy (unless we're being forced, in which case we should search immediately) if ( m_bForceActBusy || m_flNextBusySearchTime < gpGlobals->curtime ) { // If we're being forced, think again quickly if ( m_bForceActBusy || IsCombatActBusy() ) { m_flNextBusySearchTime = gpGlobals->curtime + 2.0; } else { m_flNextBusySearchTime = gpGlobals->curtime + RandomFloat(ai_actbusy_search_time.GetFloat(), ai_actbusy_search_time.GetFloat()*2); } // We may already have a node bool bForceTeleport = false; CAI_Hint *pNode = GetHintNode(); if ( !pNode ) { if( IsCombatActBusy() ) { if ( m_hActBusyGoal->IsCombatActBusyTeleportAllowed() && m_iNumConsecutivePathFailures >= 2 && !AI_GetSinglePlayer()->FInViewCone(GetOuter()) ) { // Looks like I've tried several times to find a path to a valid hint node and // haven't been able to. This means I'm on a patch of node graph that simply // does not connect to any hint nodes that match my criteria. So try to find // a node that's safe to teleport to. (sjb) ep2_outland_10 (Alyx) // (Also, I must not be in the player's viewcone) pNode = FindCombatActBusyTeleportHintNode(); bForceTeleport = true; } else { pNode = FindCombatActBusyHintNode(); } } else { pNode = FindActBusyHintNode(); } } if ( pNode ) { // Ensure we've got a sequence for the node const char *pSequenceOrActivity = STRING(pNode->HintActivityName()); Activity iNodeActivity; int iBusyAnim; // See if the node specifies that we should teleport to it const char *cSpace = strchr( pSequenceOrActivity, ' ' ); if ( cSpace ) { if ( !Q_strncmp( cSpace+1, "teleport", 8 ) ) { m_bTeleportToBusy = true; } char sActOrSeqName[512]; Q_strncpy( sActOrSeqName, pSequenceOrActivity, (cSpace-pSequenceOrActivity)+1 ); iNodeActivity = (Activity)CAI_BaseNPC::GetActivityID( sActOrSeqName ); iBusyAnim = g_ActBusyAnimDataSystem.FindBusyAnim( iNodeActivity, sActOrSeqName ); } else { iNodeActivity = (Activity)CAI_BaseNPC::GetActivityID( pSequenceOrActivity ); iBusyAnim = g_ActBusyAnimDataSystem.FindBusyAnim( iNodeActivity, pSequenceOrActivity ); } // Does this NPC have the activity or sequence for this node? if ( HasAnimForActBusy( iBusyAnim, BA_BUSY ) ) { if ( HasCondition(COND_ACTBUSY_LOST_SEE_ENTITY) ) { // We've lost our see entity, which means we can't continue. if ( m_bForceActBusy ) { // We were being told to act busy, which we can't do now that we've lost the see entity. // Abort, and assume that the mapmaker will make us retry. StopBusying(); } return iBase; } m_iCurrentBusyAnim = iBusyAnim; if ( m_iCurrentBusyAnim == -1 ) return iBase; if ( ai_debug_actbusy.GetInt() == 4 ) { Msg("ACTBUSY: NPC %s (%s) found Actbusy node %s \n", GetOuter()->GetClassname(), GetOuter()->GetDebugName(), pNode->GetDebugName() ); } if ( GetHintNode() ) { GetHintNode()->Unlock(); } SetHintNode( pNode ); if ( GetHintNode() && GetHintNode()->Lock( GetOuter() ) ) { if ( ai_debug_actbusy.GetInt() == 2 ) { // Show which actbusy we're moving towards NDebugOverlay::Line( GetOuter()->WorldSpaceCenter(), pNode->GetAbsOrigin(), 0, 255, 0, true, 5.0 ); NDebugOverlay::Box( pNode->GetAbsOrigin(), GetOuter()->WorldAlignMins(), GetOuter()->WorldAlignMaxs(), 0, 255, 0, 64, 5.0 ); } // Let our act busy know we're moving to a node if ( m_hActBusyGoal ) { m_hActBusyGoal->NPCMovingToBusy( GetOuter() ); } m_bMovingToBusy = true; if( m_hActBusyGoal && m_hActBusyGoal->m_iszSeeEntityName != NULL_STRING ) { // Set the see entity Handle if we have one. m_hSeeEntity.Set( gEntList.FindEntityByName(NULL, m_hActBusyGoal->m_iszSeeEntityName) ); } // At this point we know we're starting. ClearCondition( COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE ); // If we're supposed to teleport, do that instead if ( m_bTeleportToBusy ) { return SCHED_ACTBUSY_TELEPORT_TO_BUSY; } else if( bForceTeleport ) { // We found a place to go, so teleport there and forget that we ever had trouble. m_iNumConsecutivePathFailures = 0; return SCHED_ACTBUSY_TELEPORT_TO_BUSY; } return SCHED_ACTBUSY_START_BUSYING; } } } else { // WE DIDN'T FIND A NODE! if( IsCombatActBusy() ) { // Don't try again right away, not enough state will have changed. // Just go do something useful for a few seconds. m_flNextBusySearchTime = gpGlobals->curtime + 10.0; } } } return SCHED_NONE; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CAI_ActBusyBehavior::SelectScheduleWhileBusy( void ) { // Are we supposed to stop on our current actbusy, but stay in the actbusy state? if ( !ActBusyNodeStillActive() || (m_flEndBusyAt && gpGlobals->curtime >= m_flEndBusyAt) ) { if ( ai_debug_actbusy.GetInt() == 4 ) { Msg("ACTBUSY: NPC %s (%s) ending actbusy.\n", GetOuter()->GetClassname(), GetOuter()->GetDebugName() ); } StopBusying(); return SCHED_ACTBUSY_STOP_BUSYING; } if( IsCombatActBusy() && (HasCondition(COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE) || HasCondition(COND_ACTBUSY_ENEMY_TOO_CLOSE)) ) { return SCHED_ACTBUSY_STOP_BUSYING; } return SCHED_ACTBUSY_BUSY; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CAI_ActBusyBehavior::SelectSchedule() { int iBase = BaseClass::SelectSchedule(); // Only do something if the base ai doesn't want to do anything if ( !IsCombatActBusy() && !m_bForceActBusy && iBase != SCHED_IDLE_STAND ) { // If we're busy, we need to get out of it first if ( m_bBusy ) return SCHED_ACTBUSY_STOP_BUSYING; CheckAndCleanupOnExit(); return iBase; } // If we're supposed to be leaving, find a leave node and exit if ( m_bLeaving ) return SelectScheduleForLeaving(); // NPCs should not be busy if the actbusy behaviour has been disabled, or if they've received player squad commands bool bShouldNotBeBusy = (!m_bEnabled || HasCondition( COND_PLAYER_ADDED_TO_SQUAD ) || HasCondition( COND_RECEIVED_ORDERS ) ); if ( bShouldNotBeBusy ) { if ( !GetOuter()->IsMarkedForDeletion() && GetOuter()->IsAlive() ) return SCHED_ACTBUSY_STOP_BUSYING; } else { if ( m_bBusy ) return SelectScheduleWhileBusy(); // I'm not busy, and I'm supposed to be int schedule = SelectScheduleWhileNotBusy( iBase ); if ( schedule != SCHED_NONE ) return schedule; } CheckAndCleanupOnExit(); return iBase; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::ActBusyNodeStillActive( void ) { if ( !GetHintNode() ) return false; return ( GetHintNode()->IsDisabled() == false ); } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::IsInterruptable( void ) { if ( IsActive() ) return false; return BaseClass::IsInterruptable(); } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::CanFlinch( void ) { if ( m_bNeedsToPlayExitAnim ) return false; return BaseClass::CanFlinch(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::CanRunAScriptedNPCInteraction( bool bForced ) { // Prevent interactions during actbusy modes if ( IsActive() ) return false; return BaseClass::CanRunAScriptedNPCInteraction( bForced ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::OnScheduleChange() { if( IsCurSchedule(SCHED_ACTBUSY_BUSY, false) ) { if( HasCondition(COND_SEE_ENEMY) ) { m_bExitedBusyToDueSeeEnemy = true; } if( HasCondition(COND_ACTBUSY_LOST_SEE_ENTITY) ) { m_bExitedBusyToDueLostSeeEntity = true; } } BaseClass::OnScheduleChange(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::QueryHearSound( CSound *pSound ) { // Ignore friendly created combat sounds while in an actbusy. // Fixes friendly NPCs going in & out of actbusies when the // player fires shots at their feet. if ( pSound->IsSoundType( SOUND_COMBAT ) || pSound->IsSoundType( SOUND_BULLET_IMPACT ) ) { if ( GetOuter()->IRelationType( pSound->m_hOwner ) == D_LI ) return false; } return BaseClass::QueryHearSound( pSound ); } //----------------------------------------------------------------------------- // Purpose: Because none of the startbusy schedules break on COND_NEW_ENEMY // we have to do this distance check against all enemy NPCs we // see as we're traveling to an ACTBUSY node //----------------------------------------------------------------------------- #define ACTBUSY_ENEMY_TOO_CLOSE_DIST_SQR Square(240) // 20 feet void CAI_ActBusyBehavior::OnSeeEntity( CBaseEntity *pEntity ) { BaseClass::OnSeeEntity( pEntity ); if( IsCombatActBusy() && GetOuter()->IRelationType(pEntity) < D_LI ) { if( pEntity->GetAbsOrigin().DistToSqr( GetAbsOrigin() ) <= ACTBUSY_ENEMY_TOO_CLOSE_DIST_SQR ) { SetCondition( COND_ACTBUSY_ENEMY_TOO_CLOSE ); } } } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::ShouldPlayerAvoid( void ) { if( IsCombatActBusy() ) { // Alyx is only allowed to push if she's getting into or out of an actbusy // animation. She isn't allowed to shove you around while she's running around if ( IsCurSchedule(SCHED_ACTBUSY_START_BUSYING) ) { if ( GetCurTask() && GetCurTask()->iTask == TASK_ACTBUSY_PLAY_ENTRY ) return true; } else if ( IsCurSchedule(SCHED_ACTBUSY_STOP_BUSYING) ) { if ( GetCurTask() && GetCurTask()->iTask == TASK_ACTBUSY_PLAY_EXIT ) return true; } } else { if ( IsCurSchedule ( SCHED_ACTBUSY_START_BUSYING ) ) { if ( GetCurTask() && GetCurTask()->iTask == TASK_WAIT_FOR_MOVEMENT || GetOuter()->GetTask()->iTask == TASK_ACTBUSY_PLAY_ENTRY ) return true; } else if ( IsCurSchedule(SCHED_ACTBUSY_STOP_BUSYING) ) { if ( GetCurTask() && GetCurTask()->iTask == TASK_ACTBUSY_PLAY_EXIT ) return true; } } return BaseClass::ShouldPlayerAvoid(); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::ComputeAndSetRenderBounds() { Vector mins, maxs; if ( GetOuter()->ComputeHitboxSurroundingBox( &mins, &maxs ) ) { UTIL_SetSize( GetOuter(), mins - GetAbsOrigin(), maxs - GetAbsOrigin()); if ( GetOuter()->VPhysicsGetObject() ) { GetOuter()->SetupVPhysicsHull(); } } } //----------------------------------------------------------------------------- // Purpose: Returns true if the current NPC is acting busy, or moving to an actbusy //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::IsActive( void ) { return ( m_bBusy || m_bForceActBusy || m_bNeedsToPlayExitAnim || m_bLeaving ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::IsCombatActBusy() { if( m_hActBusyGoal != NULL ) return (m_hActBusyGoal->GetType() == ACTBUSY_TYPE_COMBAT); return false; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::CollectSafeZoneVolumes( CAI_ActBusyGoal *pActBusyGoal ) { // Reset these, so we don't use a volume from a previous actbusy goal. m_SafeZones.RemoveAll(); if( pActBusyGoal->m_iszSafeZoneVolume != NULL_STRING ) { CBaseEntity *pVolume = gEntList.FindEntityByName( NULL, pActBusyGoal->m_iszSafeZoneVolume ); while( pVolume != NULL ) { busysafezone_t newSafeZone; pVolume->CollisionProp()->WorldSpaceAABB( &newSafeZone.vecMins, &newSafeZone.vecMaxs ); m_SafeZones.AddToTail( newSafeZone ); pVolume = gEntList.FindEntityByName( pVolume, pActBusyGoal->m_iszSafeZoneVolume ); } } if( ai_debug_actbusy.GetInt() == 5 ) { Msg( "Actbusy collected %d safe zones\n", m_SafeZones.Count() ); } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::IsInSafeZone( CBaseEntity *pEntity ) { Vector vecLocation = pEntity->WorldSpaceCenter(); for( int i = 0 ; i < m_SafeZones.Count() ; i++ ) { busysafezone_t *pSafeZone = &m_SafeZones[i]; if( vecLocation.x > pSafeZone->vecMins.x && vecLocation.y > pSafeZone->vecMins.y && vecLocation.z > pSafeZone->vecMins.z && vecLocation.x < pSafeZone->vecMaxs.x && vecLocation.y < pSafeZone->vecMaxs.y && vecLocation.z < pSafeZone->vecMaxs.z ) { return true; } } return false; } //----------------------------------------------------------------------------- // Purpose: Return true if this NPC has the anims required to use the specified actbusy hint //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::HasAnimForActBusy( int iActBusy, busyanimparts_t AnimPart ) { if ( iActBusy == -1 ) return false; busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( iActBusy ); if ( !pBusyAnim ) return false; // Try and play the sequence first if ( pBusyAnim->iszSequences[AnimPart] != NULL_STRING ) return (GetOuter()->LookupSequence( (char*)STRING(pBusyAnim->iszSequences[AnimPart]) ) != ACTIVITY_NOT_AVAILABLE); // Try and play the activity second if ( pBusyAnim->iActivities[AnimPart] != ACT_INVALID ) return GetOuter()->HaveSequenceForActivity( pBusyAnim->iActivities[AnimPart] ); return false; } //----------------------------------------------------------------------------- // Purpose: Play the sound associated with the specified part of the current actbusy, if any //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::PlaySoundForActBusy( busyanimparts_t AnimPart ) { busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim ); if ( !pBusyAnim ) return; // Play the sound if ( pBusyAnim->iszSounds[AnimPart] != NULL_STRING ) { // See if we can treat it as a game sound name CSoundParameters params; if ( GetOuter()->GetParametersForSound( STRING(pBusyAnim->iszSounds[AnimPart]), params, STRING(GetOuter()->GetModelName()) ) ) { CPASAttenuationFilter filter( GetOuter() ); GetOuter()->EmitSound( filter, GetOuter()->entindex(), params ); } else { // Assume it's a response concept, and try to speak it CAI_Expresser *pExpresser = GetOuter()->GetExpresser(); if ( pExpresser ) { AIConcept_t concept(STRING(pBusyAnim->iszSounds[AnimPart])); // Must be able to speak the concept if ( !pExpresser->IsSpeaking() && pExpresser->CanSpeakConcept( concept ) ) { pExpresser->Speak( concept ); } } } } } //----------------------------------------------------------------------------- // Purpose: Play a sequence or activity for the current actbusy //----------------------------------------------------------------------------- bool CAI_ActBusyBehavior::PlayAnimForActBusy( busyanimparts_t AnimPart ) { busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim ); if ( !pBusyAnim ) return false; // Try and play the sequence first if ( pBusyAnim->iszSequences[AnimPart] != NULL_STRING ) { GetOuter()->SetSequenceByName( (char*)STRING(pBusyAnim->iszSequences[AnimPart]) ); GetOuter()->SetIdealActivity( ACT_DO_NOT_DISTURB ); return true; } // Try and play the activity second if ( pBusyAnim->iActivities[AnimPart] != ACT_INVALID ) { GetOuter()->SetIdealActivity( pBusyAnim->iActivities[AnimPart] ); return true; } return false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::StartTask( const Task_t *pTask ) { switch ( pTask->iTask ) { case TASK_ACTBUSY_PLAY_BUSY_ANIM: { // If we're not enabled here, it's due to the actbusy being deactivated during // the NPC's entry animation. We can't abort in the middle of the entry, so we // arrive here with a disabled actbusy behaviour. Exit gracefully. if ( !m_bEnabled ) { TaskComplete(); return; } // Set the flag to remind the code to recompute the NPC's box from render bounds. // This is used to delay the process so that we don't get a box built from render bounds // when a character is still interpolating to their busy pose. m_bNeedToSetBounds = true; // Get the busyanim for the specified activity busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim ); // We start "flying" so we don't collide with the world, in case the level // designer has us sitting on a chair, etc. if( !pBusyAnim || !pBusyAnim->bUseAutomovement ) { GetOuter()->AddFlag( FL_FLY ); } GetOuter()->SetGroundEntity( NULL ); // Fail if we're not on the node & facing the correct way // We only do this check if we're still moving to the busy. This will only // be true if there was no entry animation for this busy. We do it this way // because the entry code contains this same check, and so we assume we're // valid even if we're off now, because some entry animations move the // character off the node. if ( m_bMovingToBusy ) { if ( UTIL_DistApprox( GetHintNode()->GetAbsOrigin(), GetAbsOrigin() ) > 16 || !GetOuter()->FacingIdeal() ) { TaskFail( "Not correctly on hintnode" ); m_flEndBusyAt = gpGlobals->curtime; return; } } m_bMovingToBusy = false; if ( !ActBusyNodeStillActive() ) { TaskFail( FAIL_NO_HINT_NODE ); return; } // Have we just started using this node? if ( !m_bBusy ) { m_bBusy = true; GetHintNode()->NPCStartedUsing( GetOuter() ); if ( m_hActBusyGoal ) { m_hActBusyGoal->NPCStartedBusy( GetOuter() ); } if ( pBusyAnim ) { float flMaxTime = pBusyAnim->flMaxTime; float flMinTime = pBusyAnim->flMinTime; // Mapmaker input may have specified it's own max time if ( m_bForceActBusy && m_flForcedMaxTime != NO_MAX_TIME ) { flMaxTime = m_flForcedMaxTime; // Don't let non-unlimited time amounts be less than the mintime if ( flMaxTime && flMaxTime < flMinTime ) { flMinTime = flMaxTime; } } // If we have no max time, or we're in a queue, we loop forever. if ( !flMaxTime || m_bInQueue ) { m_flEndBusyAt = 0; GetOuter()->SetWait( 99999 ); } else { float flTime = RandomFloat(flMinTime, flMaxTime); m_flEndBusyAt = gpGlobals->curtime + flTime; GetOuter()->SetWait( flTime ); } } } // Start playing the act busy PlayAnimForActBusy( BA_BUSY ); PlaySoundForActBusy( BA_BUSY ); // Now that we're busy, we don't need to be forced anymore m_bForceActBusy = false; m_bTeleportToBusy = false; m_bUseNearestBusy = false; m_ForcedActivity = ACT_INVALID; // If we're supposed to use render bounds while inside the busy anim, do so if ( m_bUseRenderBoundsForCollision ) { ComputeAndSetRenderBounds(); } } break; case TASK_ACTBUSY_PLAY_ENTRY: { // We start "flying" so we don't collide with the world, in case the level // designer has us sitting on a chair, etc. // Get the busyanim for the specified activity busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim ); // We start "flying" so we don't collide with the world, in case the level // designer has us sitting on a chair, etc. if( !pBusyAnim || !pBusyAnim->bUseAutomovement ) { GetOuter()->AddFlag( FL_FLY ); } GetOuter()->SetGroundEntity( NULL ); m_bMovingToBusy = false; m_bNeedsToPlayExitAnim = HasAnimForActBusy( m_iCurrentBusyAnim, BA_EXIT ); if ( !ActBusyNodeStillActive() ) { TaskFail( FAIL_NO_HINT_NODE ); return; } // Fail if we're not on the node & facing the correct way if ( UTIL_DistApprox( GetHintNode()->GetAbsOrigin(), GetAbsOrigin() ) > 16 || !GetOuter()->FacingIdeal() ) { m_bBusy = false; TaskFail( "Not correctly on hintnode" ); return; } PlaySoundForActBusy( BA_ENTRY ); // Play the entry animation. If it fails, we don't have an entry anim, so complete immediately. if ( !PlayAnimForActBusy( BA_ENTRY ) ) { TaskComplete(); } } break; case TASK_ACTBUSY_VERIFY_EXIT: { // NPC's that changed their bounding box must ensure that they can restore their regular box // before they exit their actbusy. This task is designed to delay until that time if necessary. if( !m_bUseRenderBoundsForCollision ) { // Don't bother if we didn't alter our BBox. TaskComplete(); break; } // Set up a timer to check immediately. GetOuter()->SetWait( 0 ); } break; case TASK_ACTBUSY_PLAY_EXIT: { // If we're supposed to use render bounds while inside the busy anim, restore normal now if ( m_bUseRenderBoundsForCollision ) { GetOuter()->SetHullSizeNormal( true ); } if ( m_hActBusyGoal ) { m_hActBusyGoal->NPCStartedLeavingBusy( GetOuter() ); } PlaySoundForActBusy( BA_EXIT ); // Play the exit animation. If it fails, we don't have an entry anim, so complete immediately. if ( !PlayAnimForActBusy( BA_EXIT ) ) { m_bNeedsToPlayExitAnim = false; GetOuter()->RemoveFlag( FL_FLY ); NotifyBusyEnding(); TaskComplete(); } } break; case TASK_ACTBUSY_TELEPORT_TO_BUSY: { if ( !ActBusyNodeStillActive() ) { TaskFail( FAIL_NO_HINT_NODE ); return; } Vector vecAbsOrigin = GetHintNode()->GetAbsOrigin(); QAngle vecAbsAngles = GetHintNode()->GetAbsAngles(); GetOuter()->Teleport( &vecAbsOrigin, &vecAbsAngles, NULL ); GetOuter()->GetMotor()->SetIdealYaw( vecAbsAngles.y ); TaskComplete(); } break; case TASK_ACTBUSY_WALK_PATH_TO_BUSY: { // If we have a forced activity, use that. Otherwise, walk. if ( m_ForcedActivity != ACT_INVALID && m_ForcedActivity != ACT_RESET ) { GetNavigator()->SetMovementActivity( m_ForcedActivity ); // Cover is void once I move Forget( bits_MEMORY_INCOVER ); TaskComplete(); } else { if( IsCombatActBusy() ) { ChainStartTask( TASK_RUN_PATH ); } else { ChainStartTask( TASK_WALK_PATH ); } } break; } case TASK_ACTBUSY_GET_PATH_TO_ACTBUSY: { ChainStartTask( TASK_GET_PATH_TO_HINTNODE ); if ( !HasCondition(COND_TASK_FAILED) ) { // We successfully built a path, so stop counting consecutive failures. m_iNumConsecutivePathFailures = 0; // Set the arrival sequence for the actbusy to be the busy sequence, if we don't have an entry animation busyanim_t *pBusyAnim = g_ActBusyAnimDataSystem.GetBusyAnim( m_iCurrentBusyAnim ); if ( pBusyAnim && pBusyAnim->iszSequences[BA_ENTRY] == NULL_STRING && pBusyAnim->iActivities[BA_ENTRY] == ACT_INVALID ) { // Try and play the sequence first if ( pBusyAnim->iszSequences[BA_BUSY] != NULL_STRING ) { GetNavigator()->SetArrivalSequence( GetOuter()->LookupSequence( STRING(pBusyAnim->iszSequences[BA_BUSY]) ) ); } else if ( pBusyAnim->iActivities[BA_BUSY] != ACT_INVALID ) { // Try and play the activity second GetNavigator()->SetArrivalActivity( pBusyAnim->iActivities[BA_BUSY] ); } } else { // Robin: Set the arrival sequence / activity to be the entry animation. if ( pBusyAnim->iszSequences[BA_ENTRY] != NULL_STRING ) { GetNavigator()->SetArrivalSequence( GetOuter()->LookupSequence( STRING(pBusyAnim->iszSequences[BA_ENTRY]) ) ); } else if ( pBusyAnim->iActivities[BA_ENTRY] != ACT_INVALID ) { // Try and play the activity second GetNavigator()->SetArrivalActivity( pBusyAnim->iActivities[BA_ENTRY] ); } } } else { m_iNumConsecutivePathFailures++; if ( ai_debug_actbusy.GetInt() == 1 ) { if ( GetHintNode() ) { // Show which actbusy we're moving towards NDebugOverlay::Line( GetOuter()->WorldSpaceCenter(), GetHintNode()->GetAbsOrigin(), 255, 0, 0, true, 1.0 ); } } } break; } default: BaseClass::StartTask( pTask); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::RunTask( const Task_t *pTask ) { switch ( pTask->iTask ) { case TASK_WAIT_FOR_MOVEMENT: { // Ensure the hint node hasn't been disabled if ( IsCurSchedule( SCHED_ACTBUSY_START_BUSYING ) ) { if ( !ActBusyNodeStillActive() ) { TaskFail(FAIL_NO_HINT_NODE); return; } } if ( ai_debug_actbusy.GetInt() == 1 ) { if ( GetHintNode() ) { // Show which actbusy we're moving towards NDebugOverlay::Line( GetOuter()->WorldSpaceCenter(), GetHintNode()->GetAbsOrigin(), 0, 255, 0, true, 0.2 ); } } BaseClass::RunTask( pTask ); break; } case TASK_ACTBUSY_PLAY_BUSY_ANIM: { if( m_bUseRenderBoundsForCollision ) { if( GetOuter()->IsSequenceFinished() && m_bNeedToSetBounds ) { ComputeAndSetRenderBounds(); m_bNeedToSetBounds = false; } } if( IsCombatActBusy() ) { if( GetEnemy() != NULL && !HasCondition(COND_ENEMY_OCCLUDED) ) { // Break a combat actbusy if an enemy gets very close. // I'll probably go to hell for not doing this with conditions like I should. (sjb) float flDistSqr = GetAbsOrigin().DistToSqr( GetEnemy()->GetAbsOrigin() ); if( flDistSqr < Square(12.0f * 15.0f) ) { // End now. m_flEndBusyAt = gpGlobals->curtime; TaskComplete(); return; } } } GetOuter()->AutoMovement(); // Stop if the node's been disabled if ( !ActBusyNodeStillActive() || GetOuter()->IsWaitFinished() ) { TaskComplete(); } else { CAI_PlayerAlly *pAlly = dynamic_cast(GetOuter()); if ( pAlly ) { pAlly->SelectInterjection(); } if( HasCondition(COND_ACTBUSY_LOST_SEE_ENTITY) ) { StopBusying(); TaskComplete(); } } break; } case TASK_ACTBUSY_PLAY_ENTRY: { GetOuter()->AutoMovement(); if ( !ActBusyNodeStillActive() || GetOuter()->IsSequenceFinished() ) { TaskComplete(); } } break; case TASK_ACTBUSY_VERIFY_EXIT: { if( GetOuter()->IsWaitFinished() ) { // Trace my normal hull over this spot to see if I'm able to stand up right now. trace_t tr; CTraceFilterOnlyNPCsAndPlayer filter( GetOuter(), COLLISION_GROUP_NONE ); UTIL_TraceHull( GetOuter()->GetAbsOrigin(), GetOuter()->GetAbsOrigin(), NAI_Hull::Mins( HULL_HUMAN ), NAI_Hull::Maxs( HULL_HUMAN ), GetOuter()->GetAITraceMask(), &filter, &tr ); if( tr.startsolid ) { // Blocked. Try again later. GetOuter()->SetWait( 1.0f ); } else { // Put an entity blocker here for a moment until I get into my bounding box. CBaseEntity *pBlocker = CEntityBlocker::Create( GetOuter()->GetAbsOrigin(), NAI_Hull::Mins( HULL_HUMAN ), NAI_Hull::Maxs( HULL_HUMAN ), GetOuter(), true ); g_EventQueue.AddEvent( pBlocker, "Kill", 1.0, GetOuter(), GetOuter() ); TaskComplete(); } } } break; case TASK_ACTBUSY_PLAY_EXIT: { GetOuter()->AutoMovement(); if ( GetOuter()->IsSequenceFinished() ) { m_bNeedsToPlayExitAnim = false; GetOuter()->RemoveFlag( FL_FLY ); NotifyBusyEnding(); TaskComplete(); } } break; default: BaseClass::RunTask( pTask); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyBehavior::NotifyBusyEnding( void ) { // Be sure to disable autofire m_bAutoFireWeapon = false; // Clear the hintnode if we're done with it if ( GetHintNode() ) { if ( m_bBusy || m_bMovingToBusy ) { GetHintNode()->NPCStoppedUsing( GetOuter() ); } GetHintNode()->Unlock(); if( IsCombatActBusy() ) { // Don't allow anyone to use this node for a bit. This is so the tactical position // doesn't get re-occupied the moment I leave it. GetHintNode()->DisableForSeconds( random->RandomFloat( 10, 15) ); } SetHintNode( NULL ); } // Then, if we were busy, stop being busy if ( m_bBusy ) { m_bBusy = false; if ( m_hActBusyGoal ) { m_hActBusyGoal->NPCFinishedBusy( GetOuter() ); if ( m_bExitedBusyToDueLostSeeEntity ) { m_hActBusyGoal->NPCLostSeeEntity( GetOuter() ); m_bExitedBusyToDueLostSeeEntity = false; } if ( m_bExitedBusyToDueSeeEnemy ) { m_hActBusyGoal->NPCSeeEnemy( GetOuter() ); m_bExitedBusyToDueSeeEnemy = false; } } } else if ( m_bMovingToBusy && m_hActBusyGoal ) { // Or if we were just on our way to be busy, let the goal know m_hActBusyGoal->NPCAbortedMoveTo( GetOuter() ); } // Don't busy again for a while m_flEndBusyAt = 0; if( IsCombatActBusy() ) { // Actbusy again soon. Real soon. m_flNextBusySearchTime = gpGlobals->curtime; } else { m_flNextBusySearchTime = gpGlobals->curtime + (RandomFloat(ai_actbusy_search_time.GetFloat(), ai_actbusy_search_time.GetFloat()*2)); } } //------------------------------------- AI_BEGIN_CUSTOM_SCHEDULE_PROVIDER( CAI_ActBusyBehavior ) DECLARE_CONDITION( COND_ACTBUSY_LOST_SEE_ENTITY ) DECLARE_CONDITION( COND_ACTBUSY_AWARE_OF_ENEMY_IN_SAFE_ZONE ) DECLARE_CONDITION( COND_ACTBUSY_ENEMY_TOO_CLOSE ) DECLARE_TASK( TASK_ACTBUSY_PLAY_BUSY_ANIM ) DECLARE_TASK( TASK_ACTBUSY_PLAY_ENTRY ) DECLARE_TASK( TASK_ACTBUSY_PLAY_EXIT ) DECLARE_TASK( TASK_ACTBUSY_TELEPORT_TO_BUSY ) DECLARE_TASK( TASK_ACTBUSY_WALK_PATH_TO_BUSY ) DECLARE_TASK( TASK_ACTBUSY_GET_PATH_TO_ACTBUSY ) DECLARE_TASK( TASK_ACTBUSY_VERIFY_EXIT ) DECLARE_ANIMEVENT( AE_ACTBUSY_WEAPON_FIRE_ON ) DECLARE_ANIMEVENT( AE_ACTBUSY_WEAPON_FIRE_OFF ) //--------------------------------- DEFINE_SCHEDULE ( SCHED_ACTBUSY_START_BUSYING, " Tasks" " TASK_SET_TOLERANCE_DISTANCE 4" " TASK_ACTBUSY_GET_PATH_TO_ACTBUSY 0" " TASK_ACTBUSY_WALK_PATH_TO_BUSY 0" " TASK_WAIT_FOR_MOVEMENT 0" " TASK_STOP_MOVING 0" " TASK_FACE_HINTNODE 0" " TASK_ACTBUSY_PLAY_ENTRY 0" " TASK_SET_SCHEDULE SCHEDULE:SCHED_ACTBUSY_BUSY" "" " Interrupts" " COND_ACTBUSY_LOST_SEE_ENTITY" ) DEFINE_SCHEDULE ( SCHED_ACTBUSY_BUSY, " Tasks" " TASK_ACTBUSY_PLAY_BUSY_ANIM 0" "" " Interrupts" " COND_PROVOKED" ) DEFINE_SCHEDULE ( SCHED_ACTBUSY_STOP_BUSYING, " Tasks" " TASK_ACTBUSY_VERIFY_EXIT 0" " TASK_ACTBUSY_PLAY_EXIT 0" " TASK_WAIT 0.1" "" " Interrupts" " COND_NO_CUSTOM_INTERRUPTS" ) DEFINE_SCHEDULE ( SCHED_ACTBUSY_LEAVE, " Tasks" " TASK_SET_TOLERANCE_DISTANCE 4" " TASK_ACTBUSY_GET_PATH_TO_ACTBUSY 0" " TASK_ACTBUSY_WALK_PATH_TO_BUSY 0" " TASK_WAIT_FOR_MOVEMENT 0" "" " Interrupts" " COND_PROVOKED" ) DEFINE_SCHEDULE ( SCHED_ACTBUSY_TELEPORT_TO_BUSY, " Tasks" " TASK_ACTBUSY_TELEPORT_TO_BUSY 0" " TASK_ACTBUSY_PLAY_ENTRY 0" " TASK_SET_SCHEDULE SCHEDULE:SCHED_ACTBUSY_BUSY" "" " Interrupts" " COND_PROVOKED" ) AI_END_CUSTOM_SCHEDULE_PROVIDER() //========================================================================================================== // ACT BUSY GOALS //========================================================================================================== //----------------------------------------------------------------------------- // Purpose: A level tool to control the actbusy behavior. //----------------------------------------------------------------------------- LINK_ENTITY_TO_CLASS( ai_goal_actbusy, CAI_ActBusyGoal ); BEGIN_DATADESC( CAI_ActBusyGoal ) DEFINE_KEYFIELD( m_flBusySearchRange, FIELD_FLOAT, "busysearchrange" ), DEFINE_KEYFIELD( m_bVisibleOnly, FIELD_BOOLEAN, "visibleonly" ), DEFINE_KEYFIELD( m_iType, FIELD_INTEGER, "type" ), DEFINE_KEYFIELD( m_bAllowCombatActBusyTeleport, FIELD_BOOLEAN, "allowteleport" ), DEFINE_KEYFIELD( m_iszSeeEntityName, FIELD_STRING, "SeeEntity" ), DEFINE_KEYFIELD( m_flSeeEntityTimeout, FIELD_FLOAT, "SeeEntityTimeout" ), DEFINE_KEYFIELD( m_iszSafeZoneVolume, FIELD_STRING, "SafeZone" ), DEFINE_KEYFIELD( m_iSightMethod, FIELD_INTEGER, "sightmethod" ), // Inputs DEFINE_INPUTFUNC( FIELD_FLOAT, "SetBusySearchRange", InputSetBusySearchRange ), DEFINE_INPUTFUNC( FIELD_STRING, "ForceNPCToActBusy", InputForceNPCToActBusy ), DEFINE_INPUTFUNC( FIELD_EHANDLE, "ForceThisNPCToActBusy", InputForceThisNPCToActBusy ), DEFINE_INPUTFUNC( FIELD_EHANDLE, "ForceThisNPCToLeave", InputForceThisNPCToLeave ), // Outputs DEFINE_OUTPUT( m_OnNPCStartedBusy, "OnNPCStartedBusy" ), DEFINE_OUTPUT( m_OnNPCFinishedBusy, "OnNPCFinishedBusy" ), DEFINE_OUTPUT( m_OnNPCLeft, "OnNPCLeft" ), DEFINE_OUTPUT( m_OnNPCLostSeeEntity, "OnNPCLostSeeEntity" ), DEFINE_OUTPUT( m_OnNPCSeeEnemy, "OnNPCSeeEnemy" ), END_DATADESC() //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CAI_ActBusyBehavior *CAI_ActBusyGoal::GetBusyBehaviorForNPC( CBaseEntity *pEntity, const char *sInputName ) { CAI_BaseNPC *pActor = dynamic_cast(pEntity); if ( !pActor ) { Msg("ai_goal_actbusy input %s fired targeting an entity that isn't an NPC.\n", sInputName); return NULL; } // Get the NPC's behavior CAI_ActBusyBehavior *pBehavior; if ( !pActor->GetBehavior( &pBehavior ) ) { Msg("ai_goal_actbusy input %s fired on an NPC that doesn't support ActBusy behavior.\n", sInputName ); return NULL; } return pBehavior; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- CAI_ActBusyBehavior *CAI_ActBusyGoal::GetBusyBehaviorForNPC( const char *pszActorName, CBaseEntity *pActivator, CBaseEntity *pCaller, const char *sInputName ) { CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, MAKE_STRING(pszActorName), NULL, pActivator, pCaller ); if ( !pEntity ) { Msg("ai_goal_actbusy input %s fired targeting a non-existant entity (%s).\n", sInputName, pszActorName ); return NULL; } return GetBusyBehaviorForNPC( pEntity, sInputName ); } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CAI_ActBusyGoal::EnableGoal( CAI_BaseNPC *pAI ) { BaseClass::EnableGoal( pAI ); // Now use this actor to lookup the Behavior CAI_ActBusyBehavior *pBehavior; if ( pAI->GetBehavior( &pBehavior ) ) { // Some NPCs may already be active due to a ForceActBusy input. if ( !pBehavior->IsEnabled() ) { pBehavior->Enable( this, m_flBusySearchRange, m_bVisibleOnly ); } } else { DevMsg( "ActBusy goal entity activated for an NPC (%s) that doesn't have the ActBusy behavior\n", pAI->GetDebugName() ); return; } } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CAI_ActBusyGoal::InputActivate( inputdata_t &inputdata ) { if ( ai_debug_actbusy.GetInt() == 4 ) { Msg("ACTBUSY: Actbusy goal %s (%s) activated.\n", GetClassname(), GetDebugName() ); } BaseClass::InputActivate( inputdata ); } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CAI_ActBusyGoal::InputDeactivate( inputdata_t &inputdata ) { if ( ai_debug_actbusy.GetInt() == 4 ) { Msg("ACTBUSY: Actbusy goal %s (%s) disabled.\n", GetClassname(), GetDebugName() ); } BaseClass::InputDeactivate( inputdata ); for( int i = 0 ; i < NumActors() ; i++ ) { CAI_BaseNPC *pActor = GetActor( i ); if ( pActor ) { // Now use this actor to lookup the Behavior CAI_ActBusyBehavior *pBehavior; if ( pActor->GetBehavior( &pBehavior ) ) { pBehavior->Disable(); } else { DevMsg( "ActBusy goal entity deactivated for an NPC that doesn't have the ActBusy behavior\n" ); return; } } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyGoal::InputSetBusySearchRange( inputdata_t &inputdata ) { m_flBusySearchRange = inputdata.value.Float(); for( int i = 0 ; i < NumActors() ; i++ ) { CAI_BaseNPC *pActor = GetActor( i ); if ( pActor ) { // Now use this actor to lookup the Behavior CAI_ActBusyBehavior *pBehavior; if ( pActor->GetBehavior( &pBehavior ) ) { pBehavior->SetBusySearchRange( m_flBusySearchRange ); } } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyGoal::InputForceNPCToActBusy( inputdata_t &inputdata ) { char parseString[255]; Q_strncpy(parseString, inputdata.value.String(), sizeof(parseString)); CAI_Hint *pHintNode = NULL; float flMaxTime = NO_MAX_TIME; bool bTeleport = false; bool bUseNearestBusy = false; CBaseEntity *pSeeEntity = NULL; // Get NPC name char *pszParam = strtok(parseString," "); CAI_ActBusyBehavior *pBehavior = GetBusyBehaviorForNPC( pszParam, inputdata.pActivator, inputdata.pCaller, "InputForceNPCToActBusy" ); if ( !pBehavior ) return; // Wrapped this bugfix so that it doesn't break HL2. bool bEpisodicBugFix = hl2_episodic.GetBool(); // Do we have a specified node too? pszParam = strtok(NULL," "); if ( pszParam ) { // Find the specified hintnode CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, pszParam, NULL, inputdata.pActivator, inputdata.pCaller ); if ( pEntity ) { pHintNode = dynamic_cast(pEntity); if ( !pHintNode ) { Msg("ai_goal_actbusy input ForceNPCToActBusy fired targeting an entity that isn't a hintnode.\n", pszParam); return; } if ( bEpisodicBugFix ) { pszParam = strtok(NULL," "); } } } Activity activity = ACT_INVALID; if ( !bEpisodicBugFix ) { pszParam = strtok(NULL," "); } while ( pszParam ) { // Teleport? if ( !Q_strncmp( pszParam, "teleport", 8 ) ) { bTeleport = true; } else if ( !Q_strncmp( pszParam, "nearest", 8 ) ) { bUseNearestBusy = true; } else if ( !Q_strncmp( pszParam, "see:", 4 ) ) { pSeeEntity = gEntList.FindEntityByName( NULL, pszParam+4 ); } else if ( pszParam[0] == '$' ) { // $ signs prepend custom movement sequences / activities const char *pAnimName = pszParam+1; // Try and resolve it as an activity name activity = (Activity)ActivityList_IndexForName( pAnimName ); if ( activity == ACT_INVALID ) { // Try it as sequence name pBehavior->GetOuter()->m_iszSceneCustomMoveSeq = AllocPooledString( pAnimName ); activity = ACT_SCRIPT_CUSTOM_MOVE; } } else { // Do we have a specified time? flMaxTime = atof( pszParam ); } pszParam = strtok(NULL," "); } if ( ai_debug_actbusy.GetInt() == 4 ) { Msg("ACTBUSY: Actbusy goal %s (%s) ForceNPCToActBusy input with data: %s.\n", GetClassname(), GetDebugName(), parseString ); } // Tell the NPC to immediately act busy pBehavior->SetBusySearchRange( m_flBusySearchRange ); pBehavior->ForceActBusy( this, pHintNode, flMaxTime, m_bVisibleOnly, bTeleport, bUseNearestBusy, pSeeEntity, activity ); } //----------------------------------------------------------------------------- // Purpose: Force the passed in NPC to actbusy //----------------------------------------------------------------------------- void CAI_ActBusyGoal::InputForceThisNPCToActBusy( inputdata_t &inputdata ) { CAI_ActBusyBehavior *pBehavior = GetBusyBehaviorForNPC( inputdata.value.Entity(), "InputForceThisNPCToActBusy" ); if ( !pBehavior ) return; // Tell the NPC to immediately act busy pBehavior->SetBusySearchRange( m_flBusySearchRange ); pBehavior->ForceActBusy( this ); } //----------------------------------------------------------------------------- // Purpose: Force the passed in NPC to walk to a point and vanish //----------------------------------------------------------------------------- void CAI_ActBusyGoal::InputForceThisNPCToLeave( inputdata_t &inputdata ) { CAI_ActBusyBehavior *pBehavior = GetBusyBehaviorForNPC( inputdata.value.Entity(), "InputForceThisNPCToLeave" ); if ( !pBehavior ) return; // Tell the NPC to find a leave point and move to it pBehavior->SetBusySearchRange( m_flBusySearchRange ); pBehavior->ForceActBusyLeave(); } //----------------------------------------------------------------------------- // Purpose: // Input : *pNPC - //----------------------------------------------------------------------------- void CAI_ActBusyGoal::NPCMovingToBusy( CAI_BaseNPC *pNPC ) { } //----------------------------------------------------------------------------- // Purpose: // Input : *pNPC - //----------------------------------------------------------------------------- void CAI_ActBusyGoal::NPCStartedBusy( CAI_BaseNPC *pNPC ) { m_OnNPCStartedBusy.Set( pNPC, pNPC, this ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyGoal::NPCStartedLeavingBusy( CAI_BaseNPC *pNPC ) { } //----------------------------------------------------------------------------- // Purpose: // Input : *pNPC - //----------------------------------------------------------------------------- void CAI_ActBusyGoal::NPCAbortedMoveTo( CAI_BaseNPC *pNPC ) { } //----------------------------------------------------------------------------- // Purpose: // Input : *pNPC - //----------------------------------------------------------------------------- void CAI_ActBusyGoal::NPCFinishedBusy( CAI_BaseNPC *pNPC ) { m_OnNPCFinishedBusy.Set( pNPC, pNPC, this ); } //----------------------------------------------------------------------------- // Purpose: // Input : *pNPC - //----------------------------------------------------------------------------- void CAI_ActBusyGoal::NPCLeft( CAI_BaseNPC *pNPC ) { m_OnNPCLeft.Set( pNPC, pNPC, this ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_ActBusyGoal::NPCLostSeeEntity( CAI_BaseNPC *pNPC ) { m_OnNPCLostSeeEntity.Set( pNPC, pNPC, this ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CAI_ActBusyGoal::NPCSeeEnemy( CAI_BaseNPC *pNPC ) { m_OnNPCSeeEnemy.Set( pNPC, pNPC, this ); } //========================================================================================================== // ACT BUSY QUEUE //========================================================================================================== //----------------------------------------------------------------------------- // Purpose: A level tool to control the actbusy behavior to create NPC queues //----------------------------------------------------------------------------- LINK_ENTITY_TO_CLASS( ai_goal_actbusy_queue, CAI_ActBusyQueueGoal ); BEGIN_DATADESC( CAI_ActBusyQueueGoal ) // Keys DEFINE_FIELD( m_iCurrentQueueCount, FIELD_INTEGER ), DEFINE_ARRAY( m_hNodes, FIELD_EHANDLE, MAX_QUEUE_NODES ), DEFINE_ARRAY( m_bPlayerBlockedNodes, FIELD_BOOLEAN, MAX_QUEUE_NODES ), DEFINE_FIELD( m_hExitNode, FIELD_EHANDLE ), DEFINE_FIELD( m_hExitingNPC, FIELD_EHANDLE ), DEFINE_KEYFIELD( m_bForceReachFront, FIELD_BOOLEAN, "mustreachfront" ), // DEFINE_ARRAY( m_iszNodes, FIELD_STRING, MAX_QUEUE_NODES ), // Silence Classcheck! DEFINE_KEYFIELD( m_iszNodes[0], FIELD_STRING, "node01"), DEFINE_KEYFIELD( m_iszNodes[1], FIELD_STRING, "node02"), DEFINE_KEYFIELD( m_iszNodes[2], FIELD_STRING, "node03"), DEFINE_KEYFIELD( m_iszNodes[3], FIELD_STRING, "node04"), DEFINE_KEYFIELD( m_iszNodes[4], FIELD_STRING, "node05"), DEFINE_KEYFIELD( m_iszNodes[5], FIELD_STRING, "node06"), DEFINE_KEYFIELD( m_iszNodes[6], FIELD_STRING, "node07"), DEFINE_KEYFIELD( m_iszNodes[7], FIELD_STRING, "node08"), DEFINE_KEYFIELD( m_iszNodes[8], FIELD_STRING, "node09"), DEFINE_KEYFIELD( m_iszNodes[9], FIELD_STRING, "node10"), DEFINE_KEYFIELD( m_iszNodes[10], FIELD_STRING, "node11"), DEFINE_KEYFIELD( m_iszNodes[11], FIELD_STRING, "node12"), DEFINE_KEYFIELD( m_iszNodes[12], FIELD_STRING, "node13"), DEFINE_KEYFIELD( m_iszNodes[13], FIELD_STRING, "node14"), DEFINE_KEYFIELD( m_iszNodes[14], FIELD_STRING, "node15"), DEFINE_KEYFIELD( m_iszNodes[15], FIELD_STRING, "node16"), DEFINE_KEYFIELD( m_iszNodes[16], FIELD_STRING, "node17"), DEFINE_KEYFIELD( m_iszNodes[17], FIELD_STRING, "node18"), DEFINE_KEYFIELD( m_iszNodes[18], FIELD_STRING, "node19"), DEFINE_KEYFIELD( m_iszNodes[19], FIELD_STRING, "node20"), DEFINE_KEYFIELD( m_iszExitNode, FIELD_STRING, "node_exit"), // Inputs DEFINE_INPUTFUNC( FIELD_INTEGER, "PlayerStartedBlocking", InputPlayerStartedBlocking ), DEFINE_INPUTFUNC( FIELD_INTEGER, "PlayerStoppedBlocking", InputPlayerStoppedBlocking ), DEFINE_INPUTFUNC( FIELD_VOID, "MoveQueueUp", InputMoveQueueUp ), // Outputs DEFINE_OUTPUT( m_OnQueueMoved, "OnQueueMoved" ), DEFINE_OUTPUT( m_OnNPCLeftQueue, "OnNPCLeftQueue" ), DEFINE_OUTPUT( m_OnNPCStartedLeavingQueue, "OnNPCStartedLeavingQueue" ), DEFINE_THINKFUNC( QueueThink ), DEFINE_THINKFUNC( MoveQueueUpThink ), END_DATADESC() #define QUEUE_THINK_CONTEXT "ActBusyQueueThinkContext" #define QUEUE_MOVEUP_THINK_CONTEXT "ActBusyQueueMoveUpThinkContext" //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::Spawn( void ) { BaseClass::Spawn(); RegisterThinkContext( QUEUE_MOVEUP_THINK_CONTEXT ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::DrawDebugGeometryOverlays( void ) { BaseClass::DrawDebugGeometryOverlays(); // Debug for reservers for ( int i = 0; i < MAX_QUEUE_NODES; i++ ) { if ( !m_hNodes[i] ) continue; if ( m_bPlayerBlockedNodes[i] ) { NDebugOverlay::Box( m_hNodes[i]->GetAbsOrigin(), -Vector(5,5,5), Vector(5,5,5), 255, 0, 0, 0, 0.1 ); } else { NDebugOverlay::Box( m_hNodes[i]->GetAbsOrigin(), -Vector(5,5,5), Vector(5,5,5), 255, 255, 255, 0, 0.1 ); } } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::InputActivate( inputdata_t &inputdata ) { if ( !IsActive() ) { // Find all our nodes for ( int i = 0; i < MAX_QUEUE_NODES; i++ ) { if ( m_iszNodes[i] == NULL_STRING ) { m_hNodes[i] = NULL; continue; } CBaseEntity *pEntity = gEntList.FindEntityByName( NULL, m_iszNodes[i] ); if ( !pEntity ) { Warning( "Unable to find ai_goal_actbusy_queue %s's node %d: %s\n", STRING(GetEntityName()), i, STRING(m_iszNodes[i]) ); UTIL_Remove( this ); return; } m_hNodes[i] = dynamic_cast(pEntity); if ( !m_hNodes[i] ) { Warning( "ai_goal_actbusy_queue %s's node %d: '%s' is not an ai_hint.\n", STRING(GetEntityName()), i, STRING(m_iszNodes[i]) ); UTIL_Remove( this ); return; } // Disable all but the first node if ( i == 0 ) { m_hNodes[i]->SetDisabled( false ); } else { m_hNodes[i]->SetDisabled( true ); } } // Find the exit node m_hExitNode = gEntList.FindEntityByName( NULL, m_iszExitNode ); if ( !m_hExitNode ) { Warning( "Unable to find ai_goal_actbusy_queue %s's exit node: %s\n", STRING(GetEntityName()), STRING(m_iszExitNode) ); UTIL_Remove( this ); return; } RecalculateQueueCount(); SetContextThink( &CAI_ActBusyQueueGoal::QueueThink, gpGlobals->curtime + 5, QUEUE_THINK_CONTEXT ); } BaseClass::InputActivate( inputdata ); } //----------------------------------------------------------------------------- // Purpose: // Input : iCount - //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::RecalculateQueueCount( void ) { // First, find the highest unused node in the queue int iCount = 0; for ( int i = 0; i < MAX_QUEUE_NODES; i++ ) { if ( NodeIsOccupied(i) || m_bPlayerBlockedNodes[i] ) { iCount = i+1; } } //Msg("Count: %d (OLD %d)\n", iCount, m_iCurrentQueueCount ); // Queue hasn't changed? if ( iCount == m_iCurrentQueueCount ) return; for ( int i = 0; i < MAX_QUEUE_NODES; i++ ) { if ( m_hNodes[i] ) { // Disable nodes beyond 1 past the end of the queue if ( i > iCount ) { m_hNodes[i]->SetDisabled( true ); } else { m_hNodes[i]->SetDisabled( false ); // To prevent NPCs outside the queue moving directly to nodes within the queue, only // have the entry node be a valid actbusy node. if ( i == iCount ) { m_hNodes[i]->SetHintType( HINT_WORLD_WORK_POSITION ); } else { m_hNodes[i]->SetHintType( HINT_NONE ); } } } } m_iCurrentQueueCount = iCount; m_OnQueueMoved.Set( m_iCurrentQueueCount, this, this); } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::InputPlayerStartedBlocking( inputdata_t &inputdata ) { int iNode = inputdata.value.Int() - 1; Assert( iNode >= 0 && iNode < MAX_QUEUE_NODES ); m_bPlayerBlockedNodes[iNode] = true; /* // First, find all NPCs heading to points in front of the player's blocked spot for ( int i = 0; i < iNode; i++ ) { CAI_BaseNPC *pNPC = GetNPCOnNode(i); if ( !pNPC ) continue; CAI_ActBusyBehavior *pBehavior = GetQueueBehaviorForNPC( pNPC ); if ( pBehavior->IsMovingToBusy() ) { // We may be ahead of the player in the queue, which means we can safely // be left alone to reach the node. Make sure we're not closer to it than the player is float flPlayerDistToNode = (inputdata.pActivator->GetAbsOrigin() - m_hNodes[i]->GetAbsOrigin()).LengthSqr(); if ( (pNPC->GetAbsOrigin() - m_hNodes[i]->GetAbsOrigin()).LengthSqr() < flPlayerDistToNode ) continue; // We're an NPC heading to a node past the player, and yet the player's in our way. pBehavior->StopBusying(); } } */ // If an NPC was heading towards this node, tell him to go elsewhere CAI_BaseNPC *pNPC = GetNPCOnNode(iNode); PushNPCBackInQueue( pNPC, iNode ); RecalculateQueueCount(); } //----------------------------------------------------------------------------- // Purpose: Find a node back in the queue to move to, and push all NPCs beyond that backwards //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::PushNPCBackInQueue( CAI_BaseNPC *pNPC, int iStartingNode ) { // Push this guy back, and tell everyone behind him to move back too, until we find a gap while ( pNPC ) { CAI_ActBusyBehavior *pBehavior = GetQueueBehaviorForNPC( pNPC ); pBehavior->StopBusying(); // Find any node farther back in the queue that isn't player blocked for ( int iNext = iStartingNode+1; iNext < MAX_QUEUE_NODES; iNext++ ) { if ( !m_bPlayerBlockedNodes[iNext] ) { // Kick off any NPCs on the node we're about to steal CAI_BaseNPC *pTargetNPC = GetNPCOnNode(iNext); if ( pTargetNPC ) { CAI_ActBusyBehavior *pTargetBehavior = GetQueueBehaviorForNPC( pTargetNPC ); pTargetBehavior->StopBusying(); } // Force the NPC to move up to the empty slot pBehavior->ForceActBusy( this, m_hNodes[iNext] ); // Now look for a spot for the npc who's spot we've just stolen pNPC = pTargetNPC; iStartingNode = iNext; break; } } } } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::InputPlayerStoppedBlocking( inputdata_t &inputdata ) { int iNode = inputdata.value.Int() - 1; Assert( iNode >= 0 && iNode < MAX_QUEUE_NODES ); m_bPlayerBlockedNodes[iNode] = false; RecalculateQueueCount(); MoveQueueUp(); } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::InputMoveQueueUp( inputdata_t &inputdata ) { // Find the first NPC in the queue CAI_BaseNPC *pNPC = NULL; for ( int i = 0; i < MAX_QUEUE_NODES; i++ ) { pNPC = GetNPCOnNode(i); if ( pNPC ) { CAI_ActBusyBehavior *pBehavior = GetQueueBehaviorForNPC( pNPC ); // If we're still en-route, we're only allowed to leave if the queue // is allowed to send NPCs away that haven't reached the front. if ( !pBehavior->IsMovingToBusy() || !m_bForceReachFront ) break; pNPC = NULL; } // If queue members have to reach the front of the queue, // break after trying the first node. if ( m_bForceReachFront ) break; } // Did we find an NPC? if ( pNPC ) { // Make them leave the actbusy CAI_ActBusyBehavior *pBehavior = GetQueueBehaviorForNPC( pNPC ); pBehavior->Disable(); m_hExitingNPC = pNPC; } } //----------------------------------------------------------------------------- // Purpose: // Input : *pNPC - //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::NPCMovingToBusy( CAI_BaseNPC *pNPC ) { BaseClass::NPCMovingToBusy( pNPC ); RecalculateQueueCount(); } //----------------------------------------------------------------------------- // Purpose: // Input : *pNPC - //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::NPCStartedBusy( CAI_BaseNPC *pNPC ) { BaseClass::NPCStartedBusy( pNPC ); MoveQueueUp(); } //----------------------------------------------------------------------------- // Purpose: Start a short timer that'll clean up holes in the queue //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::MoveQueueUp( void ) { // Find the node the NPC has arrived at, and tell the guy behind him to move forward if ( GetNextThink( QUEUE_MOVEUP_THINK_CONTEXT ) < gpGlobals->curtime ) { float flTime = gpGlobals->curtime + RandomFloat( 0.3, 0.5 ); SetContextThink( &CAI_ActBusyQueueGoal::MoveQueueUpThink, flTime, QUEUE_MOVEUP_THINK_CONTEXT ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::MoveQueueUpThink( void ) { // Find empty holes in the queue, and move NPCs past them forward for ( int iEmptyNode = 0; iEmptyNode < (MAX_QUEUE_NODES-1); iEmptyNode++ ) { if ( !NodeIsOccupied(iEmptyNode) && !m_bPlayerBlockedNodes[iEmptyNode] ) { // Look for NPCs farther down the queue, but not on the other side of a player for ( int iNext = iEmptyNode+1; iNext < MAX_QUEUE_NODES; iNext++ ) { // Is the player blocking this node? If so, we're done if ( m_bPlayerBlockedNodes[iNext] ) break; CAI_BaseNPC *pNPC = GetNPCOnNode(iNext); if ( pNPC ) { CAI_ActBusyBehavior *pBehavior = GetQueueBehaviorForNPC( pNPC ); // Force the NPC to move up to the empty slot pBehavior->ForceActBusy( this, m_hNodes[iEmptyNode] ); break; } } } } } //----------------------------------------------------------------------------- // Purpose: // Input : *pNPC - //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::NPCAbortedMoveTo( CAI_BaseNPC *pNPC ) { BaseClass::NPCAbortedMoveTo( pNPC ); RemoveNPCFromQueue( pNPC ); } //----------------------------------------------------------------------------- // Purpose: // Input : *pNPC - //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::NPCFinishedBusy( CAI_BaseNPC *pNPC ) { BaseClass::NPCFinishedBusy( pNPC ); // If this NPC was at the head of the line, move him to the exit node if ( m_hExitingNPC == pNPC ) { pNPC->ScheduledMoveToGoalEntity( SCHED_IDLE_WALK, m_hExitNode, ACT_WALK ); m_OnNPCLeftQueue.Set( pNPC, pNPC, this ); m_hExitingNPC = NULL; } RemoveNPCFromQueue( pNPC ); } //----------------------------------------------------------------------------- // Purpose: // Input : *pNPC - //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::NPCStartedLeavingBusy( CAI_BaseNPC *pNPC ) { BaseClass::NPCStartedLeavingBusy( pNPC ); // If this NPC it at the head of the line, fire the output if ( m_hExitingNPC == pNPC ) { m_OnNPCStartedLeavingQueue.Set( pNPC, pNPC, this ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::RemoveNPCFromQueue( CAI_BaseNPC *pNPC ) { RecalculateQueueCount(); // Find the node the NPC was heading to, and tell the guy behind him to move forward MoveQueueUp(); } //----------------------------------------------------------------------------- // Purpose: Move the first NPC out of the queue //----------------------------------------------------------------------------- void CAI_ActBusyQueueGoal::QueueThink( void ) { if ( !GetNPCOnNode(0) ) { MoveQueueUp(); } SetContextThink( &CAI_ActBusyQueueGoal::QueueThink, gpGlobals->curtime + 5, QUEUE_THINK_CONTEXT ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- inline bool CAI_ActBusyQueueGoal::NodeIsOccupied( int i ) { return ( m_hNodes[i] && !m_hNodes[i]->IsDisabled() && m_hNodes[i]->IsLocked() ); } //----------------------------------------------------------------------------- // Purpose: // Input : iNode - // Output : CAI_BaseNPC //----------------------------------------------------------------------------- CAI_BaseNPC *CAI_ActBusyQueueGoal::GetNPCOnNode( int iNode ) { if ( !m_hNodes[iNode] ) return NULL; return dynamic_cast(m_hNodes[iNode]->User()); } //----------------------------------------------------------------------------- // Purpose: // Input : iNode - // Output : CAI_ActBusyBehavior //----------------------------------------------------------------------------- CAI_ActBusyBehavior *CAI_ActBusyQueueGoal::GetQueueBehaviorForNPC( CAI_BaseNPC *pNPC ) { CAI_ActBusyBehavior *pBehavior; pNPC->GetBehavior( &pBehavior ); Assert( pBehavior ); return pBehavior; }