diff options
Diffstat (limited to 'game/server/episodic/npc_advisor.cpp')
| -rw-r--r-- | game/server/episodic/npc_advisor.cpp | 2051 |
1 files changed, 2051 insertions, 0 deletions
diff --git a/game/server/episodic/npc_advisor.cpp b/game/server/episodic/npc_advisor.cpp new file mode 100644 index 0000000..1b7fb23 --- /dev/null +++ b/game/server/episodic/npc_advisor.cpp @@ -0,0 +1,2051 @@ +//========= 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 |