// NextBotCombatCharacter.cpp // Next generation bot system // Author: Michael Booth, April 2005 //========= Copyright Valve Corporation, All rights reserved. ============// #include "cbase.h" #include "team.h" #include "CRagdollMagnet.h" #include "NextBot.h" #include "NextBotLocomotionInterface.h" #include "NextBotBodyInterface.h" #ifdef TERROR #include "TerrorGamerules.h" #endif #include "vprof.h" #include "datacache/imdlcache.h" #include "EntityFlame.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" ConVar NextBotStop( "nb_stop", "0", FCVAR_CHEAT | FCVAR_REPLICATED, "Stop all NextBots" ); //-------------------------------------------------------------------------------------------------------- class CSendBotCommand { public: CSendBotCommand( const char *command ) { m_command = command; } bool operator() ( INextBot *bot ) { bot->OnCommandString( m_command ); return true; } const char *m_command; }; CON_COMMAND_F( nb_command, "Sends a command string to all bots", FCVAR_CHEAT ) { if ( args.ArgC() <= 1 ) { Msg( "Missing command string" ); return; } CSendBotCommand sendCmd( args.ArgS() ); TheNextBots().ForEachBot( sendCmd ); } //----------------------------------------------------------------------------------------------------- BEGIN_DATADESC( NextBotCombatCharacter ) DEFINE_THINKFUNC( DoThink ), END_DATADESC() //----------------------------------------------------------------------------------------------------- IMPLEMENT_SERVERCLASS_ST( NextBotCombatCharacter, DT_NextBot ) END_SEND_TABLE() //----------------------------------------------------------------------------------------------------- NextBotDestroyer::NextBotDestroyer( int team ) { m_team = team; } //----------------------------------------------------------------------------------------------------- bool NextBotDestroyer::operator() ( INextBot *bot ) { if ( m_team == TEAM_ANY || bot->GetEntity()->GetTeamNumber() == m_team ) { // players need to be kicked, not deleted if ( bot->GetEntity()->IsPlayer() ) { CBasePlayer *player = dynamic_cast< CBasePlayer * >( bot->GetEntity() ); engine->ServerCommand( UTIL_VarArgs( "kickid %d\n", player->GetUserID() ) ); } else { UTIL_Remove( bot->GetEntity() ); } } return true; } //----------------------------------------------------------------------------------------------------- CON_COMMAND_F( nb_delete_all, "Delete all non-player NextBot entities.", FCVAR_CHEAT ) { // Listenserver host or rcon access only! if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; CTeam *team = NULL; if ( args.ArgC() == 2 ) { const char *teamName = args[1]; for( int i=0; i < g_Teams.Count(); ++i ) { if ( FStrEq( teamName, g_Teams[i]->GetName() ) ) { // delete all bots on this team team = g_Teams[i]; break; } } if ( team == NULL ) { Msg( "Invalid team '%s'\n", teamName ); return; } } // delete all bots on all teams NextBotDestroyer destroyer( team ? team->GetTeamNumber() : TEAM_ANY ); TheNextBots().ForEachBot( destroyer ); } //----------------------------------------------------------------------------------------------------- class NextBotApproacher { public: NextBotApproacher( void ) { CBasePlayer *player = UTIL_GetListenServerHost(); if ( player ) { Vector forward; player->EyeVectors( &forward ); trace_t result; unsigned int mask = MASK_BLOCKLOS_AND_NPCS|CONTENTS_IGNORE_NODRAW_OPAQUE | CONTENTS_GRATE | CONTENTS_WINDOW; UTIL_TraceLine( player->EyePosition(), player->EyePosition() + 999999.9f * forward, mask, player, COLLISION_GROUP_NONE, &result ); if ( result.DidHit() ) { NDebugOverlay::Cross3D( result.endpos, 5, 0, 255, 0, true, 10.0f ); m_isGoalValid = true; m_goal = result.endpos; } else { m_isGoalValid = false; } } } bool operator() ( INextBot *bot ) { if ( TheNextBots().IsDebugFilterMatch( bot ) ) { bot->OnCommandApproach( m_goal ); } return true; } bool m_isGoalValid; Vector m_goal; }; CON_COMMAND_F( nb_move_to_cursor, "Tell all NextBots to move to the cursor position", FCVAR_CHEAT ) { // Listenserver host or rcon access only! if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; NextBotApproacher approach; TheNextBots().ForEachBot( approach ); } //---------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------- bool IgnoreActorsTraceFilterFunction( IHandleEntity *pServerEntity, int contentsMask ) { CBaseEntity *entity = EntityFromEntityHandle( pServerEntity ); return ( entity->MyCombatCharacterPointer() == NULL ); // includes all bots, npcs, players, and TF2 buildings } //---------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------- bool VisionTraceFilterFunction( IHandleEntity *pServerEntity, int contentsMask ) { // Honor BlockLOS also to allow seeing through partially-broken doors CBaseEntity *entity = EntityFromEntityHandle( pServerEntity ); return ( entity->MyCombatCharacterPointer() == NULL && entity->BlocksLOS() ); } //---------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------- NextBotCombatCharacter::NextBotCombatCharacter( void ) { m_lastAttacker = NULL; m_didModelChange = false; } //---------------------------------------------------------------------------------------------------------- void NextBotCombatCharacter::Spawn( void ) { BaseClass::Spawn(); // reset bot components Reset(); SetSolid( SOLID_BBOX ); AddSolidFlags( FSOLID_NOT_STANDABLE ); SetMoveType( MOVETYPE_CUSTOM ); SetCollisionGroup( COLLISION_GROUP_PLAYER ); m_iMaxHealth = m_iHealth; m_takedamage = DAMAGE_YES; MDLCACHE_CRITICAL_SECTION(); InitBoneControllers( ); // set up think callback SetThink( &NextBotCombatCharacter::DoThink ); SetNextThink( gpGlobals->curtime ); m_lastAttacker = NULL; } bool NextBotCombatCharacter::IsAreaTraversable( const CNavArea *area ) const { if ( !area ) return false; ILocomotion *mover = GetLocomotionInterface(); if ( mover && !mover->IsAreaTraversable( area ) ) return false; return BaseClass::IsAreaTraversable( area ); } //---------------------------------------------------------------------------------------------------------- void NextBotCombatCharacter::DoThink( void ) { VPROF_BUDGET( "NextBotCombatCharacter::DoThink", "NextBot" ); SetNextThink( gpGlobals->curtime ); if ( BeginUpdate() ) { // emit model change event if ( m_didModelChange ) { m_didModelChange = false; OnModelChanged(); // propagate model change into NextBot event responders for ( INextBotEventResponder *sub = FirstContainedResponder(); sub; sub = NextContainedResponder( sub ) ) { sub->OnModelChanged(); } } UpdateLastKnownArea(); // update bot components if ( !NextBotStop.GetBool() && (GetFlags() & FL_FROZEN) == 0 ) { Update(); } EndUpdate(); } } //---------------------------------------------------------------------------------------------------------- void NextBotCombatCharacter::Touch( CBaseEntity *other ) { if ( ShouldTouch( other ) ) { // propagate touch into NextBot event responders trace_t result; result = GetTouchTrace(); // OnContact refers to *physical* contact, not triggers or other non-physical entities if ( result.DidHit() || other->MyCombatCharacterPointer() != NULL ) { OnContact( other, &result ); } } BaseClass::Touch( other ); } //---------------------------------------------------------------------------------------------------------- void NextBotCombatCharacter::SetModel( const char *szModelName ) { // actually change the model BaseClass::SetModel( szModelName ); // need to do a lazy-check because precache system also invokes this m_didModelChange = true; } //---------------------------------------------------------------------------------------------------------- void NextBotCombatCharacter::Ignite( float flFlameLifetime, bool bNPCOnly, float flSize, bool bCalledByLevelDesigner ) { BaseClass::Ignite( flFlameLifetime, bNPCOnly, flSize, bCalledByLevelDesigner ); // propagate event to components OnIgnite(); } //---------------------------------------------------------------------------------------------------------- void NextBotCombatCharacter::Ignite( float flFlameLifetime, CBaseEntity *pAttacker ) { if ( IsOnFire() ) return; // BaseClass::Ignite stuff, plus SetAttacker on the flame, so our attacker gets credit CEntityFlame *pFlame = CEntityFlame::Create( this ); if ( pFlame ) { pFlame->SetLifetime( flFlameLifetime ); AddFlag( FL_ONFIRE ); SetEffectEntity( pFlame ); } m_OnIgnite.FireOutput( this, this ); // propagate event to components OnIgnite(); } //---------------------------------------------------------------------------------------------------------- int NextBotCombatCharacter::OnTakeDamage_Alive( const CTakeDamageInfo &info ) { // track our last attacker if ( info.GetAttacker() && info.GetAttacker()->MyCombatCharacterPointer() ) { m_lastAttacker = info.GetAttacker()->MyCombatCharacterPointer(); } // propagate event to components OnInjured( info ); return CBaseCombatCharacter::OnTakeDamage_Alive( info ); } //---------------------------------------------------------------------------------------------------------- int NextBotCombatCharacter::OnTakeDamage_Dying( const CTakeDamageInfo &info ) { // track our last attacker if ( info.GetAttacker()->MyCombatCharacterPointer() ) { m_lastAttacker = info.GetAttacker()->MyCombatCharacterPointer(); } // propagate event to components OnInjured( info ); return CBaseCombatCharacter::OnTakeDamage_Dying( info ); } //---------------------------------------------------------------------------------------------------------- /** * Can't use CBaseCombatCharacter's Event_Killed because it will immediately ragdoll us */ static int g_DeathStartEvent = 0; void NextBotCombatCharacter::Event_Killed( const CTakeDamageInfo &info ) { // track our last attacker if ( info.GetAttacker() && info.GetAttacker()->MyCombatCharacterPointer() ) { m_lastAttacker = info.GetAttacker()->MyCombatCharacterPointer(); } // propagate event to my components OnKilled( info ); // Advance life state to dying m_lifeState = LIFE_DYING; #ifdef TERROR /* * TODO: Make this game-generic */ // Create the death event just like players do. TerrorGameRules()->DeathNoticeForEntity( this, info ); // Infected specific event TerrorGameRules()->DeathNoticeForInfected( this, info ); #endif if ( GetOwnerEntity() != NULL ) { GetOwnerEntity()->DeathNotice( this ); } // inform the other bots TheNextBots().OnKilled( this, info ); } //---------------------------------------------------------------------------------------------------------- void NextBotCombatCharacter::PerformCustomPhysics( Vector *pNewPosition, Vector *pNewVelocity, QAngle *pNewAngles, QAngle *pNewAngVelocity ) { ILocomotion *mover = GetLocomotionInterface(); if ( mover ) { // hack to keep ground entity from being NULL'd when Z velocity is positive SetGroundEntity( mover->GetGround() ); } } //---------------------------------------------------------------------------------------------------------- bool NextBotCombatCharacter::BecomeRagdoll( const CTakeDamageInfo &info, const Vector &forceVector ) { // See if there's a ragdoll magnet that should influence our force. Vector adjustedForceVector = forceVector; CRagdollMagnet *magnet = CRagdollMagnet::FindBestMagnet( this ); if ( magnet ) { adjustedForceVector += magnet->GetForceVector( this ); } // clear the deceased's sound channels.(may have been firing or reloading when killed) EmitSound( "BaseCombatCharacter.StopWeaponSounds" ); return BaseClass::BecomeRagdoll( info, adjustedForceVector ); } //---------------------------------------------------------------------------------------------------------- void NextBotCombatCharacter::HandleAnimEvent( animevent_t *event ) { // propagate event to components OnAnimationEvent( event ); } //---------------------------------------------------------------------------------------------------------- /** * Propagate event into NextBot event responders */ void NextBotCombatCharacter::OnNavAreaChanged( CNavArea *enteredArea, CNavArea *leftArea ) { INextBotEventResponder::OnNavAreaChanged( enteredArea, leftArea ); BaseClass::OnNavAreaChanged( enteredArea, leftArea ); } //---------------------------------------------------------------------------------------------------------- Vector NextBotCombatCharacter::EyePosition( void ) { if ( GetBodyInterface() ) { return GetBodyInterface()->GetEyePosition(); } return BaseClass::EyePosition(); } //---------------------------------------------------------------------------------------------------------- /** * Return true if this object can be +used by the bot */ bool NextBotCombatCharacter::IsUseableEntity( CBaseEntity *entity, unsigned int requiredCaps ) { if ( entity ) { int caps = entity->ObjectCaps(); if ( caps & (FCAP_IMPULSE_USE|FCAP_CONTINUOUS_USE|FCAP_ONOFF_USE|FCAP_DIRECTIONAL_USE) ) { if ( (caps & requiredCaps) == requiredCaps ) { return true; } } } return false; } //---------------------------------------------------------------------------------------------------------- void NextBotCombatCharacter::UseEntity( CBaseEntity *entity, USE_TYPE useType ) { if ( IsUseableEntity( entity ) ) { variant_t emptyVariant; entity->AcceptInput( "Use", this, this, emptyVariant, useType ); } }