source-engine/game/server/episodic/npc_advisor.cpp
FluorescentCIAAfricanAmerican 3bf9df6b27 1
2020-04-22 12:56:21 -04:00

2052 lines
58 KiB
C++

//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Advisors. Large sluglike aliens with creepy psychic powers!
//
//=============================================================================
#include "cbase.h"
#include "game.h"
#include "ai_basenpc.h"
#include "ai_schedule.h"
#include "ai_hull.h"
#include "ai_hint.h"
#include "ai_motor.h"
#include "ai_navigator.h"
#include "beam_shared.h"
#include "hl2_shareddefs.h"
#include "ai_route.h"
#include "npcevent.h"
#include "gib.h"
#include "ai_interactions.h"
#include "ndebugoverlay.h"
#include "physics_saverestore.h"
#include "saverestore_utlvector.h"
#include "soundent.h"
#include "vstdlib/random.h"
#include "engine/IEngineSound.h"
#include "movevars_shared.h"
#include "particle_parse.h"
#include "weapon_physcannon.h"
// #include "mathlib/noise.h"
// this file contains the definitions for the message ID constants (eg ADVISOR_MSG_START_BEAM etc)
#include "npc_advisor_shared.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//
// Custom activities.
//
//
// Skill settings.
//
ConVar sk_advisor_health( "sk_advisor_health", "0" );
ConVar advisor_use_impact_table("advisor_use_impact_table","1",FCVAR_NONE,"If true, advisor will use her custom impact damage table.");
#if NPC_ADVISOR_HAS_BEHAVIOR
ConVar advisor_throw_velocity( "advisor_throw_velocity", "1100" );
ConVar advisor_throw_rate( "advisor_throw_rate", "4" ); // Throw an object every 4 seconds.
ConVar advisor_throw_warn_time( "advisor_throw_warn_time", "1.0" ); // Warn players one second before throwing an object.
ConVar advisor_throw_lead_prefetch_time ( "advisor_throw_lead_prefetch_time", "0.66", FCVAR_NONE, "Save off the player's velocity this many seconds before throwing.");
ConVar advisor_throw_stage_distance("advisor_throw_stage_distance","180.0",FCVAR_NONE,"Advisor will try to hold an object this far in front of him just before throwing it at you. Small values will clobber the shield and be very bad.");
// ConVar advisor_staging_num("advisor_staging_num","1",FCVAR_NONE,"Advisor will queue up this many objects to throw at Gordon.");
ConVar advisor_throw_clearout_vel("advisor_throw_clearout_vel","200",FCVAR_NONE,"TEMP: velocity with which advisor clears things out of a throwable's way");
// ConVar advisor_staging_duration("
// how long it will take an object to get hauled to the staging point
#define STAGING_OBJECT_FALLOFF_TIME 0.15f
#endif
//
// Spawnflags.
//
//
// Animation events.
//
#if NPC_ADVISOR_HAS_BEHAVIOR
//
// Custom schedules.
//
enum
{
SCHED_ADVISOR_COMBAT = LAST_SHARED_SCHEDULE,
SCHED_ADVISOR_IDLE_STAND,
SCHED_ADVISOR_TOSS_PLAYER
};
//
// Custom tasks.
//
enum
{
TASK_ADVISOR_FIND_OBJECTS = LAST_SHARED_TASK,
TASK_ADVISOR_LEVITATE_OBJECTS,
TASK_ADVISOR_STAGE_OBJECTS,
TASK_ADVISOR_BARRAGE_OBJECTS,
TASK_ADVISOR_PIN_PLAYER,
};
//
// Custom conditions.
//
enum
{
COND_ADVISOR_PHASE_INTERRUPT = LAST_SHARED_CONDITION,
};
#endif
class CNPC_Advisor;
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
class CAdvisorLevitate : public IMotionEvent
{
DECLARE_SIMPLE_DATADESC();
public:
// in the absence of goal entities, we float up before throwing and down after
inline bool OldStyle( void )
{
return !(m_vecGoalPos1.IsValid() && m_vecGoalPos2.IsValid());
}
virtual simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular );
EHANDLE m_Advisor; ///< handle to the advisor.
Vector m_vecGoalPos1;
Vector m_vecGoalPos2;
float m_flFloat;
};
BEGIN_SIMPLE_DATADESC( CAdvisorLevitate )
DEFINE_FIELD( m_flFloat, FIELD_FLOAT ),
DEFINE_FIELD( m_vecGoalPos1, FIELD_POSITION_VECTOR ),
DEFINE_FIELD( m_vecGoalPos2, FIELD_POSITION_VECTOR ),
DEFINE_FIELD( m_Advisor, FIELD_EHANDLE ),
END_DATADESC()
//-----------------------------------------------------------------------------
// The advisor class.
//-----------------------------------------------------------------------------
class CNPC_Advisor : public CAI_BaseNPC
{
DECLARE_CLASS( CNPC_Advisor, CAI_BaseNPC );
#if NPC_ADVISOR_HAS_BEHAVIOR
DECLARE_SERVERCLASS();
#endif
public:
//
// CBaseEntity:
//
virtual void Activate();
virtual void Spawn();
virtual void Precache();
virtual void OnRestore();
virtual void UpdateOnRemove();
virtual int DrawDebugTextOverlays();
//
// CAI_BaseNPC:
//
virtual float MaxYawSpeed() { return 120.0f; }
virtual Class_T Classify();
#if NPC_ADVISOR_HAS_BEHAVIOR
virtual int GetSoundInterests();
virtual int SelectSchedule();
virtual void StartTask( const Task_t *pTask );
virtual void RunTask( const Task_t *pTask );
virtual void OnScheduleChange( void );
#endif
virtual void PainSound( const CTakeDamageInfo &info );
virtual void DeathSound( const CTakeDamageInfo &info );
virtual void IdleSound();
virtual void AlertSound();
#if NPC_ADVISOR_HAS_BEHAVIOR
virtual bool QueryHearSound( CSound *pSound );
virtual void GatherConditions( void );
/// true iff I recently threw the given object (not so fast)
bool DidThrow(const CBaseEntity *pEnt);
#else
inline bool DidThrow(const CBaseEntity *pEnt) { return false; }
#endif
virtual bool IsHeavyDamage( const CTakeDamageInfo &info );
virtual int OnTakeDamage( const CTakeDamageInfo &info );
virtual const impactdamagetable_t &GetPhysicsImpactDamageTable( void );
COutputInt m_OnHealthIsNow;
#if NPC_ADVISOR_HAS_BEHAVIOR
DEFINE_CUSTOM_AI;
void InputSetThrowRate( inputdata_t &inputdata );
void InputWrenchImmediate( inputdata_t &inputdata ); ///< immediately wrench an object into the air
void InputSetStagingNum( inputdata_t &inputdata );
void InputPinPlayer( inputdata_t &inputdata );
void InputTurnBeamOn( inputdata_t &inputdata );
void InputTurnBeamOff( inputdata_t &inputdata );
void InputElightOn( inputdata_t &inputdata );
void InputElightOff( inputdata_t &inputdata );
COutputEvent m_OnPickingThrowable, m_OnThrowWarn, m_OnThrow;
enum { kMaxThrownObjectsTracked = 4 };
#endif
DECLARE_DATADESC();
protected:
#if NPC_ADVISOR_HAS_BEHAVIOR
Vector GetThrowFromPos( CBaseEntity *pEnt ); ///< Get the position in which we shall hold an object prior to throwing it
#endif
bool CanLevitateEntity( CBaseEntity *pEntity, int minMass, int maxMass );
void StartLevitatingObjects( void );
#if NPC_ADVISOR_HAS_BEHAVIOR
// void PurgeThrownObjects(); ///< clean out the recently thrown objects array
void AddToThrownObjects(CBaseEntity *pEnt); ///< add to the recently thrown objects array
void HurlObjectAtPlayer( CBaseEntity *pEnt, const Vector &leadVel );
void PullObjectToStaging( CBaseEntity *pEnt, const Vector &stagingPos );
CBaseEntity *ThrowObjectPrepare( void );
CBaseEntity *PickThrowable( bool bRequireInView ); ///< choose an object to throw at the player (so it can get stuffed in the handle array)
/// push everything out of the way between an object I'm about to throw and the player.
void PreHurlClearTheWay( CBaseEntity *pThrowable, const Vector &toPos );
#endif
CUtlVector<EHANDLE> m_physicsObjects;
IPhysicsMotionController *m_pLevitateController;
CAdvisorLevitate m_levitateCallback;
EHANDLE m_hLevitateGoal1;
EHANDLE m_hLevitateGoal2;
EHANDLE m_hLevitationArea;
#if NPC_ADVISOR_HAS_BEHAVIOR
// EHANDLE m_hThrowEnt;
CUtlVector<EHANDLE> m_hvStagedEnts;
CUtlVector<EHANDLE> m_hvStagingPositions;
// todo: write accessor functions for m_hvStagedEnts so that it doesn't have members added and removed willy nilly throughout
// code (will make the networking below more reliable)
void Write_BeamOn( CBaseEntity *pEnt ); ///< write a message turning a beam on
void Write_BeamOff( CBaseEntity *pEnt ); ///< write a message turning a beam off
void Write_AllBeamsOff( void ); ///< tell client to kill all beams
// for the pin-the-player-to-something behavior
EHANDLE m_hPlayerPinPos;
float m_playerPinFailsafeTime;
// keep track of up to four objects after we have thrown them, to prevent oscillation or levitation of recently thrown ammo.
EHANDLE m_haRecentlyThrownObjects[kMaxThrownObjectsTracked];
float m_flaRecentlyThrownObjectTimes[kMaxThrownObjectsTracked];
#endif
string_t m_iszLevitateGoal1;
string_t m_iszLevitateGoal2;
string_t m_iszLevitationArea;
#if NPC_ADVISOR_HAS_BEHAVIOR
string_t m_iszStagingEntities;
string_t m_iszPriorityEntityGroupName;
float m_flStagingEnd;
float m_flThrowPhysicsTime;
float m_flLastThrowTime;
float m_flLastPlayerAttackTime; ///< last time the player attacked something.
int m_iStagingNum; ///< number of objects advisor stages at once
bool m_bWasScripting;
// unsigned char m_pickFailures; // the number of times we have tried to pick a throwable and failed
Vector m_vSavedLeadVel; ///< save off player velocity for leading a bit before actually pelting them.
#endif
};
LINK_ENTITY_TO_CLASS( npc_advisor, CNPC_Advisor );
BEGIN_DATADESC( CNPC_Advisor )
DEFINE_KEYFIELD( m_iszLevitateGoal1, FIELD_STRING, "levitategoal_bottom" ),
DEFINE_KEYFIELD( m_iszLevitateGoal2, FIELD_STRING, "levitategoal_top" ),
DEFINE_KEYFIELD( m_iszLevitationArea, FIELD_STRING, "levitationarea"), ///< we will float all the objects in this volume
DEFINE_PHYSPTR( m_pLevitateController ),
DEFINE_EMBEDDED( m_levitateCallback ),
DEFINE_UTLVECTOR( m_physicsObjects, FIELD_EHANDLE ),
DEFINE_FIELD( m_hLevitateGoal1, FIELD_EHANDLE ),
DEFINE_FIELD( m_hLevitateGoal2, FIELD_EHANDLE ),
DEFINE_FIELD( m_hLevitationArea, FIELD_EHANDLE ),
#if NPC_ADVISOR_HAS_BEHAVIOR
DEFINE_KEYFIELD( m_iszStagingEntities, FIELD_STRING, "staging_ent_names"), ///< entities named this constitute the positions to which we stage objects to be thrown
DEFINE_KEYFIELD( m_iszPriorityEntityGroupName, FIELD_STRING, "priority_grab_name"),
DEFINE_UTLVECTOR( m_hvStagedEnts, FIELD_EHANDLE ),
DEFINE_UTLVECTOR( m_hvStagingPositions, FIELD_EHANDLE ),
DEFINE_ARRAY( m_haRecentlyThrownObjects, FIELD_EHANDLE, CNPC_Advisor::kMaxThrownObjectsTracked ),
DEFINE_ARRAY( m_flaRecentlyThrownObjectTimes, FIELD_TIME, CNPC_Advisor::kMaxThrownObjectsTracked ),
DEFINE_FIELD( m_hPlayerPinPos, FIELD_EHANDLE ),
DEFINE_FIELD( m_playerPinFailsafeTime, FIELD_TIME ),
// DEFINE_FIELD( m_hThrowEnt, FIELD_EHANDLE ),
DEFINE_FIELD( m_flThrowPhysicsTime, FIELD_TIME ),
DEFINE_FIELD( m_flLastPlayerAttackTime, FIELD_TIME ),
DEFINE_FIELD( m_flStagingEnd, FIELD_TIME ),
DEFINE_FIELD( m_iStagingNum, FIELD_INTEGER ),
DEFINE_FIELD( m_bWasScripting, FIELD_BOOLEAN ),
DEFINE_FIELD( m_flLastThrowTime, FIELD_TIME ),
DEFINE_FIELD( m_vSavedLeadVel, FIELD_VECTOR ),
DEFINE_OUTPUT( m_OnPickingThrowable, "OnPickingThrowable" ),
DEFINE_OUTPUT( m_OnThrowWarn, "OnThrowWarn" ),
DEFINE_OUTPUT( m_OnThrow, "OnThrow" ),
DEFINE_OUTPUT( m_OnHealthIsNow, "OnHealthIsNow" ),
DEFINE_INPUTFUNC( FIELD_FLOAT, "SetThrowRate", InputSetThrowRate ),
DEFINE_INPUTFUNC( FIELD_STRING, "WrenchImmediate", InputWrenchImmediate ),
DEFINE_INPUTFUNC( FIELD_INTEGER, "SetStagingNum", InputSetStagingNum),
DEFINE_INPUTFUNC( FIELD_STRING, "PinPlayer", InputPinPlayer ),
DEFINE_INPUTFUNC( FIELD_STRING, "BeamOn", InputTurnBeamOn ),
DEFINE_INPUTFUNC( FIELD_STRING, "BeamOff", InputTurnBeamOff ),
DEFINE_INPUTFUNC( FIELD_STRING, "ElightOn", InputElightOn ),
DEFINE_INPUTFUNC( FIELD_STRING, "ElightOff", InputElightOff ),
#endif
END_DATADESC()
#if NPC_ADVISOR_HAS_BEHAVIOR
IMPLEMENT_SERVERCLASS_ST(CNPC_Advisor, DT_NPC_Advisor)
END_SEND_TABLE()
#endif
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_Advisor::Spawn()
{
BaseClass::Spawn();
#ifdef _XBOX
// Always fade the corpse
AddSpawnFlags( SF_NPC_FADE_CORPSE );
#endif // _XBOX
Precache();
SetModel( STRING( GetModelName() ) );
m_iHealth = sk_advisor_health.GetFloat();
m_takedamage = DAMAGE_NO;
SetHullType( HULL_LARGE_CENTERED );
SetHullSizeNormal();
SetSolid( SOLID_BBOX );
// AddSolidFlags( FSOLID_NOT_SOLID );
SetMoveType( MOVETYPE_FLY );
m_flFieldOfView = VIEW_FIELD_FULL;
SetViewOffset( Vector( 0, 0, 80 ) ); // Position of the eyes relative to NPC's origin.
SetBloodColor( BLOOD_COLOR_GREEN );
m_NPCState = NPC_STATE_NONE;
CapabilitiesClear();
NPCInit();
SetGoalEnt( NULL );
AddEFlags( EFL_NO_DISSOLVE );
}
#if NPC_ADVISOR_HAS_BEHAVIOR
//-----------------------------------------------------------------------------
// comparison function for qsort used below. Compares "StagingPriority" keyfield
//-----------------------------------------------------------------------------
int __cdecl AdvisorStagingComparator(const EHANDLE *pe1, const EHANDLE *pe2)
{
// bool ReadKeyField( const char *varName, variant_t *var );
variant_t var;
int val1 = 10, val2 = 10; // default priority is ten
// read field one
if ( pe1->Get()->ReadKeyField( "StagingPriority", &var ) )
{
val1 = var.Int();
}
// read field two
if ( pe2->Get()->ReadKeyField( "StagingPriority", &var ) )
{
val2 = var.Int();
}
// return comparison (< 0 if pe1<pe2)
return( val1 - val2 );
}
#endif
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
#pragma warning(push)
#pragma warning(disable : 4706)
void CNPC_Advisor::Activate()
{
BaseClass::Activate();
m_hLevitateGoal1 = gEntList.FindEntityGeneric( NULL, STRING( m_iszLevitateGoal1 ), this );
m_hLevitateGoal2 = gEntList.FindEntityGeneric( NULL, STRING( m_iszLevitateGoal2 ), this );
m_hLevitationArea = gEntList.FindEntityGeneric( NULL, STRING( m_iszLevitationArea ), this );
m_levitateCallback.m_Advisor = this;
#if NPC_ADVISOR_HAS_BEHAVIOR
// load the staging positions
CBaseEntity *pEnt = NULL;
m_hvStagingPositions.EnsureCapacity(6); // reserve six
// conditional assignment: find an entity by name and save it into pEnt. Bail out when none are left.
while ( pEnt = gEntList.FindEntityByName(pEnt,m_iszStagingEntities) )
{
m_hvStagingPositions.AddToTail(pEnt);
}
// sort the staging positions by their staging number.
m_hvStagingPositions.Sort( AdvisorStagingComparator );
// positions loaded, null out the m_hvStagedEnts array with exactly as many null spaces
m_hvStagedEnts.SetCount( m_hvStagingPositions.Count() );
m_iStagingNum = 1;
AssertMsg(m_hvStagingPositions.Count() > 0, "You did not specify any staging positions in the advisor's staging_ent_names !");
#endif
}
#pragma warning(pop)
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_Advisor::UpdateOnRemove()
{
if ( m_pLevitateController )
{
physenv->DestroyMotionController( m_pLevitateController );
}
BaseClass::UpdateOnRemove();
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_Advisor::OnRestore()
{
BaseClass::OnRestore();
StartLevitatingObjects();
}
//-----------------------------------------------------------------------------
// Returns this monster's classification in the relationship table.
//-----------------------------------------------------------------------------
Class_T CNPC_Advisor::Classify()
{
return CLASS_COMBINE;
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
bool CNPC_Advisor::IsHeavyDamage( const CTakeDamageInfo &info )
{
return (info.GetDamage() > 0);
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_Advisor::StartLevitatingObjects()
{
if ( !m_pLevitateController )
{
m_pLevitateController = physenv->CreateMotionController( &m_levitateCallback );
}
m_pLevitateController->ClearObjects();
int nCount = m_physicsObjects.Count();
for ( int i = 0; i < nCount; i++ )
{
CBaseEntity *pEnt = m_physicsObjects.Element( i );
if ( !pEnt )
continue;
//NDebugOverlay::Box( pEnt->GetAbsOrigin(), pEnt->CollisionProp()->OBBMins(), pEnt->CollisionProp()->OBBMaxs(), 0, 255, 0, 1, 0.1 );
IPhysicsObject *pPhys = pEnt->VPhysicsGetObject();
if ( pPhys && pPhys->IsMoveable() )
{
m_pLevitateController->AttachObject( pPhys, false );
pPhys->Wake();
}
}
}
// This function is used by both version of the entity finder below
bool CNPC_Advisor::CanLevitateEntity( CBaseEntity *pEntity, int minMass, int maxMass )
{
if (!pEntity || pEntity->IsNPC())
return false;
IPhysicsObject *pPhys = pEntity->VPhysicsGetObject();
if (!pPhys)
return false;
float mass = pPhys->GetMass();
return ( mass >= minMass &&
mass <= maxMass &&
//pEntity->VPhysicsGetObject()->IsAsleep() &&
pPhys->IsMoveable() /* &&
!DidThrow(pEntity) */ );
}
#if NPC_ADVISOR_HAS_BEHAVIOR
// find an object to throw at the player and start the warning on it. Return object's
// pointer if we got something. Otherwise, return NULL if nothing left to throw. Will
// always leave the prepared object at the head of m_hvStagedEnts
CBaseEntity *CNPC_Advisor::ThrowObjectPrepare()
{
CBaseEntity *pThrowable = NULL;
while (m_hvStagedEnts.Count() > 0)
{
pThrowable = m_hvStagedEnts[0];
if (pThrowable)
{
IPhysicsObject *pPhys = pThrowable->VPhysicsGetObject();
if ( !pPhys )
{
// reject!
Write_BeamOff(m_hvStagedEnts[0]);
pThrowable = NULL;
}
}
// if we still have pThrowable...
if (pThrowable)
{
// we're good
break;
}
else
{
m_hvStagedEnts.Remove(0);
}
}
if (pThrowable)
{
Assert( pThrowable->VPhysicsGetObject() );
// play the sound, attach the light, fire the trigger
EmitSound( "NPC_Advisor.ObjectChargeUp" );
m_OnThrowWarn.FireOutput(pThrowable,this);
m_flThrowPhysicsTime = gpGlobals->curtime + advisor_throw_warn_time.GetFloat();
if ( GetEnemy() )
{
PreHurlClearTheWay( pThrowable, GetEnemy()->EyePosition() );
}
return pThrowable;
}
else // we had nothing to throw
{
return NULL;
}
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_Advisor::StartTask( const Task_t *pTask )
{
switch ( pTask->iTask )
{
// DVS: TODO: if this gets expensive we can start caching the results and doing it less often.
case TASK_ADVISOR_FIND_OBJECTS:
{
// if we have a trigger volume, use the contents of that. If not, use a hardcoded box (for debugging purposes)
// in both cases we validate the objects using the same helper funclet just above. When we can count on the
// trigger vol being there, we can elide the else{} clause here.
CBaseEntity *pVolume = m_hLevitationArea;
AssertMsg(pVolume, "Combine advisor needs 'levitationarea' key pointing to a trigger volume." );
if (!pVolume)
{
TaskFail( "No levitation area found!" );
break;
}
touchlink_t *touchroot = ( touchlink_t * )pVolume->GetDataObject( TOUCHLINK );
if ( touchroot )
{
m_physicsObjects.RemoveAll();
for ( touchlink_t *link = touchroot->nextLink; link != touchroot; link = link->nextLink )
{
CBaseEntity *pTouch = link->entityTouched;
if ( CanLevitateEntity( pTouch, 10, 220 ) )
{
if ( pTouch->GetMoveType() == MOVETYPE_VPHYSICS )
{
//Msg( " %d added %s\n", m_physicsObjects.Count(), STRING( list[i]->GetModelName() ) );
m_physicsObjects.AddToTail( pTouch );
}
}
}
}
/*
// this is the old mechanism, using a hardcoded box and an entity enumerator.
// since deprecated.
else
{
CBaseEntity *list[128];
m_physicsObjects.RemoveAll();
//NDebugOverlay::Box( GetAbsOrigin(), Vector( -408, -368, -188 ), Vector( 92, 208, 168 ), 255, 255, 0, 1, 5 );
// one-off class used to determine which entities we want from the UTIL_EntitiesInBox
class CAdvisorLevitateEntitiesEnum : public CFlaggedEntitiesEnum
{
public:
CAdvisorLevitateEntitiesEnum( CBaseEntity **pList, int listMax, int nMinMass, int nMaxMass )
: CFlaggedEntitiesEnum( pList, listMax, 0 ),
m_nMinMass( nMinMass ),
m_nMaxMass( nMaxMass )
{
}
virtual IterationRetval_t EnumElement( IHandleEntity *pHandleEntity )
{
CBaseEntity *pEntity = gEntList.GetBaseEntity( pHandleEntity->GetRefEHandle() );
if ( AdvisorCanLevitateEntity( pEntity, m_nMinMass, m_nMaxMass ) )
{
return CFlaggedEntitiesEnum::EnumElement( pHandleEntity );
}
return ITERATION_CONTINUE;
}
int m_nMinMass;
int m_nMaxMass;
};
CAdvisorLevitateEntitiesEnum levitateEnum( list, ARRAYSIZE( list ), 10, 220 );
int nCount = UTIL_EntitiesInBox( GetAbsOrigin() - Vector( 554, 368, 188 ), GetAbsOrigin() + Vector( 92, 208, 168 ), &levitateEnum );
for ( int i = 0; i < nCount; i++ )
{
//Msg( "%d found %s\n", m_physicsObjects.Count(), STRING( list[i]->GetModelName() ) );
if ( list[i]->GetMoveType() == MOVETYPE_VPHYSICS )
{
//Msg( " %d added %s\n", m_physicsObjects.Count(), STRING( list[i]->GetModelName() ) );
m_physicsObjects.AddToTail( list[i] );
}
}
}
*/
if ( m_physicsObjects.Count() > 0 )
{
TaskComplete();
}
else
{
TaskFail( "No physics objects found!" );
}
break;
}
case TASK_ADVISOR_LEVITATE_OBJECTS:
{
StartLevitatingObjects();
m_flThrowPhysicsTime = gpGlobals->curtime + advisor_throw_rate.GetFloat();
break;
}
case TASK_ADVISOR_STAGE_OBJECTS:
{
// m_pickFailures = 0;
// clear out previously staged throwables
/*
for (int ii = m_hvStagedEnts.Count() - 1; ii >= 0 ; --ii)
{
m_hvStagedEnts[ii] = NULL;
}
*/
Write_AllBeamsOff();
m_hvStagedEnts.RemoveAll();
m_OnPickingThrowable.FireOutput(NULL,this);
m_flStagingEnd = gpGlobals->curtime + pTask->flTaskData;
break;
}
// we're about to pelt the player with everything. Start the warning effect on the first object.
case TASK_ADVISOR_BARRAGE_OBJECTS:
{
CBaseEntity *pThrowable = ThrowObjectPrepare();
if (!pThrowable || m_hvStagedEnts.Count() < 1)
{
TaskFail( "Nothing to throw!" );
return;
}
m_vSavedLeadVel.Invalidate();
break;
}
case TASK_ADVISOR_PIN_PLAYER:
{
// should never be here
/*
Assert( m_hPlayerPinPos.IsValid() );
m_playerPinFailsafeTime = gpGlobals->curtime + 10.0f;
break;
*/
}
default:
{
BaseClass::StartTask( pTask );
}
}
}
//-----------------------------------------------------------------------------
// todo: find a way to guarantee that objects are made pickupable again when bailing out of a task
//-----------------------------------------------------------------------------
void CNPC_Advisor::RunTask( const Task_t *pTask )
{
switch ( pTask->iTask )
{
// Raise up the objects that we found and then hold them.
case TASK_ADVISOR_LEVITATE_OBJECTS:
{
float flTimeToThrow = m_flThrowPhysicsTime - gpGlobals->curtime;
if ( flTimeToThrow < 0 )
{
TaskComplete();
return;
}
// set the top and bottom on the levitation volume from the entities. If we don't have
// both, zero it out so that we can use the old-style simpler mechanism.
if ( m_hLevitateGoal1 && m_hLevitateGoal2 )
{
m_levitateCallback.m_vecGoalPos1 = m_hLevitateGoal1->GetAbsOrigin();
m_levitateCallback.m_vecGoalPos2 = m_hLevitateGoal2->GetAbsOrigin();
// swap them if necessary (1 must be the bottom)
if (m_levitateCallback.m_vecGoalPos1.z > m_levitateCallback.m_vecGoalPos2.z)
{
swap(m_levitateCallback.m_vecGoalPos1,m_levitateCallback.m_vecGoalPos2);
}
m_levitateCallback.m_flFloat = 0.06f; // this is an absolute accumulation upon gravity
}
else
{
m_levitateCallback.m_vecGoalPos1.Invalidate();
m_levitateCallback.m_vecGoalPos2.Invalidate();
// the below two stanzas are used for old-style floating, which is linked
// to float up before thrown and down after
if ( flTimeToThrow > 2.0f )
{
m_levitateCallback.m_flFloat = 1.06f;
}
else
{
m_levitateCallback.m_flFloat = 0.94f;
}
}
/*
// Draw boxes around the objects we're levitating.
for ( int i = 0; i < m_physicsObjects.Count(); i++ )
{
CBaseEntity *pEnt = m_physicsObjects.Element( i );
if ( !pEnt )
continue; // The prop has been broken!
IPhysicsObject *pPhys = pEnt->VPhysicsGetObject();
if ( pPhys && pPhys->IsMoveable() )
{
NDebugOverlay::Box( pEnt->GetAbsOrigin(), pEnt->CollisionProp()->OBBMins(), pEnt->CollisionProp()->OBBMaxs(), 0, 255, 0, 1, 0.1 );
}
}*/
break;
}
// Pick a random object that we are levitating. If we have a clear LOS from that object
// to our enemy's eyes, choose that one to throw. Otherwise, keep looking.
case TASK_ADVISOR_STAGE_OBJECTS:
{
if (m_iStagingNum > m_hvStagingPositions.Count())
{
Warning( "Advisor tries to stage %d objects but only has %d positions named %s! Overriding.\n", m_iStagingNum, m_hvStagingPositions.Count(), m_iszStagingEntities );
m_iStagingNum = m_hvStagingPositions.Count() ;
}
// advisor_staging_num
// in the future i'll distribute the staging chronologically. For now, yank all the objects at once.
if (m_hvStagedEnts.Count() < m_iStagingNum)
{
// pull another object
bool bDesperate = m_flStagingEnd - gpGlobals->curtime < 0.50f; // less than one half second left
CBaseEntity *pThrowable = PickThrowable(!bDesperate);
if (pThrowable)
{
// don't let the player take it from me
IPhysicsObject *pPhys = pThrowable->VPhysicsGetObject();
if ( pPhys )
{
// no pickup!
pPhys->SetGameFlags(pPhys->GetGameFlags() | FVPHYSICS_NO_PLAYER_PICKUP );;
}
m_hvStagedEnts.AddToTail( pThrowable );
Write_BeamOn(pThrowable);
DispatchParticleEffect( "advisor_object_charge", PATTACH_ABSORIGIN_FOLLOW,
pThrowable, 0,
false );
}
}
Assert(m_hvStagedEnts.Count() <= m_hvStagingPositions.Count());
// yank all objects into place
for (int ii = m_hvStagedEnts.Count() - 1 ; ii >= 0 ; --ii)
{
// just ignore lost objects (if the player destroys one, that's fine, leave a hole)
CBaseEntity *pThrowable = m_hvStagedEnts[ii];
if (pThrowable)
{
PullObjectToStaging(pThrowable, m_hvStagingPositions[ii]->GetAbsOrigin());
}
}
// are we done yet?
if (gpGlobals->curtime > m_flStagingEnd)
{
TaskComplete();
break;
}
break;
}
// Fling the object that we picked at our enemy's eyes!
case TASK_ADVISOR_BARRAGE_OBJECTS:
{
Assert(m_hvStagedEnts.Count() > 0);
// do I still have an enemy?
if ( !GetEnemy() )
{
// no? bail all the objects.
for (int ii = m_hvStagedEnts.Count() - 1 ; ii >=0 ; --ii)
{
IPhysicsObject *pPhys = m_hvStagedEnts[ii]->VPhysicsGetObject();
if ( pPhys )
{
pPhys->SetGameFlags(pPhys->GetGameFlags() & (~FVPHYSICS_NO_PLAYER_PICKUP) );
}
}
Write_AllBeamsOff();
m_hvStagedEnts.RemoveAll();
TaskFail( "Lost enemy" );
return;
}
// do I still have something to throw at the player?
CBaseEntity *pThrowable = m_hvStagedEnts[0];
while (!pThrowable)
{ // player has destroyed whatever I planned to hit him with, get something else
if (m_hvStagedEnts.Count() > 0)
{
pThrowable = ThrowObjectPrepare();
}
else
{
TaskComplete();
break;
}
}
// If we've gone NULL, then opt out
if ( pThrowable == NULL )
{
TaskComplete();
break;
}
if ( (gpGlobals->curtime > m_flThrowPhysicsTime - advisor_throw_lead_prefetch_time.GetFloat()) &&
!m_vSavedLeadVel.IsValid() )
{
// save off the velocity we will use to lead the player a little early, so that if he jukes
// at the last moment he'll have a better shot of dodging the object.
m_vSavedLeadVel = GetEnemy()->GetAbsVelocity();
}
// if it's time to throw something, throw it and go on to the next one.
if (gpGlobals->curtime > m_flThrowPhysicsTime)
{
IPhysicsObject *pPhys = pThrowable->VPhysicsGetObject();
Assert(pPhys);
pPhys->SetGameFlags(pPhys->GetGameFlags() & (~FVPHYSICS_NO_PLAYER_PICKUP) );
HurlObjectAtPlayer(pThrowable,Vector(0,0,0)/*m_vSavedLeadVel*/);
m_flLastThrowTime = gpGlobals->curtime;
m_flThrowPhysicsTime = gpGlobals->curtime + 0.75f;
// invalidate saved lead for next time
m_vSavedLeadVel.Invalidate();
EmitSound( "NPC_Advisor.Blast" );
Write_BeamOff(m_hvStagedEnts[0]);
m_hvStagedEnts.Remove(0);
if (!ThrowObjectPrepare())
{
TaskComplete();
break;
}
}
else
{
// wait, bide time
// PullObjectToStaging(pThrowable, m_hvStagingPositions[ii]->GetAbsOrigin());
}
break;
}
case TASK_ADVISOR_PIN_PLAYER:
{
/*
// bail out if the pin entity went away.
CBaseEntity *pPinEnt = m_hPlayerPinPos;
if (!pPinEnt)
{
GetEnemy()->SetGravity(1.0f);
GetEnemy()->SetMoveType( MOVETYPE_WALK );
TaskComplete();
break;
}
// failsafe: don't do this for more than ten seconds.
if ( gpGlobals->curtime > m_playerPinFailsafeTime )
{
GetEnemy()->SetGravity(1.0f);
GetEnemy()->SetMoveType( MOVETYPE_WALK );
Warning( "Advisor did not leave PIN PLAYER mode. Aborting due to ten second failsafe!\n" );
TaskFail("Advisor did not leave PIN PLAYER mode. Aborting due to ten second failsafe!\n");
break;
}
// if the player isn't the enemy, bail out.
if ( !GetEnemy()->IsPlayer() )
{
GetEnemy()->SetGravity(1.0f);
GetEnemy()->SetMoveType( MOVETYPE_WALK );
TaskFail( "Player is not the enemy?!" );
break;
}
GetEnemy()->SetMoveType( MOVETYPE_FLY );
GetEnemy()->SetGravity(0);
// use exponential falloff to peg the player to the pin point
const Vector &desiredPos = pPinEnt->GetAbsOrigin();
const Vector &playerPos = GetEnemy()->GetAbsOrigin();
Vector displacement = desiredPos - playerPos;
float desiredDisplacementLen = ExponentialDecay(0.250f,gpGlobals->frametime);// * sqrt(displacementLen);
Vector nuPos = playerPos + (displacement * (1.0f - desiredDisplacementLen));
GetEnemy()->SetAbsOrigin( nuPos );
break;
*/
}
default:
{
BaseClass::RunTask( pTask );
}
}
}
#endif
// helper function for testing whether or not an avisor is allowed to grab an object
static bool AdvisorCanPickObject(CBasePlayer *pPlayer, CBaseEntity *pEnt)
{
Assert( pPlayer != NULL );
// Is the player carrying something?
CBaseEntity *pHeldObject = GetPlayerHeldEntity(pPlayer);
if( !pHeldObject )
{
pHeldObject = PhysCannonGetHeldEntity( pPlayer->GetActiveWeapon() );
}
if( pHeldObject == pEnt )
{
return false;
}
if ( pEnt->GetCollisionGroup() == COLLISION_GROUP_DEBRIS )
{
return false;
}
return true;
}
#if NPC_ADVISOR_HAS_BEHAVIOR
//-----------------------------------------------------------------------------
// Choose an object to throw.
// param bRequireInView : if true, only accept objects that are in the player's fov.
//
// Can always return NULL.
// todo priority_grab_name
//-----------------------------------------------------------------------------
CBaseEntity *CNPC_Advisor::PickThrowable( bool bRequireInView )
{
CBasePlayer *pPlayer = ToBasePlayer( GetEnemy() );
Assert(pPlayer);
if (!pPlayer)
return NULL;
const int numObjs = m_physicsObjects.Count(); ///< total number of physics objects in my system
if (numObjs < 1)
return NULL; // bail out if nothing available
// used for require-in-view
Vector eyeForward, eyeOrigin;
if (pPlayer)
{
eyeOrigin = pPlayer->EyePosition();
pPlayer->EyeVectors(&eyeForward);
}
else
{
bRequireInView = false;
}
// filter-and-choose algorithm:
// build a list of candidates
Assert(numObjs < 128); /// I'll come back and utlvector this shortly -- wanted easier debugging
unsigned int candidates[128];
unsigned int numCandidates = 0;
if (!!m_iszPriorityEntityGroupName) // if the string isn't null
{
// first look to see if we have any priority objects.
for (int ii = 0 ; ii < numObjs ; ++ii )
{
CBaseEntity *pThrowEnt = m_physicsObjects[ii];
// Assert(pThrowEnt);
if (!pThrowEnt)
continue;
if (!pThrowEnt->NameMatches(m_iszPriorityEntityGroupName)) // if this is not a priority object
continue;
bool bCanPick = AdvisorCanPickObject( pPlayer, pThrowEnt ) && !m_hvStagedEnts.HasElement( m_physicsObjects[ii] );
if (!bCanPick)
continue;
// bCanPick guaranteed true here
if ( bRequireInView )
{
bCanPick = (pThrowEnt->GetAbsOrigin() - eyeOrigin).Dot(eyeForward) > 0;
}
if ( bCanPick )
{
candidates[numCandidates++] = ii;
}
}
}
// if we found no priority objects (or don't have a priority), just grab whatever
if (numCandidates == 0)
{
for (int ii = 0 ; ii < numObjs ; ++ii )
{
CBaseEntity *pThrowEnt = m_physicsObjects[ii];
// Assert(pThrowEnt);
if (!pThrowEnt)
continue;
bool bCanPick = AdvisorCanPickObject( pPlayer, pThrowEnt ) && !m_hvStagedEnts.HasElement( m_physicsObjects[ii] );
if (!bCanPick)
continue;
// bCanPick guaranteed true here
if ( bRequireInView )
{
bCanPick = (pThrowEnt->GetAbsOrigin() - eyeOrigin).Dot(eyeForward) > 0;
}
if ( bCanPick )
{
candidates[numCandidates++] = ii;
}
}
}
if ( numCandidates == 0 )
return NULL; // must have at least one candidate
// pick a random candidate.
int nRandomIndex = random->RandomInt( 0, numCandidates - 1 );
return m_physicsObjects[candidates[nRandomIndex]];
}
/*! \TODO
Correct bug where Advisor seemed to be throwing stuff at people's feet.
This is because the object was falling slightly in between the staging
and when he threw it, and that downward velocity was getting accumulated
into the throw speed. This is temporarily fixed here by using SetVelocity
instead of AddVelocity, but the proper fix is to pin the object to its
staging point during the warn period. That will require maintaining a map
of throwables to their staging points during the throw task.
*/
//-----------------------------------------------------------------------------
// Impart necessary force on any entity to make it clobber Gordon.
// Also detaches from levitate controller.
// The optional lead velocity parameter is for cases when we pre-save off the
// player's speed, to make last-moment juking more effective
//-----------------------------------------------------------------------------
void CNPC_Advisor::HurlObjectAtPlayer( CBaseEntity *pEnt, const Vector &leadVel )
{
IPhysicsObject *pPhys = pEnt->VPhysicsGetObject();
//
// Lead the target accurately. This encourages hiding behind cover
// and/or catching the thrown physics object!
//
Vector vecObjOrigin = pEnt->CollisionProp()->WorldSpaceCenter();
Vector vecEnemyPos = GetEnemy()->EyePosition();
// disabled -- no longer compensate for gravity: // vecEnemyPos.y += 12.0f;
// const Vector &leadVel = pLeadVelocity ? *pLeadVelocity : GetEnemy()->GetAbsVelocity();
Vector vecDelta = vecEnemyPos - vecObjOrigin;
float flDist = vecDelta.Length();
float flVelocity = advisor_throw_velocity.GetFloat();
if ( flVelocity == 0 )
{
flVelocity = 1000;
}
float flFlightTime = flDist / flVelocity;
Vector vecThrowAt = vecEnemyPos + flFlightTime * leadVel;
Vector vecThrowDir = vecThrowAt - vecObjOrigin;
VectorNormalize( vecThrowDir );
Vector vecVelocity = flVelocity * vecThrowDir;
pPhys->SetVelocity( &vecVelocity, NULL );
AddToThrownObjects(pEnt);
m_OnThrow.FireOutput(pEnt,this);
}
//-----------------------------------------------------------------------------
// do a sweep from an object I'm about to throw, to the target, pushing aside
// anything floating in the way.
// TODO: this is probably a good profiling candidate.
//-----------------------------------------------------------------------------
void CNPC_Advisor::PreHurlClearTheWay( CBaseEntity *pThrowable, const Vector &toPos )
{
// look for objects in the way of chucking.
CBaseEntity *list[128];
Ray_t ray;
float boundingRadius = pThrowable->BoundingRadius();
ray.Init( pThrowable->GetAbsOrigin(), toPos,
Vector(-boundingRadius,-boundingRadius,-boundingRadius),
Vector( boundingRadius, boundingRadius, boundingRadius) );
int nFoundCt = UTIL_EntitiesAlongRay( list, 128, ray, 0 );
AssertMsg(nFoundCt < 128, "Found more than 128 obstructions between advisor and Gordon while throwing. (safe to continue)\n");
// for each thing in the way that I levitate, but is not something I'm staging
// or throwing, push it aside.
for (int i = 0 ; i < nFoundCt ; ++i )
{
CBaseEntity *obstruction = list[i];
if ( obstruction != pThrowable &&
m_physicsObjects.HasElement( obstruction ) && // if it's floating
!m_hvStagedEnts.HasElement( obstruction ) && // and I'm not staging it
!DidThrow( obstruction ) ) // and I didn't just throw it
{
IPhysicsObject *pPhys = obstruction->VPhysicsGetObject();
Assert(pPhys);
// this is an object we want to push out of the way. Compute a vector perpendicular
// to the path of the throwables's travel, and thrust the object along that vector.
Vector thrust;
CalcClosestPointOnLine( obstruction->GetAbsOrigin(),
pThrowable->GetAbsOrigin(),
toPos,
thrust );
// "thrust" is now the closest point on the line to the obstruction.
// compute the difference to get the direction of impulse
thrust = obstruction->GetAbsOrigin() - thrust;
// and renormalize it to equal a giant kick out of the way
// (which I'll say is about ten feet per second -- if we want to be
// more precise we could do some kind of interpolation based on how
// far away the object is)
float thrustLen = thrust.Length();
if (thrustLen > 0.0001f)
{
thrust *= advisor_throw_clearout_vel.GetFloat() / thrustLen;
}
// heave!
pPhys->AddVelocity( &thrust, NULL );
}
}
/*
// Otherwise only help out a little
Vector extents = Vector(256, 256, 256);
Ray_t ray;
ray.Init( vecStartPoint, vecStartPoint + 2048 * vecVelDir, -extents, extents );
int nCount = UTIL_EntitiesAlongRay( list, 1024, ray, FL_NPC | FL_CLIENT );
for ( int i = 0; i < nCount; i++ )
{
if ( !IsAttractiveTarget( list[i] ) )
continue;
VectorSubtract( list[i]->WorldSpaceCenter(), vecStartPoint, vecDelta );
distance = VectorNormalize( vecDelta );
flDot = DotProduct( vecDelta, vecVelDir );
if ( flDot > flMaxDot )
{
if ( distance < flBestDist )
{
pBestTarget = list[i];
flBestDist = distance;
}
}
}
*/
}
/*
// commented out because unnecessary: we will do this during the DidThrow check
//-----------------------------------------------------------------------------
// clean out the recently thrown objects array
//-----------------------------------------------------------------------------
void CNPC_Advisor::PurgeThrownObjects()
{
float threeSecondsAgo = gpGlobals->curtime - 3.0f; // two seconds ago
for (int ii = 0 ; ii < kMaxThrownObjectsTracked ; ++ii)
{
if ( m_haRecentlyThrownObjects[ii].IsValid() &&
m_flaRecentlyThrownObjectTimes[ii] < threeSecondsAgo )
{
m_haRecentlyThrownObjects[ii].Set(NULL);
}
}
}
*/
//-----------------------------------------------------------------------------
// true iff an advisor threw the object in the last three seconds
//-----------------------------------------------------------------------------
bool CNPC_Advisor::DidThrow(const CBaseEntity *pEnt)
{
// look through all my objects and see if they match this entity. Incidentally if
// they're more than three seconds old, purge them.
float threeSecondsAgo = gpGlobals->curtime - 3.0f;
for (int ii = 0 ; ii < kMaxThrownObjectsTracked ; ++ii)
{
// if object is old, skip it.
CBaseEntity *pTestEnt = m_haRecentlyThrownObjects[ii];
if ( pTestEnt )
{
if ( m_flaRecentlyThrownObjectTimes[ii] < threeSecondsAgo )
{
m_haRecentlyThrownObjects[ii].Set(NULL);
continue;
}
else if (pTestEnt == pEnt)
{
return true;
}
}
}
return false;
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_Advisor::AddToThrownObjects(CBaseEntity *pEnt)
{
Assert(pEnt);
// try to find an empty slot, or if none exists, the oldest object
int oldestThrownObject = 0;
for (int ii = 0 ; ii < kMaxThrownObjectsTracked ; ++ii)
{
if (m_haRecentlyThrownObjects[ii].IsValid())
{
if (m_flaRecentlyThrownObjectTimes[ii] < m_flaRecentlyThrownObjectTimes[oldestThrownObject])
{
oldestThrownObject = ii;
}
}
else
{ // just use this one
oldestThrownObject = ii;
break;
}
}
m_haRecentlyThrownObjects[oldestThrownObject] = pEnt;
m_flaRecentlyThrownObjectTimes[oldestThrownObject] = gpGlobals->curtime;
}
//-----------------------------------------------------------------------------
// Drag a particular object towards its staging location.
//-----------------------------------------------------------------------------
void CNPC_Advisor::PullObjectToStaging( CBaseEntity *pEnt, const Vector &stagingPos )
{
IPhysicsObject *pPhys = pEnt->VPhysicsGetObject();
Assert(pPhys);
Vector curPos = pEnt->CollisionProp()->WorldSpaceCenter();
Vector displacement = stagingPos - curPos;
// quick and dirty -- use exponential decay to haul the object into place
// ( a better looking solution would be to use a spring system )
float desiredDisplacementLen = ExponentialDecay(STAGING_OBJECT_FALLOFF_TIME, gpGlobals->frametime);// * sqrt(displacementLen);
Vector vel; AngularImpulse angimp;
pPhys->GetVelocity(&vel,&angimp);
vel = (1.0f / gpGlobals->frametime)*(displacement * (1.0f - desiredDisplacementLen));
pPhys->SetVelocity(&vel,&angimp);
}
#endif
int CNPC_Advisor::OnTakeDamage( const CTakeDamageInfo &info )
{
// Clip our max
CTakeDamageInfo newInfo = info;
if ( newInfo.GetDamage() > 20.0f )
{
newInfo.SetDamage( 20.0f );
}
// Hack to make him constantly flinch
m_flNextFlinchTime = gpGlobals->curtime;
const float oldLastDamageTime = m_flLastDamageTime;
int retval = BaseClass::OnTakeDamage(newInfo);
// we have a special reporting output
if ( oldLastDamageTime != gpGlobals->curtime )
{
// only fire once per frame
m_OnHealthIsNow.Set( GetHealth(), newInfo.GetAttacker(), this);
}
return retval;
}
#if NPC_ADVISOR_HAS_BEHAVIOR
//-----------------------------------------------------------------------------
// Returns the best new schedule for this NPC based on current conditions.
//-----------------------------------------------------------------------------
int CNPC_Advisor::SelectSchedule()
{
if ( IsInAScript() )
return SCHED_ADVISOR_IDLE_STAND;
switch ( m_NPCState )
{
case NPC_STATE_IDLE:
case NPC_STATE_ALERT:
{
return SCHED_ADVISOR_IDLE_STAND;
}
case NPC_STATE_COMBAT:
{
if ( GetEnemy() && GetEnemy()->IsAlive() )
{
if ( false /* m_hPlayerPinPos.IsValid() */ )
return SCHED_ADVISOR_TOSS_PLAYER;
else
return SCHED_ADVISOR_COMBAT;
}
return SCHED_ADVISOR_IDLE_STAND;
}
}
return BaseClass::SelectSchedule();
}
//-----------------------------------------------------------------------------
// return the position where an object should be staged before throwing
//-----------------------------------------------------------------------------
Vector CNPC_Advisor::GetThrowFromPos( CBaseEntity *pEnt )
{
Assert(pEnt);
Assert(pEnt->VPhysicsGetObject());
const CCollisionProperty *cProp = pEnt->CollisionProp();
Assert(cProp);
float effecRadius = cProp->BoundingRadius(); // radius of object (important for kickout)
float howFarInFront = advisor_throw_stage_distance.GetFloat() + effecRadius * 1.43f;// clamp(lenToPlayer - posDist + effecRadius,effecRadius*2,90.f + effecRadius);
Vector fwd;
GetVectors(&fwd,NULL,NULL);
return GetAbsOrigin() + fwd*howFarInFront;
}
#endif
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_Advisor::Precache()
{
BaseClass::Precache();
PrecacheModel( STRING( GetModelName() ) );
#if NPC_ADVISOR_HAS_BEHAVIOR
PrecacheModel( "sprites/lgtning.vmt" );
#endif
PrecacheScriptSound( "NPC_Advisor.Blast" );
PrecacheScriptSound( "NPC_Advisor.Gib" );
PrecacheScriptSound( "NPC_Advisor.Idle" );
PrecacheScriptSound( "NPC_Advisor.Alert" );
PrecacheScriptSound( "NPC_Advisor.Die" );
PrecacheScriptSound( "NPC_Advisor.Pain" );
PrecacheScriptSound( "NPC_Advisor.ObjectChargeUp" );
PrecacheParticleSystem( "Advisor_Psychic_Beam" );
PrecacheParticleSystem( "advisor_object_charge" );
PrecacheModel("sprites/greenglow1.vmt");
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
void CNPC_Advisor::IdleSound()
{
EmitSound( "NPC_Advisor.Idle" );
}
void CNPC_Advisor::AlertSound()
{
EmitSound( "NPC_Advisor.Alert" );
}
void CNPC_Advisor::PainSound( const CTakeDamageInfo &info )
{
EmitSound( "NPC_Advisor.Pain" );
}
void CNPC_Advisor::DeathSound( const CTakeDamageInfo &info )
{
EmitSound( "NPC_Advisor.Die" );
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
int CNPC_Advisor::DrawDebugTextOverlays()
{
int nOffset = BaseClass::DrawDebugTextOverlays();
return nOffset;
}
#if NPC_ADVISOR_HAS_BEHAVIOR
//-----------------------------------------------------------------------------
// Determines which sounds the advisor cares about.
//-----------------------------------------------------------------------------
int CNPC_Advisor::GetSoundInterests()
{
return SOUND_WORLD | SOUND_COMBAT | SOUND_PLAYER | SOUND_DANGER;
}
//-----------------------------------------------------------------------------
// record the last time we heard a combat sound
//-----------------------------------------------------------------------------
bool CNPC_Advisor::QueryHearSound( CSound *pSound )
{
// Disregard footsteps from our own class type
CBaseEntity *pOwner = pSound->m_hOwner;
if ( pOwner && pSound->IsSoundType( SOUND_COMBAT ) && pSound->SoundChannel() != SOUNDENT_CHANNEL_NPC_FOOTSTEP && pSound->m_hOwner.IsValid() && pOwner->IsPlayer() )
{
// Msg("Heard player combat.\n");
m_flLastPlayerAttackTime = gpGlobals->curtime;
}
return BaseClass::QueryHearSound(pSound);
}
//-----------------------------------------------------------------------------
// designer hook for setting throw rate
//-----------------------------------------------------------------------------
void CNPC_Advisor::InputSetThrowRate( inputdata_t &inputdata )
{
advisor_throw_rate.SetValue(inputdata.value.Float());
}
void CNPC_Advisor::InputSetStagingNum( inputdata_t &inputdata )
{
m_iStagingNum = inputdata.value.Int();
}
//
// cause the player to be pinned to a point in space
//
void CNPC_Advisor::InputPinPlayer( inputdata_t &inputdata )
{
string_t targetname = inputdata.value.StringID();
// null string means designer is trying to unpin the player
if (!targetname)
{
m_hPlayerPinPos = NULL;
}
// otherwise try to look up the entity and make it a target.
CBaseEntity *pEnt = gEntList.FindEntityByName(NULL,targetname);
if (pEnt)
{
m_hPlayerPinPos = pEnt;
}
else
{
// if we couldn't find the target, just bail on the behavior.
Warning("Advisor tried to pin player to %s but that does not exist.\n", targetname.ToCStr());
m_hPlayerPinPos = NULL;
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_Advisor::OnScheduleChange( void )
{
Write_AllBeamsOff();
m_hvStagedEnts.RemoveAll();
BaseClass::OnScheduleChange();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CNPC_Advisor::GatherConditions( void )
{
BaseClass::GatherConditions();
// Handle script state changes
bool bInScript = IsInAScript();
if ( ( m_bWasScripting && bInScript == false ) || ( m_bWasScripting == false && bInScript ) )
{
SetCondition( COND_ADVISOR_PHASE_INTERRUPT );
}
// Retain this
m_bWasScripting = bInScript;
}
//-----------------------------------------------------------------------------
// designer hook for yanking an object into the air right now
//-----------------------------------------------------------------------------
void CNPC_Advisor::InputWrenchImmediate( inputdata_t &inputdata )
{
string_t groupname = inputdata.value.StringID();
Assert(!!groupname);
// for all entities with that name that aren't floating, punt them at me and add them to the levitation
CBaseEntity *pEnt = NULL;
const Vector &myPos = GetAbsOrigin() + Vector(0,36.0f,0);
// conditional assignment: find an entity by name and save it into pEnt. Bail out when none are left.
while ( ( pEnt = gEntList.FindEntityByName(pEnt,groupname) ) != NULL )
{
// if I'm not already levitating it, and if I didn't just throw it
if (!m_physicsObjects.HasElement(pEnt) )
{
// add to levitation
IPhysicsObject *pPhys = pEnt->VPhysicsGetObject();
if ( pPhys )
{
// if the object isn't moveable, make it so.
if ( !pPhys->IsMoveable() )
{
pPhys->EnableMotion( true );
}
// first, kick it at me
Vector objectToMe;
pPhys->GetPosition(&objectToMe,NULL);
objectToMe = myPos - objectToMe;
// compute a velocity that will get it here in about a second
objectToMe /= (1.5f * gpGlobals->frametime);
objectToMe *= random->RandomFloat(0.25f,1.0f);
pPhys->SetVelocity( &objectToMe, NULL );
// add it to tracked physics objects
m_physicsObjects.AddToTail( pEnt );
m_pLevitateController->AttachObject( pPhys, false );
pPhys->Wake();
}
else
{
Warning( "Advisor tried to wrench %s, but it is not moveable!", pEnt->GetEntityName().ToCStr());
}
}
}
}
//-----------------------------------------------------------------------------
// write a message turning a beam on
//-----------------------------------------------------------------------------
void CNPC_Advisor::Write_BeamOn( CBaseEntity *pEnt )
{
Assert( pEnt );
EntityMessageBegin( this, true );
WRITE_BYTE( ADVISOR_MSG_START_BEAM );
WRITE_LONG( pEnt->entindex() );
MessageEnd();
}
//-----------------------------------------------------------------------------
// write a message turning a beam off
//-----------------------------------------------------------------------------
void CNPC_Advisor::Write_BeamOff( CBaseEntity *pEnt )
{
Assert( pEnt );
EntityMessageBegin( this, true );
WRITE_BYTE( ADVISOR_MSG_STOP_BEAM );
WRITE_LONG( pEnt->entindex() );
MessageEnd();
}
//-----------------------------------------------------------------------------
// tell client to kill all beams
//-----------------------------------------------------------------------------
void CNPC_Advisor::Write_AllBeamsOff( void )
{
EntityMessageBegin( this, true );
WRITE_BYTE( ADVISOR_MSG_STOP_ALL_BEAMS );
MessageEnd();
}
//-----------------------------------------------------------------------------
// input wrapper around Write_BeamOn
//-----------------------------------------------------------------------------
void CNPC_Advisor::InputTurnBeamOn( inputdata_t &inputdata )
{
// inputdata should specify a target
CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, inputdata.value.StringID() );
if ( pTarget )
{
Write_BeamOn( pTarget );
}
else
{
Warning("InputTurnBeamOn could not find object %s", inputdata.value.String() );
}
}
//-----------------------------------------------------------------------------
// input wrapper around Write_BeamOff
//-----------------------------------------------------------------------------
void CNPC_Advisor::InputTurnBeamOff( inputdata_t &inputdata )
{
// inputdata should specify a target
CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, inputdata.value.StringID() );
if ( pTarget )
{
Write_BeamOff( pTarget );
}
else
{
Warning("InputTurnBeamOn could not find object %s", inputdata.value.String() );
}
}
void CNPC_Advisor::InputElightOn( inputdata_t &inputdata )
{
EntityMessageBegin( this, true );
WRITE_BYTE( ADVISOR_MSG_START_ELIGHT );
MessageEnd();
}
void CNPC_Advisor::InputElightOff( inputdata_t &inputdata )
{
EntityMessageBegin( this, true );
WRITE_BYTE( ADVISOR_MSG_STOP_ELIGHT );
MessageEnd();
}
#endif
//==============================================================================================
// MOTION CALLBACK
//==============================================================================================
CAdvisorLevitate::simresult_e CAdvisorLevitate::Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular )
{
// this function can be optimized to minimize branching if necessary (PPE branch prediction)
CNPC_Advisor *pAdvisor = static_cast<CNPC_Advisor *>(m_Advisor.Get());
Assert(pAdvisor);
if ( !OldStyle() )
{ // independent movement of all objects
// if an object was recently thrown, just zero out its gravity.
if (pAdvisor->DidThrow(static_cast<CBaseEntity *>(pObject->GetGameData())))
{
linear = Vector( 0, 0, GetCurrentGravity() );
return SIM_GLOBAL_ACCELERATION;
}
else
{
Vector vel; AngularImpulse angvel;
pObject->GetVelocity(&vel,&angvel);
Vector pos;
pObject->GetPosition(&pos,NULL);
bool bMovingUp = vel.z > 0;
// if above top limit and moving up, move down. if below bottom limit and moving down, move up.
if (bMovingUp)
{
if (pos.z > m_vecGoalPos2.z)
{
// turn around move down
linear = Vector( 0, 0, Square((1.0f - m_flFloat)) * GetCurrentGravity() );
angular = Vector( 0, -5, 0 );
}
else
{ // keep moving up
linear = Vector( 0, 0, (1.0f + m_flFloat) * GetCurrentGravity() );
angular = Vector( 0, 0, 10 );
}
}
else
{
if (pos.z < m_vecGoalPos1.z)
{
// turn around move up
linear = Vector( 0, 0, Square((1.0f + m_flFloat)) * GetCurrentGravity() );
angular = Vector( 0, 5, 0 );
}
else
{ // keep moving down
linear = Vector( 0, 0, (1.0f - m_flFloat) * GetCurrentGravity() );
angular = Vector( 0, 0, 10 );
}
}
return SIM_GLOBAL_ACCELERATION;
}
//NDebugOverlay::Cross3D(pos,24.0f,255,255,0,true,0.04f);
}
else // old stateless technique
{
Warning("Advisor using old-style object movement!\n");
/* // obsolete
CBaseEntity *pEnt = (CBaseEntity *)pObject->GetGameData();
Vector vecDir1 = m_vecGoalPos1 - pEnt->GetAbsOrigin();
VectorNormalize( vecDir1 );
Vector vecDir2 = m_vecGoalPos2 - pEnt->GetAbsOrigin();
VectorNormalize( vecDir2 );
*/
linear = Vector( 0, 0, m_flFloat * GetCurrentGravity() );// + m_flFloat * 0.5 * ( vecDir1 + vecDir2 );
angular = Vector( 0, 0, 10 );
return SIM_GLOBAL_ACCELERATION;
}
}
//==============================================================================================
// ADVISOR PHYSICS DAMAGE TABLE
//==============================================================================================
static impactentry_t advisorLinearTable[] =
{
{ 100*100, 10 },
{ 250*250, 25 },
{ 350*350, 50 },
{ 500*500, 75 },
{ 1000*1000,100 },
};
static impactentry_t advisorAngularTable[] =
{
{ 50* 50, 10 },
{ 100*100, 25 },
{ 150*150, 50 },
{ 200*200, 75 },
};
static impactdamagetable_t gAdvisorImpactDamageTable =
{
advisorLinearTable,
advisorAngularTable,
ARRAYSIZE(advisorLinearTable),
ARRAYSIZE(advisorAngularTable),
200*200,// minimum linear speed squared
180*180,// minimum angular speed squared (360 deg/s to cause spin/slice damage)
15, // can't take damage from anything under 15kg
10, // anything less than 10kg is "small"
5, // never take more than 1 pt of damage from anything under 15kg
128*128,// <15kg objects must go faster than 36 in/s to do damage
45, // large mass in kg
2, // large mass scale (anything over 500kg does 4X as much energy to read from damage table)
1, // large mass falling scale
0, // my min velocity
};
//-----------------------------------------------------------------------------
// Purpose:
// Output : const impactdamagetable_t
//-----------------------------------------------------------------------------
const impactdamagetable_t &CNPC_Advisor::GetPhysicsImpactDamageTable( void )
{
return advisor_use_impact_table.GetBool() ? gAdvisorImpactDamageTable : BaseClass::GetPhysicsImpactDamageTable();
}
#if NPC_ADVISOR_HAS_BEHAVIOR
//-----------------------------------------------------------------------------
//
// Schedules
//
//-----------------------------------------------------------------------------
AI_BEGIN_CUSTOM_NPC( npc_advisor, CNPC_Advisor )
DECLARE_TASK( TASK_ADVISOR_FIND_OBJECTS )
DECLARE_TASK( TASK_ADVISOR_LEVITATE_OBJECTS )
/*
DECLARE_TASK( TASK_ADVISOR_PICK_THROW_OBJECT )
DECLARE_TASK( TASK_ADVISOR_THROW_OBJECT )
*/
DECLARE_CONDITION( COND_ADVISOR_PHASE_INTERRUPT ) // A stage has interrupted us
DECLARE_TASK( TASK_ADVISOR_STAGE_OBJECTS ) // haul all the objects into the throw-from slots
DECLARE_TASK( TASK_ADVISOR_BARRAGE_OBJECTS ) // hurl all the objects in sequence
DECLARE_TASK( TASK_ADVISOR_PIN_PLAYER ) // pinion the player to a point in space
//=========================================================
DEFINE_SCHEDULE
(
SCHED_ADVISOR_COMBAT,
" Tasks"
" TASK_ADVISOR_FIND_OBJECTS 0"
" TASK_ADVISOR_LEVITATE_OBJECTS 0"
" TASK_ADVISOR_STAGE_OBJECTS 1"
" TASK_ADVISOR_BARRAGE_OBJECTS 0"
" "
" Interrupts"
" COND_ADVISOR_PHASE_INTERRUPT"
" COND_ENEMY_DEAD"
)
//=========================================================
DEFINE_SCHEDULE
(
SCHED_ADVISOR_IDLE_STAND,
" Tasks"
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE"
" TASK_WAIT 3"
""
" Interrupts"
" COND_NEW_ENEMY"
" COND_SEE_FEAR"
" COND_ADVISOR_PHASE_INTERRUPT"
)
DEFINE_SCHEDULE
(
SCHED_ADVISOR_TOSS_PLAYER,
" Tasks"
" TASK_ADVISOR_FIND_OBJECTS 0"
" TASK_ADVISOR_LEVITATE_OBJECTS 0"
" TASK_ADVISOR_PIN_PLAYER 0"
" "
" Interrupts"
)
AI_END_CUSTOM_NPC()
#endif