|
|
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: Bullseyes act as targets for other NPC's to attack and to trigger
// events
//
// $NoKeywords: $
//=============================================================================//
#include "cbase.h"
#include "basecombatcharacter.h"
#include "ai_basenpc.h"
#include "decals.h"
#include "IEffects.h"
#include "ai_squad.h"
#include "ai_utils.h"
#include "ai_senses.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
#define SF_ENEMY_FINDER_CHECK_VIS (1 << 16)
#define SF_ENEMY_FINDER_APC_VIS (1 << 17)
#define SF_ENEMY_FINDER_SHORT_MEMORY (1 << 18)
#define SF_ENEMY_FINDER_ENEMY_ALLOWED (1 << 19)
ConVar ai_debug_enemyfinders( "ai_debug_enemyfinders", "0" );
class CNPC_EnemyFinder : public CAI_BaseNPC { public: DECLARE_CLASS( CNPC_EnemyFinder, CAI_BaseNPC );
CNPC_EnemyFinder() { m_PlayerFreePass.SetOuter( this ); }
void Precache( void ); void Spawn( void ); void StartNPC ( void ); void PrescheduleThink(); bool ShouldAlwaysThink(); void UpdateEfficiency( bool bInPVS ) { SetEfficiency( ( GetSleepState() != AISS_AWAKE ) ? AIE_DORMANT : AIE_NORMAL ); SetMoveEfficiency( AIME_NORMAL ); } void GatherConditions( void ); bool ShouldChooseNewEnemy(); bool IsValidEnemy( CBaseEntity *pTarget ); bool CanBeAnEnemyOf( CBaseEntity *pEnemy ) { return HasSpawnFlags( SF_ENEMY_FINDER_ENEMY_ALLOWED ); } bool FVisible( CBaseEntity *pEntity, int traceMask, CBaseEntity **ppBlocker ); Class_T Classify( void ); bool CanBeSeenBy( CAI_BaseNPC *pNPC ) { return CanBeAnEnemyOf( pNPC ); } // allows entities to be 'invisible' to NPC senses.
virtual int SelectSchedule( void ); virtual void DrawDebugGeometryOverlays( void );
// Input handlers.
void InputTurnOn( inputdata_t &inputdata ); void InputTurnOff( inputdata_t &inputdata );
virtual void Wake( bool bFireOutput = true );
private: int m_nStartOn; float m_flMinSearchDist; float m_flMaxSearchDist; CAI_FreePass m_PlayerFreePass; CSimpleSimTimer m_ChooseEnemyTimer;
bool m_bEnemyStatus;
COutputEvent m_OnLostEnemies; COutputEvent m_OnAcquireEnemies;
DECLARE_DATADESC(); DEFINE_CUSTOM_AI; };
LINK_ENTITY_TO_CLASS( npc_enemyfinder, CNPC_EnemyFinder );
//-----------------------------------------------------------------------------
// Custom schedules.
//-----------------------------------------------------------------------------
enum { SCHED_EFINDER_SEARCH = LAST_SHARED_SCHEDULE, };
IMPLEMENT_CUSTOM_AI( npc_enemyfinder, CNPC_EnemyFinder );
BEGIN_DATADESC( CNPC_EnemyFinder )
DEFINE_EMBEDDED( m_PlayerFreePass ), DEFINE_EMBEDDED( m_ChooseEnemyTimer ),
// Inputs
DEFINE_INPUT( m_nStartOn, FIELD_INTEGER, "StartOn" ), DEFINE_INPUT( m_flFieldOfView, FIELD_FLOAT, "FieldOfView" ), DEFINE_INPUT( m_flMinSearchDist, FIELD_FLOAT, "MinSearchDist" ), DEFINE_INPUT( m_flMaxSearchDist, FIELD_FLOAT, "MaxSearchDist" ),
DEFINE_FIELD( m_bEnemyStatus, FIELD_BOOLEAN ),
DEFINE_INPUTFUNC( FIELD_VOID, "TurnOn", InputTurnOn ), DEFINE_INPUTFUNC( FIELD_VOID, "TurnOff", InputTurnOff ),
DEFINE_OUTPUT( m_OnLostEnemies, "OnLostEnemies"), DEFINE_OUTPUT( m_OnAcquireEnemies, "OnAcquireEnemies"),
END_DATADESC()
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_EnemyFinder::InitCustomSchedules( void ) { INIT_CUSTOM_AI( CNPC_EnemyFinder );
ADD_CUSTOM_SCHEDULE( CNPC_EnemyFinder, SCHED_EFINDER_SEARCH ); AI_LOAD_SCHEDULE( CNPC_EnemyFinder, SCHED_EFINDER_SEARCH ); }
//-----------------------------------------------------------------------------
// Purpose: Input handler for turning the enemy finder on.
//-----------------------------------------------------------------------------
void CNPC_EnemyFinder::InputTurnOn( inputdata_t &inputdata ) { SetThink( &CNPC_EnemyFinder::CallNPCThink ); SetNextThink( gpGlobals->curtime ); }
//-----------------------------------------------------------------------------
// Purpose: Input handler for turning the enemy finder off.
//-----------------------------------------------------------------------------
void CNPC_EnemyFinder::InputTurnOff( inputdata_t &inputdata ) { SetThink(NULL); }
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_EnemyFinder::Precache( void ) { PrecacheModel( "models/player.mdl" ); BaseClass::Precache(); }
//-----------------------------------------------------------------------------
// Purpose:
//
//
//-----------------------------------------------------------------------------
void CNPC_EnemyFinder::Spawn( void ) { Precache();
SetModel( "models/player.mdl" ); // This is a dummy model that is never used!
UTIL_SetSize(this, vec3_origin, vec3_origin);
SetMoveType( MOVETYPE_NONE ); SetBloodColor( DONT_BLEED ); SetGravity( 0.0 ); m_iHealth = 1; AddFlag( FL_NPC );
SetSolid( SOLID_NONE );
m_bEnemyStatus = false;
if (m_flFieldOfView < -1.0) { DevMsg("ERROR: EnemyFinder field of view must be between -1.0 and 1.0\n"); m_flFieldOfView = 0.5; } else if (m_flFieldOfView > 1.0) { DevMsg("ERROR: EnemyFinder field of view must be between -1.0 and 1.0\n"); m_flFieldOfView = 1.0; } CapabilitiesAdd ( bits_CAP_SQUAD );
NPCInit();
// Set this after NPCInit()
m_takedamage = DAMAGE_NO; AddEffects( EF_NODRAW ); m_NPCState = NPC_STATE_ALERT; // always alert
SetViewOffset( vec3_origin ); if ( m_flMaxSearchDist ) { SetDistLook( m_flMaxSearchDist ); }
if ( HasSpawnFlags( SF_ENEMY_FINDER_SHORT_MEMORY ) ) { GetEnemies()->SetEnemyDiscardTime( 0.2 ); } }
//-----------------------------------------------------------------------------
// Purpose:
// Output :
//-----------------------------------------------------------------------------
int CNPC_EnemyFinder::SelectSchedule( void ) { return SCHED_EFINDER_SEARCH; }
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void CNPC_EnemyFinder::Wake( bool bFireOutput ) { BaseClass::Wake( bFireOutput );
//Enemy finder is not allowed to become visible.
AddEffects( EF_NODRAW ); }
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
bool CNPC_EnemyFinder::FVisible( CBaseEntity *pTarget, int traceMask, CBaseEntity **ppBlocker ) { float flTargetDist = GetAbsOrigin().DistTo( pTarget->GetAbsOrigin() ); if ( flTargetDist < m_flMinSearchDist) return false;
if ( m_flMaxSearchDist && flTargetDist > m_flMaxSearchDist) return false;
if ( !FBitSet( m_spawnflags, SF_ENEMY_FINDER_CHECK_VIS) ) return true;
if ( !HasSpawnFlags(SF_ENEMY_FINDER_APC_VIS) ) { bool bIsVisible = BaseClass::FVisible( pTarget, traceMask, ppBlocker ); if ( bIsVisible && pTarget == m_PlayerFreePass.GetPassTarget() ) bIsVisible = m_PlayerFreePass.ShouldAllowFVisible( bIsVisible );
return bIsVisible; }
// Make sure I can see the target from my position
trace_t tr;
// Trace from launch position to target position.
// Use position above actual barral based on vertical launch speed
Vector vStartPos = GetAbsOrigin(); Vector vEndPos = pTarget->EyePosition();
CBaseEntity *pVehicle = NULL; if ( pTarget->IsPlayer() ) { CBasePlayer *pPlayer = assert_cast<CBasePlayer*>(pTarget); pVehicle = pPlayer->GetVehicleEntity(); }
CTraceFilterSkipTwoEntities traceFilter( pTarget, pVehicle, COLLISION_GROUP_NONE ); AI_TraceLine( vStartPos, vEndPos, MASK_SHOT, &traceFilter, &tr ); if ( ppBlocker ) { *ppBlocker = tr.m_pEnt; } return (tr.fraction == 1.0); }
//------------------------------------------------------------------------------
bool CNPC_EnemyFinder::ShouldChooseNewEnemy() { if ( m_ChooseEnemyTimer.Expired() ) { m_ChooseEnemyTimer.Set( 0.3 ); return true; } return false; }
//------------------------------------------------------------------------------
// Purpose : Override base class to check range and visibility
// Input :
// Output :
//------------------------------------------------------------------------------
bool CNPC_EnemyFinder::IsValidEnemy( CBaseEntity *pTarget ) { float flTargetDist = GetAbsOrigin().DistTo( pTarget->GetAbsOrigin() ); if (flTargetDist < m_flMinSearchDist) return false;
if ( m_flMaxSearchDist && flTargetDist > m_flMaxSearchDist) return false;
if ( !FBitSet( m_spawnflags, SF_ENEMY_FINDER_CHECK_VIS) ) return true;
if ( GetSenses()->DidSeeEntity( pTarget ) ) return true;
// Trace from launch position to target position.
// Use position above actual barral based on vertical launch speed
Vector vStartPos = GetAbsOrigin(); Vector vEndPos = pTarget->EyePosition();
// Test our line of sight to the target
trace_t tr; AI_TraceLOS( vStartPos, vEndPos, this, &tr );
// If the player is in a vehicle, see if we can see that instead
if ( pTarget->IsPlayer() ) { CBasePlayer *pPlayer = assert_cast<CBasePlayer*>(pTarget); if ( tr.m_pEnt == pPlayer->GetVehicleEntity() ) return true; }
// Line must be clear
if ( tr.fraction == 1.0f || tr.m_pEnt == pTarget ) return true;
// Otherwise we can't see anything
return false; }
//------------------------------------------------------------------------------
// Purpose :
// Input :
// Output :
//------------------------------------------------------------------------------
void CNPC_EnemyFinder::StartNPC ( void ) { AddSpawnFlags(SF_NPC_FALL_TO_GROUND); // this prevents CAI_BaseNPC from slamming the finder to
// the ground just because it's not MOVETYPE_FLY
BaseClass::StartNPC();
if ( AI_IsSinglePlayer() && m_PlayerFreePass.GetParams().duration > 0.1 ) { m_PlayerFreePass.SetPassTarget( UTIL_PlayerByIndex(1) );
AI_FreePassParams_t freePassParams = m_PlayerFreePass.GetParams();
freePassParams.coverDist = 120; freePassParams.peekEyeDist = 1.75; freePassParams.peekEyeDistZ = 4;
m_PlayerFreePass.SetParams( freePassParams ); }
if (!m_nStartOn) { SetThink(NULL); } }
//------------------------------------------------------------------------------
void CNPC_EnemyFinder::PrescheduleThink() { BaseClass::PrescheduleThink();
bool bHasEnemies = GetEnemies()->NumEnemies() > 0; if ( GetEnemies()->NumEnemies() > 0 ) { //If I haven't seen my enemy in half a second then we'll assume he's gone.
if ( gpGlobals->curtime - GetEnemyLastTimeSeen() >= 0.5f ) { bHasEnemies = false; } }
if ( m_bEnemyStatus != bHasEnemies ) { if ( bHasEnemies ) { m_OnAcquireEnemies.FireOutput( this, this ); } else { m_OnLostEnemies.FireOutput( this, this ); } m_bEnemyStatus = bHasEnemies; }
if( ai_debug_enemyfinders.GetBool() ) { m_debugOverlays |= OVERLAY_BBOX_BIT;
if( IsInSquad() && GetSquad()->NumMembers() > 1 ) { AISquadIter_t iter; CAI_BaseNPC *pSquadmate = m_pSquad ? m_pSquad->GetFirstMember( &iter ) : NULL; while ( pSquadmate ) { NDebugOverlay::Line( WorldSpaceCenter(), pSquadmate->EyePosition(), 255, 255, 0, false, 0.1f ); pSquadmate = m_pSquad->GetNextMember( &iter ); } } } }
//------------------------------------------------------------------------------
bool CNPC_EnemyFinder::ShouldAlwaysThink() { if ( BaseClass::ShouldAlwaysThink() ) return true; CBasePlayer *pPlayer = AI_GetSinglePlayer(); if ( pPlayer && IRelationType( pPlayer ) == D_HT ) { float playerDistSqr = GetAbsOrigin().DistToSqr( pPlayer->GetAbsOrigin() );
if ( !m_flMaxSearchDist || playerDistSqr <= Square(m_flMaxSearchDist) ) { if ( !FBitSet( m_spawnflags, SF_ENEMY_FINDER_CHECK_VIS) ) return true; if ( playerDistSqr <= Square( 50 * 12 ) ) return true; } } return false; }
//------------------------------------------------------------------------------
// Purpose :
// Input :
// Output :
//------------------------------------------------------------------------------
void CNPC_EnemyFinder::GatherConditions() { // This works with old data because need to do before base class so as to not choose as enemy
m_PlayerFreePass.Update(); BaseClass::GatherConditions(); }
//-----------------------------------------------------------------------------
// Purpose:
//
//
// Output :
//-----------------------------------------------------------------------------
Class_T CNPC_EnemyFinder::Classify( void ) { if ( GetSquad() ) { AISquadIter_t iter; CAI_BaseNPC *pSquadmate = GetSquad()->GetFirstMember( &iter ); while ( pSquadmate ) { if ( pSquadmate != this && !pSquadmate->ClassMatches( GetClassname() ) ) { return pSquadmate->Classify(); } pSquadmate = GetSquad()->GetNextMember( &iter ); } }
return CLASS_NONE; }
//-----------------------------------------------------------------------------
// Purpose: Add a visualizer to the text, if turned on
//-----------------------------------------------------------------------------
void CNPC_EnemyFinder::DrawDebugGeometryOverlays( void ) { // Turn on npc_relationships if we're displaying text
int oldDebugOverlays = m_debugOverlays; if ( m_debugOverlays & OVERLAY_TEXT_BIT ) { m_debugOverlays |= OVERLAY_NPC_RELATION_BIT; }
// Draw our base overlays
BaseClass::DrawDebugGeometryOverlays();
// Restore the old values
m_debugOverlays = oldDebugOverlays; }
ConVar ai_ef_hate_npc_frequency( "ai_ef_hate_npc_frequency", "5" ); ConVar ai_ef_hate_npc_duration( "ai_ef_hate_npc_duration", "1.5" );
//-----------------------------------------------------------------------------
// Derived class with a few changes that make the Combine Cannon behave the
// way we want.
//-----------------------------------------------------------------------------
#define EF_COMBINE_CANNON_HATE_TIME_INVALID -1
static CUtlVector<CBaseEntity*> s_ListEnemyfinders;
class CNPC_EnemyFinderCombineCannon : public CNPC_EnemyFinder { public: DECLARE_CLASS( CNPC_EnemyFinderCombineCannon, CNPC_EnemyFinder ); DECLARE_DATADESC();
CNPC_EnemyFinderCombineCannon() { m_flTimeNextHateNPC = gpGlobals->curtime; m_flTimeStopHateNPC = EF_COMBINE_CANNON_HATE_TIME_INVALID; };
public: void Spawn(); void Activate(); void UpdateOnRemove(); bool FVisible( CBaseEntity *pEntity, int traceMask, CBaseEntity **ppBlocker ); bool IsValidEnemy( CBaseEntity *pTarget ); void GatherConditions();
void InputSetWideFOVForSeconds( inputdata_t &inputdata );
public: float m_flTimeNextHateNPC; float m_flTimeStopHateNPC; float m_flOriginalFOV; float m_flTimeWideFOV; // If this is > gpGlobals->curtime, we have 180 degree viewcone.
string_t m_iszSnapToEnt; }; LINK_ENTITY_TO_CLASS( npc_enemyfinder_combinecannon, CNPC_EnemyFinderCombineCannon );
BEGIN_DATADESC( CNPC_EnemyFinderCombineCannon ) DEFINE_FIELD( m_flTimeNextHateNPC, FIELD_TIME ), DEFINE_FIELD( m_flTimeStopHateNPC, FIELD_TIME ), DEFINE_FIELD( m_flOriginalFOV, FIELD_FLOAT ), DEFINE_FIELD( m_flTimeWideFOV, FIELD_TIME ), DEFINE_KEYFIELD( m_iszSnapToEnt, FIELD_STRING, "snaptoent" ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetWideFOVForSeconds", InputSetWideFOVForSeconds ), END_DATADESC()
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_EnemyFinderCombineCannon::Spawn() { BaseClass::Spawn(); m_flOriginalFOV = m_flFieldOfView; m_flTimeWideFOV = -1.0f;
if( m_iszSnapToEnt != NULL_STRING ) { CBaseEntity *pSnapToEnt = gEntList.FindEntityByName( NULL, m_iszSnapToEnt );
if( pSnapToEnt != NULL ) { //!!!HACKHACK - this eight-inch offset puts this enemyfinder perfectly on-bore
// with the prefab for a func_tank_combinecannon
UTIL_SetOrigin( this, pSnapToEnt->WorldSpaceCenter() + Vector( 0, 0, 8) ); } else { DevMsg( this, "Enemyfinder %s can't snap to %s because it doesn't exist\n", GetDebugName(), STRING(m_iszSnapToEnt) ); } } }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_EnemyFinderCombineCannon::Activate() { BaseClass::Activate();
// See if I'm in the list of Combine enemyfinders
// If not, add me.
if( s_ListEnemyfinders.Find(this) == -1 ) { s_ListEnemyfinders.AddToTail(this); } }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_EnemyFinderCombineCannon::UpdateOnRemove() { BaseClass::UpdateOnRemove();
// See if I'm in the list of Combine enemyfinders
int index = s_ListEnemyfinders.Find(this); if( index != -1 ) { s_ListEnemyfinders.Remove(index); } }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
bool CNPC_EnemyFinderCombineCannon::FVisible( CBaseEntity *pEntity, int traceMask, CBaseEntity **ppBlocker ) { #if 1
CBaseEntity *pBlocker = NULL; bool result;
if(ppBlocker == NULL) { // Whoever called this didn't care about the blocker, but we do.
// So substitute our local pBlocker pointer and don't disturb ppBlocker
result = BaseClass::FVisible( pEntity, traceMask, &pBlocker ); } else { // Copy the ppBlocker to our local pBlocker pointer, but do not
// disturb the ppBlocker that was passed to us.
result = BaseClass::FVisible( pEntity, traceMask, ppBlocker ); pBlocker = (*ppBlocker); }
if(pEntity->IsPlayer() && result == false) { // IF we are trying to see the player, but we don't, examine the blocker
// and see the player anyway if we can hurt the blocker.
if(pBlocker != NULL) { if( pBlocker->m_takedamage >= DAMAGE_YES ) // also DAMAGE_AIM
{ // Anytime the line of sight is blocked by something I can hurt, I have line of sight.
// This will make the func_tank_combinecannon shoot the blocking object. This will
// continue until the gun bores through to the player or clears all interposing breakables
// and finds its progress impeded by something truly solid. So lie, and say we CAN see the player.
result = true; } } }
return result; #endif
}
//-----------------------------------------------------------------------------
// Purpose: Ignore NPC's most of the time when the player is a potential target.
// Go through short periods of time where NPCs may distract me
//
// ALSO- ignore NPC's (focus only on the player) when I'm in
// wide viewcone mode.
//-----------------------------------------------------------------------------
bool CNPC_EnemyFinderCombineCannon::IsValidEnemy( CBaseEntity *pTarget ) { if( m_flTimeWideFOV > gpGlobals->curtime && !pTarget->IsPlayer() ) { // ONLY allowed to hate the player when I'm in hyper-vigilant wide FOV mode.
// This forces all guns in outland_09 to shoot at the player once any
// gun has fired at the player. This allows the other guns to specifically
// kill zombies most of the time, but immediately turn their attention to the
// player when necessary (by ignoring everything else)
return pTarget->IsPlayer(); }
bool bResult = BaseClass::IsValidEnemy( pTarget );
if( bResult && !pTarget->IsPlayer() ) { // This is a valid enemy, but we have to make sure no other enemyfinders for
// combine cannons are currently attacking it.
int i; for( i = 0 ; i < s_ListEnemyfinders.Count() ; i++ ) { if( s_ListEnemyfinders[i] == this ) continue;
if( s_ListEnemyfinders[i]->GetEnemy() == pTarget ) return false;// someone else is already busy with this target.
} }
/*
CBasePlayer *pPlayer = AI_GetSinglePlayer(); int iPlayerRelationPriority = -1;
if( pPlayer != NULL ) { iPlayerRelationPriority = IRelationPriority(pPlayer); }
if( bResult == true && pTarget->IsNPC() && pPlayer != NULL && FInViewCone( pPlayer ) ) { if( HasCondition(COND_SEE_PLAYER) ) { // The player is visible! Immediately ignore all NPCs as enemies.
return false; }
// The base class wants to call this a valid enemy. We may choose to interfere
// If the player is in my viewcone. That means that my func_tank could potentially
// harass the player. This means I should meter the time I spend shooting at npcs
// NPCs so that I can focus on the player.
if( m_flTimeStopHateNPC != EF_COMBINE_CANNON_HATE_TIME_INVALID ) { // We currently hate NPC's. But is it time to stop?
if( gpGlobals->curtime > m_flTimeStopHateNPC ) { // Don't interfere with the result
m_flTimeStopHateNPC = EF_COMBINE_CANNON_HATE_TIME_INVALID; m_flTimeNextHateNPC = gpGlobals->curtime + ai_ef_hate_npc_frequency.GetFloat(); return bResult; } } else { // We do not hate NPCs at the moment. Is it time to turn it on?
if( gpGlobals->curtime > m_flTimeNextHateNPC ) { m_flTimeStopHateNPC = gpGlobals->curtime + ai_ef_hate_npc_duration.GetFloat(); } else { // Stop harassing player to attack something else higher priority.
if( IRelationPriority(pTarget) > iPlayerRelationPriority ) return bResult; }
// Make this enemy invalid.
return false; } } */
return bResult; }
//-----------------------------------------------------------------------------
// Purpose: Control the width of my viewcone
//-----------------------------------------------------------------------------
void CNPC_EnemyFinderCombineCannon::GatherConditions() { if( m_flTimeWideFOV > gpGlobals->curtime ) { // I'm in a hyper-vigilant period of time where I get a 270 degree viewcone
m_flFieldOfView = -0.5f; } else { m_flFieldOfView = m_flOriginalFOV; }
BaseClass::GatherConditions(); }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_EnemyFinderCombineCannon::InputSetWideFOVForSeconds( inputdata_t &inputdata ) { m_flTimeWideFOV = gpGlobals->curtime + inputdata.value.Float(); }
//-----------------------------------------------------------------------------
//
// Schedules
//
//-----------------------------------------------------------------------------
//=========================================================
// > SCHED_EFINDER_SEARCH
//=========================================================
AI_DEFINE_SCHEDULE ( SCHED_EFINDER_SEARCH,
" Tasks" " TASK_WAIT_RANDOM 0.5 " " " " Interrupts" " COND_NEW_ENEMY" );
|