diff options
Diffstat (limited to 'game/server/episodic')
25 files changed, 20987 insertions, 0 deletions
diff --git a/game/server/episodic/ai_behavior_alyx_injured.cpp b/game/server/episodic/ai_behavior_alyx_injured.cpp new file mode 100644 index 0000000..a762587 --- /dev/null +++ b/game/server/episodic/ai_behavior_alyx_injured.cpp @@ -0,0 +1,621 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: FIXME: This will ultimately become a more generic implementation +// +//============================================================================= + +#include "cbase.h" +#include "ai_memory.h" +#include "ai_speech.h" +#include "ai_behavior.h" +#include "ai_navigator.h" +#include "ai_playerally.h" +#include "ai_behavior_follow.h" +#include "ai_moveprobe.h" + +#include "ai_behavior_alyx_injured.h" + +ConVar g_debug_injured_follow( "g_debug_injured_follow", "0" ); +ConVar injured_help_plee_range( "injured_help_plee_range", "256" ); + +#define TLK_INJURED_FOLLOW_TOO_FAR "TLK_INJURED_FOLLOW_TOO_FAR" + +BEGIN_DATADESC( CAI_BehaviorAlyxInjured ) + DEFINE_FIELD( m_flNextWarnTime, FIELD_TIME ), + // m_ActivityMap + +END_DATADESC(); + +Activity ACT_INJURED_COWER; +Activity ACT_GESTURE_INJURED_COWER_FLINCH; + +#define COVER_DISTANCE 128.0f // Distance behind target to find cover +#define MIN_ENEMY_MOB 3 // Number of enemies considerd overwhelming +#define MAX_DIST_FROM_FOLLOW_TARGET 256 // If the follow target is farther than this, the NPC will run to it + +//============================================================================= + +CAI_BehaviorAlyxInjured::CAI_BehaviorAlyxInjured( void ) : m_flNextWarnTime( 0.0f ) +{ + SetDefLessFunc( m_ActivityMap ); +} + +struct ActivityMap_t +{ + Activity activity; + Activity translation; +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_BehaviorAlyxInjured::PopulateActivityMap( void ) +{ + // Maps one activity to a translated one + ActivityMap_t map[] = + { + // Runs + { ACT_RUN, ACT_RUN_HURT }, + { ACT_RUN_AIM, ACT_RUN_AIM }, // FIMXE: No appropriate temp anim right now! + { ACT_RUN_CROUCH, ACT_RUN_HURT }, + { ACT_RUN_CROUCH_AIM, ACT_RUN_HURT }, + { ACT_RUN_PROTECTED, ACT_RUN_HURT }, + { ACT_RUN_RELAXED, ACT_RUN_HURT }, + { ACT_RUN_STIMULATED, ACT_RUN_HURT }, + { ACT_RUN_AGITATED, ACT_RUN_HURT }, + { ACT_RUN_AIM_RELAXED, ACT_RUN_AIM_RELAXED }, // FIMXE: No appropriate temp anim right now! + { ACT_RUN_AIM_STIMULATED, ACT_RUN_AIM_STIMULATED }, // FIMXE: No appropriate temp anim right now! + { ACT_RUN_AIM_AGITATED, ACT_RUN_AIM_AGITATED }, // FIMXE: No appropriate temp anim right now! + { ACT_RUN_HURT, ACT_RUN_HURT }, + + // Walks + { ACT_WALK, ACT_WALK_HURT }, + { ACT_WALK_AIM, ACT_WALK_HURT }, + { ACT_WALK_CROUCH, ACT_WALK_HURT }, + { ACT_WALK_CROUCH_AIM, ACT_WALK_HURT }, + { ACT_WALK_RELAXED, ACT_WALK_HURT }, + { ACT_WALK_STIMULATED, ACT_WALK_HURT }, + { ACT_WALK_AGITATED, ACT_WALK_HURT }, + { ACT_WALK_AIM_RELAXED, ACT_WALK_HURT }, + { ACT_WALK_AIM_STIMULATED, ACT_WALK_HURT }, + { ACT_WALK_AIM_AGITATED, ACT_WALK_HURT }, + { ACT_WALK_HURT, ACT_WALK_HURT }, + + { ACT_IDLE, ACT_IDLE_HURT }, + { ACT_COVER_LOW, ACT_INJURED_COWER }, + { ACT_COWER, ACT_INJURED_COWER }, + }; + + // Clear the map + m_ActivityMap.RemoveAll(); + + // Add all translations + for ( int i = 0; i < ARRAYSIZE( map ); i++ ) + { + Assert( m_ActivityMap.Find( map[i].activity ) == m_ActivityMap.InvalidIndex() ); + m_ActivityMap.Insert( map[i].activity, map[i].translation ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Populate the list after save/load +//----------------------------------------------------------------------------- +void CAI_BehaviorAlyxInjured::OnRestore( void ) +{ + PopulateActivityMap(); +} + +//----------------------------------------------------------------------------- +// Purpose: Populate the list on spawn +//----------------------------------------------------------------------------- +void CAI_BehaviorAlyxInjured::Spawn( void ) +{ + PopulateActivityMap(); +} + +//----------------------------------------------------------------------------- +// Purpose: Get the flinch activity for us to play +// Input : bHeavyDamage - +// bGesture - +// Output : Activity +//----------------------------------------------------------------------------- +Activity CAI_BehaviorAlyxInjured::GetFlinchActivity( bool bHeavyDamage, bool bGesture ) +{ + // + if ( ( bGesture == false ) || ( GetOuter()->GetActivity() != ACT_COWER ) ) + return BaseClass::GetFlinchActivity( bHeavyDamage, bGesture ); + + // Translate the flinch if we're cowering + return ACT_GESTURE_INJURED_COWER_FLINCH; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : nActivity - +//----------------------------------------------------------------------------- +Activity CAI_BehaviorAlyxInjured::NPC_TranslateActivity( Activity nActivity ) +{ + // Find out what the base class wants to do with the activity + Activity nNewActivity = BaseClass::NPC_TranslateActivity( nActivity ); + + // Look it up in the translation map + int nIndex = m_ActivityMap.Find( nNewActivity ); + + if ( m_ActivityMap.IsValidIndex( nIndex ) ) + return m_ActivityMap[nIndex]; + + return nNewActivity; +} + +//----------------------------------------------------------------------------- +// Purpose: Determines if Alyx should run away from enemies or stay put +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_BehaviorAlyxInjured::ShouldRunToCover( void ) +{ + Vector vecRetreatPos; + float flRetreatRadius = 128.0f; + + // See how far off from our cover position we are + if ( FindCoverFromEnemyBehindTarget( GetFollowTarget(), flRetreatRadius, &vecRetreatPos ) ) + { + float flDestDistSqr = ( GetOuter()->WorldSpaceCenter() - vecRetreatPos ).LengthSqr(); + if ( flDestDistSqr > Square( flRetreatRadius ) ) + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: See if we need to follow our goal +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_BehaviorAlyxInjured::ShouldRunToFollowGoal( void ) +{ + // If we're too far from our follow target, we need to chase after them + float flDistToFollowGoalSqr = ( GetOuter()->GetAbsOrigin() - GetFollowTarget()->GetAbsOrigin() ).LengthSqr(); + if ( flDistToFollowGoalSqr > Square(MAX_DIST_FROM_FOLLOW_TARGET) ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Translate base schedules into overridden forms +//----------------------------------------------------------------------------- +int CAI_BehaviorAlyxInjured::TranslateSchedule( int scheduleType ) +{ + switch( scheduleType ) + { + case SCHED_RUN_FROM_ENEMY: + case SCHED_RUN_FROM_ENEMY_MOB: + { + // Get under cover if we're able to + if ( ShouldRunToCover() ) + return SCHED_INJURED_RUN_FROM_ENEMY; + + // Run to our follow goal if we're too far away from it + if ( ShouldRunToFollowGoal() ) + return SCHED_FOLLOW; + + // Cower if surrounded + if ( HasCondition( COND_INJURED_OVERWHELMED ) ) + return SCHED_INJURED_COWER; + + // Face our enemies + return SCHED_INJURED_FEAR_FACE; + } + break; + + case SCHED_RUN_FROM_ENEMY_FALLBACK: + return SCHED_INJURED_COWER; + break; + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + +//----------------------------------------------------------------------------- +// Purpose: Pick up failure cases and handle them +//----------------------------------------------------------------------------- +int CAI_BehaviorAlyxInjured::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode ) +{ + // Failed schedules + switch( failedSchedule ) + { + case SCHED_RUN_FROM_ENEMY: + case SCHED_RUN_FROM_ENEMY_MOB: + case SCHED_FOLLOW: + return SCHED_INJURED_COWER; + } + + // Failed tasks + switch( failedTask ) + { + case TASK_FIND_COVER_FROM_ENEMY: + case TASK_FIND_INJURED_COVER_FROM_ENEMY: + + // Only cower if we're already near enough to our follow target + float flDistToFollowTargetSqr = ( GetOuter()->GetAbsOrigin() - GetFollowTarget()->GetAbsOrigin() ).LengthSqr(); + if ( flDistToFollowTargetSqr > Square( 256 ) ) + return SCHED_FOLLOW; + + return SCHED_INJURED_COWER; + break; + } + + return BaseClass::SelectFailSchedule( failedSchedule, failedTask, taskFailCode ); +} + +//----------------------------------------------------------------------------- +// Purpose: Find the general direction enemies are coming towards us at +//----------------------------------------------------------------------------- +bool CAI_BehaviorAlyxInjured::FindThreatDirection2D( const Vector &vecSource, Vector *vecOut ) +{ + // Find the general direction our threat is coming from + bool bValid = false; + Vector vecScratch; + AIEnemiesIter_t iter; + + // Iterate through all known enemies + for( AI_EnemyInfo_t *pMemory = GetOuter()->GetEnemies()->GetFirst(&iter); pMemory != NULL; pMemory = GetOuter()->GetEnemies()->GetNext(&iter) ) + { + if ( pMemory == NULL || pMemory->hEnemy == NULL ) + continue; + + vecScratch = ( vecSource - pMemory->hEnemy->WorldSpaceCenter() ); + VectorNormalize( vecScratch ); + + (*vecOut) += vecScratch; + bValid = true; + } + + // Find the general direction + (*vecOut).z = 0.0f; + VectorNormalize( (*vecOut) ); + return bValid; +} + +//----------------------------------------------------------------------------- +// Purpose: Find a position that hides us from our threats while interposing the +// target entity between us and the threat +// Input : pTarget - entity to hide behind +// flRadius - Radius around the target to search +// *vecOut - position +//----------------------------------------------------------------------------- +bool CAI_BehaviorAlyxInjured::FindCoverFromEnemyBehindTarget( CBaseEntity *pTarget, float flRadius, Vector *vecOut ) +{ + if ( pTarget == NULL ) + return false; + + Vector vecTargetPos = pTarget->GetAbsOrigin(); + Vector vecThreatDir = vec3_origin; + + // Find our threat direction and base our cover on that + if ( FindThreatDirection2D( vecTargetPos, &vecThreatDir ) ) + { + // Get a general location for taking cover + Vector vecTestPos = vecTargetPos + ( vecThreatDir * flRadius ); + + if ( g_debug_injured_follow.GetBool() ) + { + NDebugOverlay::HorzArrow( GetOuter()->GetAbsOrigin(), vecTestPos, 8.0f, 255, 255, 0, 32, true, 2.0f ); + } + + // Make sure we never move towards our threat to get to cover! + Vector vecMoveDir = GetOuter()->GetAbsOrigin() - vecTestPos; + VectorNormalize( vecMoveDir ); + float flDotToCover = DotProduct( vecMoveDir, vecThreatDir ); + if ( flDotToCover > 0.0f ) + { + if ( g_debug_injured_follow.GetBool() ) + { + NDebugOverlay::HorzArrow( GetOuter()->GetAbsOrigin(), vecTestPos, 8.0f, 255, 0, 0, 32, true, 2.0f ); + } + + return false; + } + + AIMoveTrace_t moveTrace; + GetOuter()->GetMoveProbe()->MoveLimit( NAV_GROUND, + GetOuter()->GetAbsOrigin(), + vecTestPos, + MASK_SOLID_BRUSHONLY, + NULL, + 0, + &moveTrace ); + + bool bWithinRangeToGoal = ( moveTrace.vEndPosition - vecTestPos ).Length2DSqr() < Square( GetOuter()->GetHullWidth() * 3.0f ); + bool bCanStandAtGoal = GetOuter()->GetMoveProbe()->CheckStandPosition( moveTrace.vEndPosition, MASK_SOLID_BRUSHONLY ); + + if ( bWithinRangeToGoal == false || bCanStandAtGoal == false ) + { + if ( g_debug_injured_follow.GetBool() ) + { + NDebugOverlay::SweptBox( GetOuter()->GetAbsOrigin(), vecTestPos, GetOuter()->GetHullMins(), GetOuter()->GetHullMaxs(), vec3_angle, 255, 0, 0, 0, 2.0f ); + } + + return false; + } + + // Accept it + *vecOut = moveTrace.vEndPosition; + + if ( g_debug_injured_follow.GetBool() ) + { + NDebugOverlay::SweptBox( GetOuter()->GetAbsOrigin(), (*vecOut), GetOuter()->GetHullMins(), GetOuter()->GetHullMaxs(), vec3_angle, 0, 255, 0, 0, 2.0f ); + } + + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pTask - +//----------------------------------------------------------------------------- +void CAI_BehaviorAlyxInjured::StartTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_FIND_COVER_FROM_ENEMY: + { + CBaseEntity *pLeader = GetFollowTarget(); + if ( !pLeader ) + { + BaseClass::StartTask( pTask ); + break; + } + + // Find a position behind our follow target + Vector coverPos = vec3_invalid; + if ( FindCoverFromEnemyBehindTarget( pLeader, COVER_DISTANCE, &coverPos ) ) + { + AI_NavGoal_t goal( GOALTYPE_LOCATION, coverPos, ACT_RUN, AIN_HULL_TOLERANCE, AIN_DEF_FLAGS ); + GetOuter()->GetNavigator()->SetGoal( goal ); + GetOuter()->m_flMoveWaitFinished = gpGlobals->curtime + pTask->flTaskData; + TaskComplete(); + return; + } + + // Couldn't find anything + TaskFail( FAIL_NO_COVER ); + break; + } + + default: + BaseClass::StartTask( pTask ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Whether or not Alyx is injured +//----------------------------------------------------------------------------- +bool CAI_BehaviorAlyxInjured::IsInjured( void ) const +{ + return IsAlyxInInjuredMode(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_BehaviorAlyxInjured::GatherConditions( void ) +{ + BaseClass::GatherConditions(); + + // Always stomp over this + ClearCondition( COND_INJURED_TOO_FAR_FROM_PLAYER ); + ClearCondition( COND_INJURED_OVERWHELMED ); + + // See if we're overwhelmed by foes + if ( NumKnownEnemiesInRadius( GetOuter()->GetAbsOrigin(), COVER_DISTANCE ) >= MIN_ENEMY_MOB ) + { + SetCondition( COND_INJURED_OVERWHELMED ); + } + + // Determines whether we consider ourselves in danger + bool bInDanger = ( HasCondition( COND_LIGHT_DAMAGE ) || + HasCondition( COND_HEAVY_DAMAGE ) || + HasCondition( COND_INJURED_OVERWHELMED ) ); + + // See if we're too far away from the player and in danger + if ( AI_IsSinglePlayer() && bInDanger ) + { + bool bWarnPlayer = false; + + // This only works in single-player + CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); + if ( pPlayer != NULL ) + { + // FIXME: This distance may need to be the length of the shortest walked path between the follower and the target + + // Get our approximate distance to the player + float flDistToPlayer = UTIL_DistApprox2D( GetOuter()->GetAbsOrigin(), pPlayer->GetAbsOrigin() ); + if ( flDistToPlayer > injured_help_plee_range.GetFloat() ) + { + bWarnPlayer = true; + } + else if ( flDistToPlayer > (injured_help_plee_range.GetFloat()*0.5f) && HasCondition( COND_SEE_PLAYER ) == false ) + { + // Cut our distance in half if we can't see the player + bWarnPlayer = true; + } + } + + // Yell for help! + if ( bWarnPlayer ) + { + // FIXME: This should be routed through the normal speaking code with a system to emit from the player's suit. + + CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); + //float flPlayerDistSqr = ( GetOuter()->GetAbsOrigin() - pPlayer->GetAbsOrigin() ).LengthSqr(); + + // If the player is too far away or we can't see him + //if ( HasCondition( COND_SEE_PLAYER ) == false || flPlayerDistSqr > Square( 128 ) ) + { + if ( m_flNextWarnTime < gpGlobals->curtime ) + { + pPlayer->EmitSound( "npc_alyx.injured_too_far" ); + m_flNextWarnTime = gpGlobals->curtime + random->RandomFloat( 3.0f, 5.0f ); + } + } + /* + else + { + SpeakIfAllowed( TLK_INJURED_FOLLOW_TOO_FAR ); + m_flNextWarnTime = gpGlobals->curtime + random->RandomFloat( 3.0f, 5.0f ); + } + */ + + SetCondition( COND_INJURED_TOO_FAR_FROM_PLAYER ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Speak a concept if we're able to +//----------------------------------------------------------------------------- +void CAI_BehaviorAlyxInjured::SpeakIfAllowed( AIConcept_t concept ) +{ + CAI_Expresser *pExpresser = GetOuter()->GetExpresser(); + if ( pExpresser == NULL ) + return; + + // Must be able to speak the concept + if ( pExpresser->CanSpeakConcept( concept ) ) + { + pExpresser->Speak( concept ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Get the number of known enemies within a radius to a point +//----------------------------------------------------------------------------- +int CAI_BehaviorAlyxInjured::NumKnownEnemiesInRadius( const Vector &vecSource, float flRadius ) +{ + int nNumEnemies = 0; + float flRadiusSqr = Square( flRadius ); + + AIEnemiesIter_t iter; + + // Iterate through all known enemies + for( AI_EnemyInfo_t *pMemory = GetEnemies()->GetFirst(&iter); pMemory != NULL; pMemory = GetEnemies()->GetNext(&iter) ) + { + if ( pMemory == NULL || pMemory->hEnemy == NULL ) + continue; + + // Must hate or fear them + if ( GetOuter()->IRelationType( pMemory->hEnemy ) != D_HT && GetOuter()->IRelationType( pMemory->hEnemy ) != D_FR ) + continue; + + // Count only the enemies I've seen recently + if ( gpGlobals->curtime - pMemory->timeLastSeen > 0.5f ) + continue; + + // Must be within the radius we've specified + float flEnemyDistSqr = ( vecSource - pMemory->hEnemy->GetAbsOrigin() ).Length2DSqr(); + if ( flEnemyDistSqr < flRadiusSqr ) + { + nNumEnemies++; + } + } + + return nNumEnemies; +} + +// ---------------------------------------------- +// Custom AI declarations +// ---------------------------------------------- + +AI_BEGIN_CUSTOM_SCHEDULE_PROVIDER( CAI_BehaviorAlyxInjured ) +{ + DECLARE_ACTIVITY( ACT_GESTURE_INJURED_COWER_FLINCH ) + DECLARE_ACTIVITY( ACT_INJURED_COWER ) + + DECLARE_CONDITION( COND_INJURED_TOO_FAR_FROM_PLAYER ) + DECLARE_CONDITION( COND_INJURED_OVERWHELMED ) + + DECLARE_TASK( TASK_FIND_INJURED_COVER_FROM_ENEMY ) + + DEFINE_SCHEDULE + ( + SCHED_INJURED_COWER, + + " Tasks" + // TOOD: Announce cower + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_COWER" + " TASK_WAIT 2" + "" + " Interrupts" + " COND_GIVE_WAY" + " COND_PLAYER_PUSHING" + ) + + DEFINE_SCHEDULE + ( + SCHED_INJURED_FEAR_FACE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" // FIXME: Scared idle? + " TASK_FACE_ENEMY 0" + "" + " Interrupts" + " COND_GIVE_WAY" + " COND_PLAYER_PUSHING" + ); + + DEFINE_SCHEDULE + ( + SCHED_INJURED_RUN_FROM_ENEMY, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_INJURED_COWER" + " TASK_STOP_MOVING 0" + " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + "" + " Interrupts" + ); + + AI_END_CUSTOM_SCHEDULE_PROVIDER() +} + +//----------------------------------------------------------------------------- +// CAI_InjuredFollowGoal +//----------------------------------------------------------------------------- + +BEGIN_DATADESC( CAI_InjuredFollowGoal ) +END_DATADESC() + +LINK_ENTITY_TO_CLASS( ai_goal_injured_follow, CAI_InjuredFollowGoal ); + +//------------------------------------- + +void CAI_InjuredFollowGoal::EnableGoal( CAI_BaseNPC *pAI ) +{ + CAI_BehaviorAlyxInjured *pBehavior; + if ( !pAI->GetBehavior( &pBehavior ) ) + return; + + if ( GetGoalEntity() == NULL ) + return; + + pBehavior->SetFollowGoal( this ); +} + +//------------------------------------- + +void CAI_InjuredFollowGoal::DisableGoal( CAI_BaseNPC *pAI ) +{ + CAI_BehaviorAlyxInjured *pBehavior; + if ( !pAI->GetBehavior( &pBehavior ) ) + return; + + pBehavior->ClearFollowGoal( this ); +} diff --git a/game/server/episodic/ai_behavior_alyx_injured.h b/game/server/episodic/ai_behavior_alyx_injured.h new file mode 100644 index 0000000..2eb1ff1 --- /dev/null +++ b/game/server/episodic/ai_behavior_alyx_injured.h @@ -0,0 +1,95 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: FIXME: This will ultimately become a more generic implementation +// +//============================================================================= + +#ifndef AI_BEHAVIOR_ALYX_INJURED_H +#define AI_BEHAVIOR_ALYX_INJURED_H +#ifdef _WIN32 +#pragma once +#endif + +#include "utlmap.h" + +extern bool IsAlyxInInjuredMode( void ); + +// +// +// + +class CAI_InjuredFollowGoal : public CAI_FollowGoal +{ + DECLARE_CLASS( CAI_InjuredFollowGoal, CAI_FollowGoal ); + +public: + + virtual void EnableGoal( CAI_BaseNPC *pAI ); + virtual void DisableGoal( CAI_BaseNPC *pAI ); + + DECLARE_DATADESC(); +}; + +// +// +// + +class CAI_BehaviorAlyxInjured : public CAI_FollowBehavior +{ + DECLARE_CLASS( CAI_BehaviorAlyxInjured, CAI_FollowBehavior ); + DECLARE_DATADESC(); + +public: + CAI_BehaviorAlyxInjured( void ); + + virtual const char *GetName( void ) { return "AlyxInjuredFollow"; } + virtual Activity NPC_TranslateActivity( Activity nActivity ); + virtual int TranslateSchedule( int scheduleType ); + virtual void Spawn( void ); + virtual void OnRestore( void ); + virtual void StartTask( const Task_t *pTask ); + virtual int SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode ); + virtual void GatherConditions( void ); + virtual Activity GetFlinchActivity( bool bHeavyDamage, bool bGesture ); + + enum + { + // Schedules + SCHED_INJURED_COWER = BaseClass::NEXT_SCHEDULE, + SCHED_INJURED_FEAR_FACE, + SCHED_INJURED_RUN_FROM_ENEMY, + NEXT_SCHEDULE, + + // Tasks + TASK_FIND_INJURED_COVER_FROM_ENEMY = BaseClass::NEXT_TASK, + NEXT_TASK, + + // Conditions + COND_INJURED_TOO_FAR_FROM_PLAYER = BaseClass::NEXT_CONDITION, + COND_INJURED_OVERWHELMED, + NEXT_CONDITION + }; + + bool IsReadinessCapable( void ) { return ( IsInjured() == false ); } // Never use the readiness system when injured + bool IsInjured( void ) const; + +private: + + void SpeakIfAllowed( AIConcept_t concept ); + bool ShouldRunToCover( void ); + bool ShouldRunToFollowGoal( void ); + bool FindThreatDirection2D( const Vector &vecSource, Vector *vecOut ); + bool FindCoverFromEnemyBehindTarget( CBaseEntity *pTarget, float flRadius, Vector *vecOut ); + void PopulateActivityMap( void ); + int NumKnownEnemiesInRadius( const Vector &vecSource, float flRadius ); + + CUtlMap<Activity,Activity> m_ActivityMap; + + float m_flNextWarnTime; + +protected: + DEFINE_CUSTOM_SCHEDULE_PROVIDER; +}; + + +#endif // AI_BEHAVIOR_ALYX_INJURED_H diff --git a/game/server/episodic/ai_behavior_passenger_companion.cpp b/game/server/episodic/ai_behavior_passenger_companion.cpp new file mode 100644 index 0000000..f96c2a5 --- /dev/null +++ b/game/server/episodic/ai_behavior_passenger_companion.cpp @@ -0,0 +1,2053 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Companion NPCs riding in cars +// +//============================================================================= + +#include "cbase.h" +#include "ai_speech.h" +#include "ai_pathfinder.h" +#include "ai_waypoint.h" +#include "ai_navigator.h" +#include "ai_navgoaltype.h" +#include "ai_memory.h" +#include "ai_behavior_passenger_companion.h" +#include "ai_squadslot.h" +#include "npc_playercompanion.h" +#include "ai_route.h" +#include "saverestore_utlvector.h" +#include "cplane.h" +#include "util_shared.h" +#include "sceneentity.h" + +bool SphereWithinPlayerFOV( CBasePlayer *pPlayer, const Vector &vecCenter, float flRadius ); + +#define PASSENGER_NEAR_VEHICLE_THRESHOLD 64.0f + +#define MIN_OVERTURNED_DURATION 1.0f // seconds +#define MIN_FAILED_EXIT_ATTEMPTS 4 +#define MIN_OVERTURNED_WARN_DURATION 4.0f // seconds + +ConVar passenger_collision_response_threshold( "passenger_collision_response_threshold", "250.0" ); +ConVar passenger_debug_entry( "passenger_debug_entry", "0" ); +ConVar passenger_use_leaning("passenger_use_leaning", "1" ); +extern ConVar passenger_debug_transition; + +// Custom activities +Activity ACT_PASSENGER_IDLE_AIM; +Activity ACT_PASSENGER_RELOAD; +Activity ACT_PASSENGER_OVERTURNED; +Activity ACT_PASSENGER_IMPACT; +Activity ACT_PASSENGER_IMPACT_WEAPON; +Activity ACT_PASSENGER_POINT; +Activity ACT_PASSENGER_POINT_BEHIND; +Activity ACT_PASSENGER_IDLE_READY; +Activity ACT_PASSENGER_GESTURE_JOSTLE_LARGE; +Activity ACT_PASSENGER_GESTURE_JOSTLE_SMALL; +Activity ACT_PASSENGER_GESTURE_JOSTLE_LARGE_STIMULATED; +Activity ACT_PASSENGER_GESTURE_JOSTLE_SMALL_STIMULATED; +Activity ACT_PASSENGER_COWER_IN; +Activity ACT_PASSENGER_COWER_LOOP; +Activity ACT_PASSENGER_COWER_OUT; +Activity ACT_PASSENGER_IDLE_FIDGET; + +BEGIN_DATADESC( CAI_PassengerBehaviorCompanion ) + + DEFINE_EMBEDDED( m_VehicleMonitor ), + + DEFINE_UTLVECTOR( m_FailedEntryPositions, FIELD_EMBEDDED ), + + DEFINE_FIELD( m_flOverturnedDuration, FIELD_FLOAT ), + DEFINE_FIELD( m_flUnseenDuration, FIELD_FLOAT ), + DEFINE_FIELD( m_nExitAttempts, FIELD_INTEGER ), + DEFINE_FIELD( m_flNextOverturnWarning, FIELD_TIME ), + DEFINE_FIELD( m_flEnterBeginTime, FIELD_TIME ), + DEFINE_FIELD( m_hCompanion, FIELD_EHANDLE ), + DEFINE_FIELD( m_flNextJostleTime, FIELD_TIME ), + DEFINE_FIELD( m_nVisibleEnemies, FIELD_INTEGER ), + DEFINE_FIELD( m_flLastLateralLean, FIELD_FLOAT ), + DEFINE_FIELD( m_flEntraceUpdateTime, FIELD_TIME ), + DEFINE_FIELD( m_flNextEnterAttempt, FIELD_TIME ), + DEFINE_FIELD( m_flNextFidgetTime, FIELD_TIME ), + +END_DATADESC(); + +BEGIN_SIMPLE_DATADESC( FailPosition_t ) + + DEFINE_FIELD( vecPosition, FIELD_VECTOR ), + DEFINE_FIELD( flTime, FIELD_TIME ), + +END_DATADESC(); + +CAI_PassengerBehaviorCompanion::CAI_PassengerBehaviorCompanion( void ) : +m_flUnseenDuration( 0.0f ), +m_flNextOverturnWarning( 0.0f ), +m_flOverturnedDuration( 0.0f ), +m_nExitAttempts( 0 ), +m_flNextEnterAttempt( 0.0f ), +m_flLastLateralLean( 0.0f ), +m_flNextJostleTime( 0.0f ) +{ + memset( &m_vehicleState, 0, sizeof( m_vehicleState ) ); + m_VehicleMonitor.ClearMark(); +} + +void CAI_PassengerBehaviorCompanion::Enable( CPropJeepEpisodic *pVehicle, bool bImmediateEnter /*= false*/ ) +{ + BaseClass::Enable( pVehicle ); + + // Store this up for quick reference later on + m_hCompanion = dynamic_cast<CNPC_PlayerCompanion *>(GetOuter()); + + // See if we want to sit in the vehicle immediately + if ( bImmediateEnter ) + { + // Find the seat and sit in it + if ( ReserveEntryPoint( VEHICLE_SEAT_ANY ) ) + { + // Attach + AttachToVehicle(); + + // This will slam us into the right position and clean up + FinishEnterVehicle(); + GetOuter()->IncrementInterpolationFrame(); + + // Start our schedule immediately + ClearSchedule( "Immediate entry to vehicle" ); + } + } +} + +//----------------------------------------------------------------------------- +// Set up the shot regulator based on the equipped weapon +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::OnUpdateShotRegulator( void ) +{ + if ( GetVehicleSpeed() > 250 ) + { + // Default values + GetOuter()->GetShotRegulator()->SetBurstInterval( 0.1f, 0.5f ); + GetOuter()->GetShotRegulator()->SetBurstShotCountRange( 1, 4 ); + GetOuter()->GetShotRegulator()->SetRestInterval( 0.25f, 1.0f ); + } + else + { + BaseClass::OnUpdateShotRegulator(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::IsValidEnemy( CBaseEntity *pEntity ) +{ + // The target must be much closer in the vehicle + float flDistSqr = ( pEntity->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); + if ( flDistSqr > Square( (40*12) ) && pEntity->Classify() != CLASS_BULLSEYE ) + return false; + + // Determine if the target is going to move past us? + return BaseClass::IsValidEnemy( pEntity ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the speed the vehicle is moving at +// Output : units per second +//----------------------------------------------------------------------------- +float CAI_PassengerBehaviorCompanion::GetVehicleSpeed( void ) +{ + if ( m_hVehicle == NULL ) + { + Assert(0); + return -1.0f; + } + + Vector vecVelocity; + m_hVehicle->GetVelocity( &vecVelocity, NULL ); + + // Get our speed + return vecVelocity.Length(); +} + +//----------------------------------------------------------------------------- +// Purpose: Detect oncoming collisions +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::GatherVehicleCollisionConditions( const Vector &localVelocity ) +{ + // Look for walls in front of us + if ( localVelocity.y > passenger_collision_response_threshold.GetFloat() ) + { + // Detect an upcoming collision + Vector vForward; + m_hVehicle->GetVectors( &vForward, NULL, NULL ); + + // Use a smaller bounding box to make it detect mostly head-on impacts + Vector mins, maxs; + mins.Init( -24, -24, 32 ); + maxs.Init( 24, 24, 64 ); + + float dt = 0.6f; // Seconds + float distance = localVelocity.y * dt; + + // Find our angular velocity as a vector + Vector vecAngularVelocity; + vecAngularVelocity.z = 0.0f; + SinCos( DEG2RAD( m_vehicleState.m_vecLastAngles.z * dt ), &vecAngularVelocity.y, &vecAngularVelocity.x ); + + Vector vecOffset; + VectorRotate( vecAngularVelocity, m_hVehicle->GetAbsAngles() + QAngle( 0, 90, 0 ), vecOffset ); + + vForward += vecOffset; + VectorNormalize( vForward ); + + // Trace ahead of us to see what's there + CTraceFilterNoNPCsOrPlayer filter( m_hVehicle, COLLISION_GROUP_NONE ); // We don't care about NPCs or the player (certainly if they're in the vehicle!) + + trace_t tr; + UTIL_TraceHull( m_hVehicle->GetAbsOrigin(), m_hVehicle->GetAbsOrigin() + ( vForward * distance ), mins, maxs, MASK_SOLID, &filter, &tr ); + + bool bWarnCollision = true; + if ( tr.DidHit() ) + { + // We need to see how "head-on" to the surface we are + float impactDot = DotProduct( tr.plane.normal, vForward ); + + // Don't warn over grazing blows or slopes + if ( impactDot < -0.9f && tr.plane.normal.z < 0.75f ) + { + // Make sure this is a worthwhile thing to warn about + if ( tr.m_pEnt ) + { + // If it's physical and moveable, then ignore it because we'll probably smash or move it + IPhysicsObject *pObject = tr.m_pEnt->VPhysicsGetObject(); + if ( pObject && pObject->IsMoveable() ) + { + bWarnCollision = false; + } + } + + // Note that we should say something to the player about it + if ( bWarnCollision ) + { + SetCondition( COND_PASSENGER_WARN_COLLISION ); + } + } + } + } + + if ( passenger_use_leaning.GetBool() ) + { + // Calculate how our body is leaning + CalculateBodyLean(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Speak various lines about the state of the vehicle +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::SpeakVehicleConditions( void ) +{ + Assert( m_hVehicle != NULL ); + if ( m_hVehicle == NULL ) + return; + + // Speak if we just hit something + if ( HasCondition( COND_PASSENGER_HARD_IMPACT ) ) + { + SpeakIfAllowed( TLK_PASSENGER_IMPACT ); + } + + // Speak if we're overturned + if ( HasCondition( COND_PASSENGER_OVERTURNED ) ) + { + SpeakIfAllowed( TLK_PASSENGER_OVERTURNED ); + } + + // Speak if we're about to hit something + if ( HasCondition( COND_PASSENGER_WARN_COLLISION ) ) + { + // Make Alyx look at the impending impact + Vector vecForward; + m_hVehicle->GetVectors( &vecForward, NULL, NULL ); + Vector vecLookPos = m_hVehicle->WorldSpaceCenter() + ( vecForward * 64.0f ); + GetOuter()->AddLookTarget( vecLookPos, 1.0f, 1.0f ); + + SpeakIfAllowed( TLK_PASSENGER_WARN_COLLISION ); + ClearCondition( COND_PASSENGER_WARN_COLLISION ); + } + + // Speak if the player is driving like a madman + if ( HasCondition( COND_PASSENGER_ERRATIC_DRIVING ) ) + { + SpeakIfAllowed( TLK_PASSENGER_ERRATIC_DRIVING ); + } + + // The vehicle has come to a halt + if ( HasCondition( COND_PASSENGER_VEHICLE_STOPPED ) ) + { + float flDist = ( WorldSpaceCenter() - m_hVehicle->WorldSpaceCenter() ).Length(); + CFmtStrN<128> modifiers( "vehicle_distance:%f", flDist ); + SpeakIfAllowed( TLK_PASSENGER_VEHICLE_STOPPED, modifiers ); + } + + // The vehicle has started to move + if ( HasCondition( COND_PASSENGER_VEHICLE_STARTED ) ) + { + float flDist = ( WorldSpaceCenter() - m_hVehicle->WorldSpaceCenter() ).Length(); + CFmtStrN<128> modifiers( "vehicle_distance:%f", flDist ); + SpeakIfAllowed( TLK_PASSENGER_VEHICLE_STARTED, modifiers ); + } + + // Player got in + if ( HasCondition( COND_PASSENGER_PLAYER_EXITED_VEHICLE ) ) + { + CPropJeepEpisodic *pJalopy = dynamic_cast<CPropJeepEpisodic*>(m_hVehicle.Get()); + if( pJalopy != NULL && pJalopy->NumRadarContacts() > 0 ) + { + SpeakIfAllowed( TLK_PASSENGER_PLAYER_EXITED, "radar_has_targets" ); + } + else + { + SpeakIfAllowed( TLK_PASSENGER_PLAYER_EXITED ); + } + } + + // Player got out + if ( HasCondition( COND_PASSENGER_PLAYER_ENTERED_VEHICLE ) ) + { + SpeakIfAllowed( TLK_PASSENGER_PLAYER_ENTERED ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Whether or not we should jostle at this moment +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::CanPlayJostle( bool bLargeJostle ) +{ + // We've been told to suppress the jostle + if ( m_flNextJostleTime > gpGlobals->curtime ) + return false; + + // Can't do this if we're at a high readiness level + if ( m_hCompanion && m_hCompanion->ShouldBeAiming() ) + return false; + + // Can't do this when we're upside-down + if ( HasCondition( COND_PASSENGER_OVERTURNED ) ) + return false; + + // Allow our normal impact code to handle this one instead + if ( HasCondition( COND_PASSENGER_HARD_IMPACT ) || IsCurSchedule( SCHED_PASSENGER_IMPACT ) ) + return false; + + if ( bLargeJostle ) + { + // Don't bother under certain circumstances + if ( IsCurSchedule( SCHED_PASSENGER_COWER ) || + IsCurSchedule( SCHED_PASSENGER_FIDGET ) ) + return false; + } + else + { + // Don't interrupt a larger gesture + if ( GetOuter()->IsPlayingGesture( ACT_PASSENGER_GESTURE_JOSTLE_LARGE ) || GetOuter()->IsPlayingGesture( ACT_PASSENGER_GESTURE_JOSTLE_LARGE_STIMULATED ) ) + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Gather conditions we can comment on or react to while riding in the vehicle +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::GatherVehicleStateConditions( void ) +{ + // Gather the base class + BaseClass::GatherVehicleStateConditions(); + + // See if we're going to collide with anything soon + GatherVehicleCollisionConditions( m_vehicleState.m_vecLastLocalVelocity ); + + // Say anything we're meant to through the response rules + SpeakVehicleConditions(); +} + +//----------------------------------------------------------------------------- +// Purpose: Handles exit failure notifications +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::OnExitVehicleFailed( void ) +{ + m_nExitAttempts++; +} + +//----------------------------------------------------------------------------- +// Purpose: Track how long we've been overturned +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::UpdateStuckStatus( void ) +{ + if ( m_hVehicle == NULL ) + return; + + // Always clear this to start out with + ClearCondition( COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE ); + + // If we can't exit the vehicle, then don't bother with these checks + if ( m_hVehicle->NPC_CanExitVehicle( GetOuter(), true ) == false ) + return; + + bool bVisibleToPlayer = false; + bool bPlayerInVehicle = false; + + CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); + if ( pPlayer ) + { + bVisibleToPlayer = pPlayer->FInViewCone( GetOuter()->GetAbsOrigin() ); + bPlayerInVehicle = pPlayer->IsInAVehicle(); + } + + // If we're not overturned, just reset our counter + if ( m_vehicleState.m_bWasOverturned == false ) + { + m_flOverturnedDuration = 0.0f; + m_flUnseenDuration = 0.0f; + } + else + { + // Add up the time since we last checked + m_flOverturnedDuration += ( gpGlobals->curtime - GetLastThink() ); + } + + // Warn about being stuck upside-down if it's been long enough + if ( m_flOverturnedDuration > MIN_OVERTURNED_WARN_DURATION && m_flNextOverturnWarning < gpGlobals->curtime ) + { + SetCondition( COND_PASSENGER_WARN_OVERTURNED ); + } + + // If the player can see us or is still in the vehicle, we never exit + if ( bVisibleToPlayer || bPlayerInVehicle ) + { + // Reset our timer + m_flUnseenDuration = 0.0f; + return; + } + + // Add up the time since we last checked + m_flUnseenDuration += ( gpGlobals->curtime - GetLastThink() ); + + // If we've been overturned for long enough or tried to exit one too many times + if ( m_vehicleState.m_bWasOverturned ) + { + if ( m_flUnseenDuration > MIN_OVERTURNED_DURATION ) + { + SetCondition( COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE ); + } + } + else if ( m_nExitAttempts >= MIN_FAILED_EXIT_ATTEMPTS ) + { + // The player can't be looking at us + SetCondition( COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Gather conditions for our use in making decisions +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::GatherConditions( void ) +{ + // Code below relies on these conditions being set first! + BaseClass::GatherConditions(); + + // We're not enabled + if ( IsEnabled() == false ) + return; + + // In-car conditions + if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) + { + // If we're jostling, then note that + if ( HasCondition( COND_PASSENGER_ERRATIC_DRIVING ) ) + { + if ( CanPlayJostle( true ) ) + { + // Add the gesture to be played. If it's already playing, the underlying function will simply opt-out + int nSequence = GetOuter()->AddGesture( GetOuter()->NPC_TranslateActivity( ACT_PASSENGER_GESTURE_JOSTLE_LARGE ), true ); + + GetOuter()->SetNextAttack( gpGlobals->curtime + ( GetOuter()->SequenceDuration( nSequence ) * 2.0f ) ); + GetOuter()->GetShotRegulator()->FireNoEarlierThan( GetOuter()->GetNextAttack() ); + + // Push out our fidget into the future so that we don't act unnaturally over bumpy terrain + ExtendFidgetDelay( random->RandomFloat( 1.5f, 3.0f ) ); + } + } + else if ( HasCondition( COND_PASSENGER_JOSTLE_SMALL ) ) + { + if ( CanPlayJostle( false ) ) + { + // Add the gesture to be played. If it's already playing, the underlying function will simply opt-out + GetOuter()->AddGesture( GetOuter()->NPC_TranslateActivity( ACT_PASSENGER_GESTURE_JOSTLE_SMALL ), true ); + + // Push out our fidget into the future so that we don't act unnaturally over bumpy terrain + ExtendFidgetDelay( random->RandomFloat( 1.5f, 3.0f ) ); + } + } + + // See if we're upside-down + UpdateStuckStatus(); + + // See if we're able to fidget + if ( CanFidget() ) + { + SetCondition( COND_PASSENGER_CAN_FIDGET ); + } + } + + // Clear this out + ClearCondition( COND_PASSENGER_CAN_ENTER_IMMEDIATELY ); + + // Make sure a vehicle doesn't stray from its mark + if ( IsCurSchedule( SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE ) ) + { + if ( m_VehicleMonitor.TargetMoved( m_hVehicle ) ) + { + SetCondition( COND_PASSENGER_VEHICLE_MOVED_FROM_MARK ); + } + + // If we can get in the car right away, set us up to do so + int nNearestSequence; + if ( CanEnterVehicleImmediately( &nNearestSequence, &m_vecTargetPosition, &m_vecTargetAngles ) ) + { + SetTransitionSequence( nNearestSequence ); + SetCondition( COND_PASSENGER_ENTERING ); + SetCondition( COND_PASSENGER_CAN_ENTER_IMMEDIATELY ); + } + } + + // Clear the number for now + m_nVisibleEnemies = 0; + + AIEnemiesIter_t iter; + for( AI_EnemyInfo_t *pEMemory = GetEnemies()->GetFirst(&iter); pEMemory != NULL; pEMemory = GetEnemies()->GetNext(&iter) ) + { + if( GetOuter()->IRelationType( pEMemory->hEnemy ) == D_HT ) + { + if( pEMemory->timeLastSeen == gpGlobals->curtime ) + { + m_nVisibleEnemies++; + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::AimGun( void ) +{ + // If there is no aiming target, return to center + if ( GetEnemy() == NULL ) + { + GetOuter()->RelaxAim(); + return; + } + + // Otherwise try and shoot down the barrel + Vector vecForward, vecRight, vecUp; + GetOuter()->GetVectors( &vecForward, &vecRight, &vecUp ); + Vector vecTorso = GetAbsOrigin() + ( vecUp * 48.0f ); + + Vector vecShootDir = GetOuter()->GetShootEnemyDir( vecTorso, false ); + + Vector vecDirToEnemy = GetEnemy()->GetAbsOrigin() - vecTorso; + VectorNormalize( vecDirToEnemy ); + + bool bRightSide = ( DotProduct( vecDirToEnemy, vecRight ) > 0.0f ); + float flTargetDot = ( bRightSide ) ? -0.7f : 0.0f; + + if ( DotProduct( vecForward, vecDirToEnemy ) <= flTargetDot ) + { + // Don't aim at something that's outside our reach + GetOuter()->RelaxAim(); + } + else + { + // Aim at it + GetOuter()->SetAim( vecShootDir ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Allow us to deny selecting a schedule if we're not in a state to do so +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::CanSelectSchedule( void ) +{ + if ( BaseClass::CanSelectSchedule() == false ) + return false; + + // We're in a period where we're allowing our base class to override us + if ( m_flNextEnterAttempt > gpGlobals->curtime ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Deal with enter/exit of the vehicle +//----------------------------------------------------------------------------- +int CAI_PassengerBehaviorCompanion::SelectTransitionSchedule( void ) +{ + // Attempt to instantly enter the vehicle + if ( HasCondition( COND_PASSENGER_CAN_ENTER_IMMEDIATELY ) ) + { + // Snap to position and begin to animate into the seat + EnterVehicleImmediately(); + return SCHED_PASSENGER_ENTER_VEHICLE_IMMEDIATELY; + } + + // Entering schedule + if ( HasCondition( COND_PASSENGER_ENTERING ) || m_PassengerIntent == PASSENGER_INTENT_ENTER ) + { + if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) + { + ClearCondition( COND_PASSENGER_ENTERING ); + m_PassengerIntent = PASSENGER_INTENT_NONE; + return SCHED_NONE; + } + + // Don't attempt to enter for a period of time + if ( m_flNextEnterAttempt > gpGlobals->curtime ) + return SCHED_NONE; + + ClearCondition( COND_PASSENGER_ENTERING ); + + // Failing that, run to the right place + return SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE; + } + + return BaseClass::SelectTransitionSchedule(); +} + +//----------------------------------------------------------------------------- +// Purpose: Select schedules when we're riding in the car +//----------------------------------------------------------------------------- +int CAI_PassengerBehaviorCompanion::SelectScheduleInsideVehicle( void ) +{ + // Overturned + if ( HasCondition( COND_PASSENGER_OVERTURNED ) ) + return SCHED_PASSENGER_OVERTURNED; + + if ( HasCondition( COND_PASSENGER_HARD_IMPACT ) ) + { + // Push out our fidget into the future so that we don't act unnaturally over bumpy terrain + ExtendFidgetDelay( random->RandomFloat( 1.5f, 3.0f ) ); + m_flNextJostleTime = gpGlobals->curtime + random->RandomFloat( 2.5f, 4.0f ); + return SCHED_PASSENGER_IMPACT; + } + + // Look for exiting the vehicle + if ( HasCondition( COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE ) ) + return SCHED_PASSENGER_EXIT_STUCK_VEHICLE; + + // Cower if we're about to get nailed + if ( HasCondition( COND_HEAR_DANGER ) && IsCurSchedule( SCHED_PASSENGER_COWER ) == false ) + { + SpeakIfAllowed( TLK_DANGER ); + return SCHED_PASSENGER_COWER; + } + + // Fire on targets + if ( GetEnemy() ) + { + // Limit how long we'll keep an enemy if there are many on screen + if ( HasCondition( COND_NEW_ENEMY ) && m_nVisibleEnemies > 1 ) + { + GetEnemies()->SetTimeValidEnemy( GetEnemy(), random->RandomFloat( 0.5f, 1.0f ) ); + } + + // Always face + GetOuter()->AddLookTarget( GetEnemy(), 1.0f, 2.0f ); + + if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) && ( GetOuter()->GetShotRegulator()->IsInRestInterval() == false ) ) + return SCHED_PASSENGER_RANGE_ATTACK1; + } + + // Reload when we have the chance + if ( HasCondition( COND_LOW_PRIMARY_AMMO ) && HasCondition( COND_SEE_ENEMY ) == false ) + return SCHED_PASSENGER_RELOAD; + + // Say an overturned line + if ( HasCondition( COND_PASSENGER_WARN_OVERTURNED ) ) + { + SpeakIfAllowed( TLK_PASSENGER_REQUEST_UPRIGHT ); + m_flNextOverturnWarning = gpGlobals->curtime + random->RandomFloat( 5.0f, 10.0f ); + ClearCondition( COND_PASSENGER_WARN_OVERTURNED ); + } + + // Should we fidget? + if ( HasCondition( COND_PASSENGER_CAN_FIDGET ) ) + { + ExtendFidgetDelay( random->RandomFloat( 6.0f, 12.0f ) ); + return SCHED_PASSENGER_FIDGET; + } + + return SCHED_NONE; +} + +//----------------------------------------------------------------------------- +// Purpose: Select schedules while we're outside the car +//----------------------------------------------------------------------------- +int CAI_PassengerBehaviorCompanion::SelectScheduleOutsideVehicle( void ) +{ + // FIXME: How can we get in here? + Assert( m_hVehicle ); + if ( m_hVehicle == NULL ) + return SCHED_NONE; + + // Handle our mark moving + if ( HasCondition( COND_PASSENGER_VEHICLE_MOVED_FROM_MARK ) ) + { + // Reset our mark + m_VehicleMonitor.SetMark( m_hVehicle, 36.0f ); + ClearCondition( COND_PASSENGER_VEHICLE_MOVED_FROM_MARK ); + } + + // If we want to get in, the try to do so + if ( m_PassengerIntent == PASSENGER_INTENT_ENTER ) + { + // If we're not attempting to enter the vehicle again, just fall to the base class + if ( m_flNextEnterAttempt > gpGlobals->curtime ) + return BaseClass::SelectSchedule(); + + // Otherwise try and enter thec car + return SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE; + } + + // This means that we're outside the vehicle with no intent to enter, which should have disabled us! + Disable(); + Assert( 0 ); + + return SCHED_NONE; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPlayer - +// &vecCenter - +// flRadius - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool SphereWithinPlayerFOV( CBasePlayer *pPlayer, const Vector &vecCenter, float flRadius ) +{ + // TODO: For safety sake, we might want to do a more fully qualified test against the frustum using the bbox + + // If the player can see us, then we can't enter immediately anyway + if ( pPlayer == NULL ) + return false; + + // Find the length to the point + Vector los = ( vecCenter - pPlayer->EyePosition() ); + float flLength = VectorNormalize( los ); + + // Get the player's forward direction + Vector vecPlayerForward; + pPlayer->EyeVectors( &vecPlayerForward, NULL, NULL ); + + // This is the additional number of degrees to add to account for our distance + float flArcAddition = atan2( flRadius, flLength ); + + // Find if the sphere is within our FOV + float flDot = DotProduct( los, vecPlayerForward ); + float flPlayerFOV = cos( DEG2RAD( pPlayer->GetFOV() / 2.0f ) ); + + return ( flDot > (flPlayerFOV-flArcAddition) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::CanEnterVehicleImmediately( int *pResultSequence, Vector *pResultPos, QAngle *pResultAngles ) +{ + // Must wait a short time before trying to do this (otherwise we stack up on the player!) + if ( ( gpGlobals->curtime - m_flEnterBeginTime ) < 0.5f ) + return false; + + // Vehicle can't be moving too quickly + if ( GetVehicleSpeed() > 150 ) + return false; + + // If the player can see us, then we can't enter immediately anyway + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + if ( pPlayer == NULL ) + return false; + + Vector vecPosition = GetOuter()->WorldSpaceCenter(); + float flRadius = GetOuter()->CollisionProp()->BoundingRadius2D(); + if ( SphereWithinPlayerFOV( pPlayer, vecPosition, flRadius ) ) + return false; + + // Reserve an entry point + if ( ReserveEntryPoint( VEHICLE_SEAT_ANY ) == false ) + return false; + + // Get a list of all our animations + const PassengerSeatAnims_t *pEntryAnims = m_hVehicle->GetServerVehicle()->NPC_GetPassengerSeatAnims( GetOuter(), PASSENGER_SEAT_ENTRY ); + if ( pEntryAnims == NULL ) + return -1; + + // Get the ultimate position we'll end up at + Vector vecStartPos, vecEndPos; + QAngle vecStartAngles; + if ( m_hVehicle->GetServerVehicle()->NPC_GetPassengerSeatPosition( GetOuter(), &vecEndPos, NULL ) == false ) + return -1; + + // Categorize the passenger in terms of being on the left or right side of the vehicle + Vector vecRight; + m_hVehicle->GetVectors( NULL, &vecRight, NULL ); + + CPlane lateralPlane; + lateralPlane.InitializePlane( vecRight, m_hVehicle->WorldSpaceCenter() ); + + bool bPlaneSide = lateralPlane.PointInFront( GetOuter()->GetAbsOrigin() ); + + Vector vecPassengerOffset = ( GetOuter()->WorldSpaceCenter() - GetOuter()->GetAbsOrigin() ); + + const CPassengerSeatTransition *pTransition; + float flNearestDistSqr = FLT_MAX; + float flSeatDistSqr; + int nNearestSequence = -1; + int nSequence; + Vector vecNearestPos; + QAngle vecNearestAngles; + + // Test each animation (sorted by priority) for the best match + for ( int i = 0; i < pEntryAnims->Count(); i++ ) + { + // Find the activity for this animation name + pTransition = &pEntryAnims->Element(i); + nSequence = GetOuter()->LookupSequence( STRING( pTransition->GetAnimationName() ) ); + if ( nSequence == -1 ) + continue; + + // Test this entry for validity + if ( GetEntryPoint( nSequence, &vecStartPos, &vecStartAngles ) == false ) + continue; + + // See if the passenger would be visible if standing at this position + if ( SphereWithinPlayerFOV( pPlayer, (vecStartPos+vecPassengerOffset), flRadius ) ) + continue; + + // Otherwise distance is the deciding factor + flSeatDistSqr = ( vecStartPos - GetOuter()->GetAbsOrigin() ).LengthSqr(); + + // We must be within a certain distance to the vehicle + if ( flSeatDistSqr > Square( 25*12 ) ) + continue; + + // We cannot cross between the plane which splits the vehicle laterally in half down the middle + // This avoids cases where the character magically ends up on one side of the vehicle after they were + // clearly just on the other side. + if ( lateralPlane.PointInFront( vecStartPos ) != bPlaneSide ) + continue; + + // Closer, take it + if ( flSeatDistSqr < flNearestDistSqr ) + { + flNearestDistSqr = flSeatDistSqr; + nNearestSequence = nSequence; + vecNearestPos = vecStartPos; + vecNearestAngles = vecStartAngles; + } + } + + // Fail if we didn't find anything + if ( nNearestSequence == -1 ) + return false; + + // Return the results + if ( pResultSequence ) + { + *pResultSequence = nNearestSequence; + } + + if ( pResultPos ) + { + *pResultPos = vecNearestPos; + } + + if ( pResultAngles ) + { + *pResultAngles = vecNearestAngles; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Put us into the vehicle immediately +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::EnterVehicleImmediately( void ) +{ + // Now play the animation + GetOuter()->SetIdealActivity( ACT_SCRIPT_CUSTOM_MOVE ); + GetOuter()->GetNavigator()->ClearGoal(); + + // Put us there and get going (no interpolation!) + GetOuter()->Teleport( &m_vecTargetPosition, &m_vecTargetAngles, &vec3_origin ); + GetOuter()->IncrementInterpolationFrame(); +} + +//----------------------------------------------------------------------------- +// Purpose: Overrides the schedule selection +// Output : int - Schedule to play +//----------------------------------------------------------------------------- +int CAI_PassengerBehaviorCompanion::SelectSchedule( void ) +{ + // First, keep track of our transition state (enter/exit) + int nSched = SelectTransitionSchedule(); + if ( nSched != SCHED_NONE ) + return nSched; + + // Handle schedules based on our passenger state + if ( GetPassengerState() == PASSENGER_STATE_OUTSIDE ) + { + nSched = SelectScheduleOutsideVehicle(); + if ( nSched != SCHED_NONE ) + return nSched; + } + else if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) + { + nSched = SelectScheduleInsideVehicle(); + if ( nSched != SCHED_NONE ) + return nSched; + } + + return BaseClass::SelectSchedule(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int CAI_PassengerBehaviorCompanion::SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode ) +{ + switch( failedTask ) + { + case TASK_GET_PATH_TO_VEHICLE_ENTRY_POINT: + { + // This is not allowed! + if ( GetPassengerState() != PASSENGER_STATE_OUTSIDE ) + { + Assert( 0 ); + return SCHED_FAIL; + } + + // If we're not close enough, then get nearer the target + if ( UTIL_DistApprox( m_hVehicle->GetAbsOrigin(), GetOuter()->GetAbsOrigin() ) > PASSENGER_NEAR_VEHICLE_THRESHOLD ) + return SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE_FAILED; + } + + // Fall through + + case TASK_GET_PATH_TO_NEAR_VEHICLE: + m_flNextEnterAttempt = gpGlobals->curtime + 3.0f; + break; + } + + return BaseClass::SelectFailSchedule( failedSchedule, failedTask, taskFailCode ); +} + +//----------------------------------------------------------------------------- +// Purpose: Start to enter the vehicle +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::EnterVehicle( void ) +{ + BaseClass::EnterVehicle(); + + m_nExitAttempts = 0; + m_VehicleMonitor.SetMark( m_hVehicle, 8.0f ); + m_flEnterBeginTime = gpGlobals->curtime; + + // Remove this flag because we're sitting so close we always think we're going to hit the player + // FIXME: We need to store this state so we don't incorrectly restore it later + GetOuter()->CapabilitiesRemove( bits_CAP_NO_HIT_PLAYER ); + + // Discard enemies quickly + GetOuter()->GetEnemies()->SetEnemyDiscardTime( 2.0f ); + + SpeakIfAllowed( TLK_PASSENGER_BEGIN_ENTRANCE ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::FinishEnterVehicle( void ) +{ + BaseClass::FinishEnterVehicle(); + + // We succeeded + ResetVehicleEntryFailedState(); + + // Push this out into the future so we don't always fidget immediately in the vehicle + ExtendFidgetDelay( random->RandomFloat( 4.0, 15.0f ) ); + + SpeakIfAllowed( TLK_PASSENGER_FINISH_ENTRANCE ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::ExitVehicle( void ) +{ + BaseClass::ExitVehicle(); + + SpeakIfAllowed( TLK_PASSENGER_BEGIN_EXIT ); +} + +//----------------------------------------------------------------------------- +// Purpose: Vehicle has been completely exited +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::FinishExitVehicle( void ) +{ + BaseClass::FinishExitVehicle(); + + m_nExitAttempts = 0; + m_VehicleMonitor.ClearMark(); + + // FIXME: We need to store this state so we don't incorrectly restore it later + GetOuter()->CapabilitiesAdd( bits_CAP_NO_HIT_PLAYER ); + + // FIXME: Restore this properly + GetOuter()->GetEnemies()->SetEnemyDiscardTime( AI_DEF_ENEMY_DISCARD_TIME ); + + SpeakIfAllowed( TLK_PASSENGER_FINISH_EXIT ); +} + +//----------------------------------------------------------------------------- +// Purpose: Tries to build a route to the entry point of the target vehicle. +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::FindPathToVehicleEntryPoint( void ) +{ + // Set our custom move name + // bool bFindNearest = ( GetOuter()->m_NPCState == NPC_STATE_COMBAT || GetOuter()->m_NPCState == NPC_STATE_ALERT ); + bool bFindNearest = true; // For the sake of quick gameplay, just make Alyx move directly! + int nSequence = FindEntrySequence( bFindNearest ); + if ( nSequence == -1 ) + return false; + + // We have to do this specially because the activities are not named + SetTransitionSequence( nSequence ); + + // Get the entry position + Vector vecEntryPoint; + QAngle vecEntryAngles; + if ( GetEntryPoint( m_nTransitionSequence, &vecEntryPoint, &vecEntryAngles ) == false ) + { + MarkVehicleEntryFailed( vecEntryPoint ); + return false; + } + + // If we're already close enough, just succeed + float flDistToGoalSqr = ( GetOuter()->GetAbsOrigin() - vecEntryPoint ).LengthSqr(); + if ( flDistToGoalSqr < Square(3*12) ) + return true; + + // Setup our goal + AI_NavGoal_t goal( GOALTYPE_LOCATION ); + // goal.arrivalActivity = ACT_SCRIPT_CUSTOM_MOVE; + goal.dest = vecEntryPoint; + + // See if we need a radial route around the car, to our goal + if ( UseRadialRouteToEntryPoint( vecEntryPoint ) ) + { + // Find the bounding radius of the vehicle + Vector vecCenterPoint = m_hVehicle->WorldSpaceCenter(); + vecCenterPoint.z = vecEntryPoint.z; + bool bClockwise; + float flArc = GetArcToEntryPoint( vecCenterPoint, vecEntryPoint, bClockwise ); + float flRadius = m_hVehicle->CollisionProp()->BoundingRadius2D(); + + // Try and set a radial route + if ( GetOuter()->GetNavigator()->SetRadialGoal( vecEntryPoint, vecCenterPoint, flRadius, flArc, 64.0f, bClockwise ) == false ) + { + // Try the opposite way + flArc = 360.0f - flArc; + + // Try the opposite way around + if ( GetOuter()->GetNavigator()->SetRadialGoal( vecEntryPoint, vecCenterPoint, flRadius, flArc, 64.0f, !bClockwise ) == false ) + { + // Try and set a direct route as a last resort + if ( GetOuter()->GetNavigator()->SetGoal( goal ) == false ) + return false; + } + } + + // We found a goal + GetOuter()->GetNavigator()->SetArrivalDirection( vecEntryAngles ); + GetOuter()->GetNavigator()->SetArrivalSpeed( 64.0f ); + return true; + } + else + { + // Try and set a direct route + if ( GetOuter()->GetNavigator()->SetGoal( goal ) ) + { + GetOuter()->GetNavigator()->SetArrivalDirection( vecEntryAngles ); + GetOuter()->GetNavigator()->SetArrivalSpeed( 64.0f ); + return true; + } + } + + // We failed, so remember it + MarkVehicleEntryFailed( vecEntryPoint ); + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Tests the route and position to see if it's valid +// Input : &vecTestPos - position to test +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::CanExitAtPosition( const Vector &vecTestPos ) +{ + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + if ( pPlayer == NULL ) + return false; + + // Can't be in our potential view + if ( pPlayer->FInViewCone( vecTestPos ) ) + return false; + + // NOTE: There's no reason to do this since this is only called from a node's reported position + // Find the exact ground at this position + //Vector vecGroundPos; + //if ( FindGroundAtPosition( vecTestPos, 16.0f, 64.0f, &vecGroundPos ) == false ) + // return false; + + // Get the ultimate position we'll end up at + Vector vecStartPos; + if ( m_hVehicle->GetServerVehicle()->NPC_GetPassengerSeatPosition( GetOuter(), &vecStartPos, NULL ) == false ) + return false; + + // See if we can move from where we are to that position in space + if ( IsValidTransitionPoint( vecStartPos, vecTestPos ) == false ) + return false; + + // Trace down to the ground + // FIXME: This piece of code is redundant and happening in IsValidTransitionPoint() as well + /* + Vector vecGroundPos; + if ( FindGroundAtPosition( vecTestPos, GetOuter()->StepHeight(), 64.0f, &vecGroundPos ) == false ) + return false; + */ + + // Try and sweep a box through space and make sure it's clear of obstructions + /* + trace_t tr; + CTraceFilterVehicleTransition skipFilter( GetOuter(), m_hVehicle, COLLISION_GROUP_NONE ); + + // These are very approximated (and magical) numbers to allow passengers greater head room and leg room when transitioning + Vector vecMins = GetOuter()->GetHullMins() + Vector( 0, 0, GetOuter()->StepHeight()*2.0f ); // FIXME: + Vector vecMaxs = GetOuter()->GetHullMaxs() - Vector( 0, 0, GetOuter()->StepHeight() ); + + UTIL_TraceHull( GetOuter()->GetAbsOrigin(), vecGroundPos, vecMins, vecMaxs, MASK_NPCSOLID, &skipFilter, &tr ); + + // If we're blocked, we can't get out there + if ( tr.fraction < 1.0f || tr.allsolid || tr.startsolid ) + { + if ( passenger_debug_transition.GetBool() ) + { + NDebugOverlay::SweptBox( GetOuter()->GetAbsOrigin(), vecGroundPos, vecMins, GetOuter()->GetHullMaxs(), vec3_angle, 255, 0, 0, 64, 2.0f ); + } + return false; + } + */ + + return true; +} + +#define NUM_EXIT_ITERATIONS 8 + +//----------------------------------------------------------------------------- +// Purpose: Find a position we can use to exit the vehicle via teleportation +// Input : *vecResult - safe place to exit to +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::GetStuckExitPos( Vector *vecResult ) +{ + // Get our right direction + Vector vecVehicleRight; + m_hVehicle->GetVectors( NULL, &vecVehicleRight, NULL ); + + // Get the vehicle's rough horizontal bounds + float flVehicleRadius = m_hVehicle->CollisionProp()->BoundingRadius2D(); + + // Use the vehicle's center as our hub + Vector vecCenter = m_hVehicle->WorldSpaceCenter(); + + // Angle whose tan is: y/x + float flCurAngle = atan2f( vecVehicleRight.y, vecVehicleRight.x ); + float flAngleIncr = (M_PI*2.0f)/(float)NUM_EXIT_ITERATIONS; + Vector vecTestPos; + + // Test a number of discrete exit routes + for ( int i = 0; i <= NUM_EXIT_ITERATIONS-1; i++ ) + { + // Get our position + SinCos( flCurAngle, &vecTestPos.y, &vecTestPos.x ); + vecTestPos.z = 0.0f; + vecTestPos *= flVehicleRadius; + vecTestPos += vecCenter; + + // Now find the nearest node and use that + int nNearNode = GetOuter()->GetPathfinder()->NearestNodeToPoint( vecTestPos ); + if ( nNearNode != NO_NODE ) + { + Vector vecNodePos = g_pBigAINet->GetNodePosition( GetOuter()->GetHullType(), nNearNode ); + + // Test the position + if ( CanExitAtPosition( vecNodePos ) ) + { + // Take the result + *vecResult = vecNodePos; + return true; + } + + // Move to the next iteration + flCurAngle += flAngleIncr; + } + } + + // None found + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Attempt to get out of an overturned vehicle when the player isn't looking +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::ExitStuckVehicle( void ) +{ + // Try and find an exit position + Vector vecExitPos; + if ( GetStuckExitPos( &vecExitPos ) == false ) + return false; + + // Detach from the parent + GetOuter()->SetParent( NULL ); + + // Do all necessary clean-up + FinishExitVehicle(); + + // Teleport to the destination + // TODO: Make sure that the player can't see this! + GetOuter()->Teleport( &vecExitPos, &vec3_angle, &vec3_origin ); + GetOuter()->IncrementInterpolationFrame(); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::StartTask( const Task_t *pTask ) +{ + // We need to override these so we never face + if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) + { + if ( pTask->iTask == TASK_FACE_TARGET || + pTask->iTask == TASK_FACE_ENEMY || + pTask->iTask == TASK_FACE_IDEAL || + pTask->iTask == TASK_FACE_HINTNODE || + pTask->iTask == TASK_FACE_LASTPOSITION || + pTask->iTask == TASK_FACE_PATH || + pTask->iTask == TASK_FACE_PLAYER || + pTask->iTask == TASK_FACE_REASONABLE || + pTask->iTask == TASK_FACE_SAVEPOSITION || + pTask->iTask == TASK_FACE_SCRIPT ) + { + return TaskComplete(); + } + } + + switch ( pTask->iTask ) + { + case TASK_RUN_TO_VEHICLE_ENTRANCE: + { + // Get a move on! + GetOuter()->GetNavigator()->SetMovementActivity( ACT_RUN ); + } + break; + + case TASK_GET_PATH_TO_VEHICLE_ENTRY_POINT: + { + if ( GetPassengerState() != PASSENGER_STATE_OUTSIDE ) + { + Assert( 0 ); + TaskFail( "Trying to run while inside a vehicle!\n"); + return; + } + + // Reserve an entry point + if ( ReserveEntryPoint( VEHICLE_SEAT_ANY ) == false ) + { + TaskFail( "No valid entry point!\n" ); + return; + } + + // Find where we're going + if ( FindPathToVehicleEntryPoint() ) + { + TaskComplete(); + return; + } + + // We didn't find a path + TaskFail( "TASK_GET_PATH_TO_VEHICLE_ENTRY_POINT: Unable to run to entry point" ); + } + break; + + case TASK_GET_PATH_TO_TARGET: + { + GetOuter()->SetTarget( m_hVehicle ); + BaseClass::StartTask( pTask ); + } + break; + + case TASK_GET_PATH_TO_NEAR_VEHICLE: + { + if ( m_hVehicle == NULL ) + { + TaskFail("Lost vehicle pointer\n"); + return; + } + + // Find the passenger offset we're going for + Vector vecRight; + m_hVehicle->GetVectors( NULL, &vecRight, NULL ); + Vector vecTargetOffset = vecRight * 64.0f; + + // Try and find a path near there + AI_NavGoal_t goal( GOALTYPE_TARGETENT, vecTargetOffset, AIN_DEF_ACTIVITY, 64.0f, AIN_UPDATE_TARGET_POS, m_hVehicle ); + GetOuter()->SetTarget( m_hVehicle ); + if ( GetOuter()->GetNavigator()->SetGoal( goal ) ) + { + TaskComplete(); + return; + } + + TaskFail( "Unable to find path to get closer to vehicle!\n" ); + return; + } + + break; + + case TASK_PASSENGER_RELOAD: + { + GetOuter()->SetIdealActivity( ACT_PASSENGER_RELOAD ); + return; + } + break; + + case TASK_PASSENGER_EXIT_STUCK_VEHICLE: + { + if ( ExitStuckVehicle() ) + { + TaskComplete(); + return; + } + + TaskFail("Unable to exit overturned vehicle!\n"); + } + break; + + case TASK_PASSENGER_OVERTURNED: + { + // Go into our overturned animation + if ( GetOuter()->GetActivity() != ACT_PASSENGER_OVERTURNED ) + { + GetOuter()->SetActivity( ACT_RESET ); + GetOuter()->SetActivity( ACT_PASSENGER_OVERTURNED ); + } + + TaskComplete(); + } + break; + + case TASK_PASSENGER_IMPACT: + { + // Stomp anything currently playing on top of us, this has to take priority + GetOuter()->RemoveAllGestures(); + + // Go into our impact animation + GetOuter()->ResetIdealActivity( ACT_PASSENGER_IMPACT ); + + // Delay for twice the duration of our impact animation + int nSequence = GetOuter()->SelectWeightedSequence( ACT_PASSENGER_IMPACT ); + float flSeqDuration = GetOuter()->SequenceDuration( nSequence ); + float flStunTime = flSeqDuration + random->RandomFloat( 1.0f, 2.0f ); + GetOuter()->SetNextAttack( gpGlobals->curtime + flStunTime ); + ExtendFidgetDelay( flStunTime ); + } + break; + + default: + BaseClass::StartTask( pTask ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::IsCurTaskContinuousMove( void ) +{ + const Task_t *pCurTask = GetCurTask(); + if ( pCurTask && pCurTask->iTask == TASK_RUN_TO_VEHICLE_ENTRANCE ) + return true; + + return BaseClass::IsCurTaskContinuousMove(); +} + +//----------------------------------------------------------------------------- +// Purpose: Update our path if we're running towards the vehicle (since it can move) +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::UpdateVehicleEntrancePath( void ) +{ + // If it's too soon to check again, don't bother + if ( m_flEntraceUpdateTime > gpGlobals->curtime ) + return true; + + // Find out if we need to update + if ( m_VehicleMonitor.TargetMoved2D( m_hVehicle ) == false ) + { + m_flEntraceUpdateTime = gpGlobals->curtime + 0.5f; + return true; + } + + // Don't attempt again for some amount of time + m_flEntraceUpdateTime = gpGlobals->curtime + 1.0f; + + int nSequence = FindEntrySequence( true ); + if ( nSequence == -1 ) + return false; + + SetTransitionSequence( nSequence ); + + // Get the entry position + Vector vecEntryPoint; + QAngle vecEntryAngles; + if ( GetEntryPoint( m_nTransitionSequence, &vecEntryPoint, &vecEntryAngles ) == false ) + return false; + + // Move the entry point forward in time a bit to predict where it'll be + Vector vecVehicleSpeed = m_hVehicle->GetSmoothedVelocity(); + + // Tack on the smoothed velocity + vecEntryPoint += vecVehicleSpeed; // one second + + // Update our entry point + if ( GetOuter()->GetNavigator()->UpdateGoalPos( vecEntryPoint ) == false ) + return false; + + // Reset the goal angles + GetNavigator()->SetArrivalDirection( vecEntryAngles ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::RunTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_PASSENGER_RELOAD: + { + if ( GetOuter()->IsSequenceFinished() ) + { + TaskComplete(); + } + } + break; + + case TASK_PASSENGER_IMPACT: + { + if ( GetOuter()->IsSequenceFinished() ) + { + TaskComplete(); + return; + } + } + break; + + case TASK_RUN_TO_VEHICLE_ENTRANCE: + { + // Update our entrance point if we can + if ( UpdateVehicleEntrancePath() == false ) + { + TaskFail("Unable to find entrance to vehicle"); + break; + } + + // See if we're close enough to our goal + if ( GetOuter ()->GetNavigator()->IsGoalActive() == false ) + { + // See if we're close enough now to enter the vehicle + Vector vecEntryPoint; + GetEntryPoint( m_nTransitionSequence, &vecEntryPoint ); + if ( ( vecEntryPoint - GetAbsOrigin() ).Length2DSqr() < Square( 36.0f ) ) + { + if ( GetNavigator()->GetArrivalActivity() != ACT_INVALID ) + { + SetActivity( GetNavigator()->GetArrivalActivity() ); + } + + TaskComplete(); + } + else + { + TaskFail( "Unable to navigate to vehicle" ); + } + } + + // Keep merrily going! + } + break; + + default: + BaseClass::RunTask( pTask ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Add custom interrupt conditions +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::BuildScheduleTestBits( void ) +{ + // Always break on being able to exit + if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) + { + GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE ) ); + GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_PASSENGER_HARD_IMPACT) ); + + if ( IsCurSchedule( SCHED_PASSENGER_OVERTURNED ) == false ) + { + GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_PASSENGER_OVERTURNED ) ); + } + + // Append the ability to break on fidgeting + if ( IsCurSchedule( SCHED_PASSENGER_IDLE ) ) + { + GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_PASSENGER_CAN_FIDGET ) ); + } + + // Add this so we're prompt about exiting the vehicle when able to + if ( m_PassengerIntent == PASSENGER_INTENT_EXIT ) + { + GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_PASSENGER_VEHICLE_STOPPED ) ); + } + } + + BaseClass::BuildScheduleTestBits(); +} +//----------------------------------------------------------------------------- +// Purpose: Determines if the passenger should take a radial route to the goal +// Input : &vecEntryPoint - Point of entry +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::UseRadialRouteToEntryPoint( const Vector &vecEntryPoint ) +{ + // Get the center position of the vehicle we'll radiate around + Vector vecCenterPos = m_hVehicle->WorldSpaceCenter(); + vecCenterPos.z = vecEntryPoint.z; + + // Find out if we need to go around the vehicle + float flDistToVehicleCenter = ( vecCenterPos - GetOuter()->GetAbsOrigin() ).Length(); + float flDistToGoal = ( vecEntryPoint - GetOuter()->GetAbsOrigin() ).Length(); + if ( flDistToGoal > flDistToVehicleCenter ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Find the arc in degrees to reach our goal position +// Input : &vecCenterPoint - Point around which the arc rotates +// &vecEntryPoint - Point we're trying to reach +// &bClockwise - If we should move clockwise or not to get there +// Output : float - degrees around arc to follow +//----------------------------------------------------------------------------- +float CAI_PassengerBehaviorCompanion::GetArcToEntryPoint( const Vector &vecCenterPoint, const Vector &vecEntryPoint, bool &bClockwise ) +{ + // We want the entry point to be at the same level as the center to make this a two dimensional problem + Vector vecEntryPointAdjusted = vecEntryPoint; + vecEntryPointAdjusted.z = vecCenterPoint.z; + + // Direction from vehicle center to passenger + Vector vecVehicleToPassenger = ( GetOuter()->GetAbsOrigin() - vecCenterPoint ); + VectorNormalize( vecVehicleToPassenger ); + + // Direction from vehicle center to entry point + Vector vecVehicleToEntry = ( vecEntryPointAdjusted - vecCenterPoint ); + VectorNormalize( vecVehicleToEntry ); + + float flVehicleToPassengerYaw = UTIL_VecToYaw( vecVehicleToPassenger ); + float flVehicleToEntryYaw = UTIL_VecToYaw( vecVehicleToEntry ); + float flArcDist = UTIL_AngleDistance( flVehicleToEntryYaw, flVehicleToPassengerYaw ); + + bClockwise = ( flArcDist < 0.0f ); + return fabs( flArcDist ); +} + +//----------------------------------------------------------------------------- +// Purpose: Removes all failed points +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::ResetVehicleEntryFailedState( void ) +{ + m_FailedEntryPositions.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: Adds a failed position to the list and marks when it occurred +// Input : &vecPosition - Position that failed +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::MarkVehicleEntryFailed( const Vector &vecPosition ) +{ + FailPosition_t failPos; + failPos.flTime = gpGlobals->curtime; + failPos.vecPosition = vecPosition; + m_FailedEntryPositions.AddToTail( failPos ); + + // Show this as failed + if ( passenger_debug_entry.GetBool() ) + { + NDebugOverlay::Box( vecPosition, -Vector(8,8,8), Vector(8,8,8), 255, 0, 0, 0, 2.0f ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: See if a vector is near enough to a previously failed position +// Input : &vecPosition - position to test +// Output : Returns true if the point is near enough another to be considered equivalent +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::PointIsWithinEntryFailureRadius( const Vector &vecPosition ) +{ + // Test this point against our known failed points and reject it if it's too near + for ( int i = 0; i < m_FailedEntryPositions.Count(); i++ ) + { + // If our time has expired, kill the position + if ( ( gpGlobals->curtime - m_FailedEntryPositions[i].flTime ) > 3.0f ) + { + // Show that we've cleared it + if ( passenger_debug_entry.GetBool() ) + { + NDebugOverlay::Box( m_FailedEntryPositions[i].vecPosition, -Vector(12,12,12), Vector(12,12,12), 255, 255, 0, 0, 2.0f ); + } + + m_FailedEntryPositions.Remove( i ); + continue; + } + + // See if this position is too near our last failed attempt + if ( ( vecPosition - m_FailedEntryPositions[i].vecPosition ).LengthSqr() < Square(3*12) ) + { + // Show that this was denied + if ( passenger_debug_entry.GetBool() ) + { + NDebugOverlay::Box( m_FailedEntryPositions[i].vecPosition, -Vector(12,12,12), Vector(12,12,12), 255, 0, 0, 128, 2.0f ); + } + + return true; + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Find the proper sequence to use (weighted by priority or distance from current position) +// to enter the vehicle. +// Input : bNearest - Use distance as the criteria for a "best" sequence. Otherwise the order of the +// seats is their priority. +// Output : int - sequence index +//----------------------------------------------------------------------------- +int CAI_PassengerBehaviorCompanion::FindEntrySequence( bool bNearest /*= false*/ ) +{ + // Get a list of all our animations + const PassengerSeatAnims_t *pEntryAnims = m_hVehicle->GetServerVehicle()->NPC_GetPassengerSeatAnims( GetOuter(), PASSENGER_SEAT_ENTRY ); + if ( pEntryAnims == NULL ) + return -1; + + // Get the ultimate position we'll end up at + Vector vecStartPos, vecEndPos; + if ( m_hVehicle->GetServerVehicle()->NPC_GetPassengerSeatPosition( GetOuter(), &vecEndPos, NULL ) == false ) + return -1; + + const CPassengerSeatTransition *pTransition; + float flNearestDistSqr = FLT_MAX; + float flSeatDistSqr; + int nNearestSequence = -1; + int nSequence; + + // Test each animation (sorted by priority) for the best match + for ( int i = 0; i < pEntryAnims->Count(); i++ ) + { + // Find the activity for this animation name + pTransition = &pEntryAnims->Element(i); + nSequence = GetOuter()->LookupSequence( STRING( pTransition->GetAnimationName() ) ); + if ( nSequence == -1 ) + continue; + + // Test this entry for validity + if ( GetEntryPoint( nSequence, &vecStartPos ) == false ) + continue; + + // See if this entry position is in our list of known unreachable places + if ( PointIsWithinEntryFailureRadius( vecStartPos ) ) + continue; + + // Check to see if we can use this + if ( IsValidTransitionPoint( vecStartPos, vecEndPos ) ) + { + // If we're just looking for the first, we're done + if ( bNearest == false ) + return nSequence; + + // Otherwise distance is the deciding factor + flSeatDistSqr = ( vecStartPos - GetOuter()->GetAbsOrigin() ).LengthSqr(); + + // Closer, take it + if ( flSeatDistSqr < flNearestDistSqr ) + { + flNearestDistSqr = flSeatDistSqr; + nNearestSequence = nSequence; + } + } + + } + + return nNearestSequence; +} + +//----------------------------------------------------------------------------- +// Purpose: Override certain animations +//----------------------------------------------------------------------------- +Activity CAI_PassengerBehaviorCompanion::NPC_TranslateActivity( Activity activity ) +{ + Activity newActivity = BaseClass::NPC_TranslateActivity( activity ); + + // Handle animations from inside the vehicle + if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) + { + // Alter idle depending on the vehicle's state + if ( newActivity == ACT_IDLE ) + { + // Always play the overturned animation + if ( m_vehicleState.m_bWasOverturned ) + return ACT_PASSENGER_OVERTURNED; + } + } + + return newActivity; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::CanExitVehicle( void ) +{ + if ( BaseClass::CanExitVehicle() == false ) + return false; + + // If we're tipped too much, we can't exit + Vector vecUp; + GetOuter()->GetVectors( NULL, NULL, &vecUp ); + if ( DotProduct( vecUp, Vector(0,0,1) ) < DOT_45DEGREE ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: NPC needs to get to their marks, so do so with urgent navigation +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::IsNavigationUrgent( void ) +{ + // If we're running to the vehicle, do so urgently + if ( GetPassengerState() == PASSENGER_STATE_OUTSIDE && m_PassengerIntent == PASSENGER_INTENT_ENTER ) + return true; + + return BaseClass::IsNavigationUrgent(); +} + +//----------------------------------------------------------------------------- +// Purpose: Calculate our body lean based on our delta velocity +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::CalculateBodyLean( void ) +{ + // Calculate our lateral displacement from a perfectly centered start + float flLateralDisp = SimpleSplineRemapVal( m_vehicleState.m_vecLastAngles.z, 100.0f, -100.0f, -1.0f, 1.0f ); + flLateralDisp = clamp( flLateralDisp, -1.0f, 1.0f ); + + // FIXME: Framerate dependent! + m_flLastLateralLean = ( m_flLastLateralLean * 0.2f ) + ( flLateralDisp * 0.8f ); + + // Here we can make Alyx do something different on an "extreme" lean condition + if ( fabs( m_flLastLateralLean ) > 0.75f ) + { + // Large lean, make us react? + } + + // Set these parameters + GetOuter()->SetPoseParameter( "vehicle_lean", m_flLastLateralLean ); +} + +//----------------------------------------------------------------------------- +// Purpose: Whether or not we're allowed to fidget +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::CanFidget( void ) +{ + // Can't fidget again too quickly + if ( m_flNextFidgetTime > gpGlobals->curtime ) + return false; + + // FIXME: Really we want to check our readiness level at this point + if ( GetOuter()->GetEnemy() != NULL ) + return false; + + // Don't fidget unless we're at low readiness + if ( m_hCompanion && ( m_hCompanion->GetReadinessLevel() > AIRL_RELAXED ) ) + return false; + + // Don't fidget while we're in a script + if ( GetOuter()->IsInAScript() || GetOuter()->GetIdealState() == NPC_STATE_SCRIPT || IsRunningScriptedScene( GetOuter() ) ) + return false; + + // If we're upside down, don't bother + if ( HasCondition( COND_PASSENGER_OVERTURNED ) ) + return false; + + // Must be visible to the player + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + if ( pPlayer && pPlayer->FInViewCone( GetOuter()->EyePosition() ) == false ) + return false; + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Extends the fidget delay by the time specified +// Input : flDuration - in seconds +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorCompanion::ExtendFidgetDelay( float flDuration ) +{ + // If we're already expired, just set this as the next time + if ( m_flNextFidgetTime < gpGlobals->curtime ) + { + m_flNextFidgetTime = gpGlobals->curtime + flDuration; + } + else + { + // Otherwise bump the delay farther into the future + m_flNextFidgetTime += flDuration; + } +} + +//----------------------------------------------------------------------------- +// Purpose: We never want to be marked as crouching when inside a vehicle +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorCompanion::IsCrouching( void ) +{ + return false; +} + +AI_BEGIN_CUSTOM_SCHEDULE_PROVIDER( CAI_PassengerBehaviorCompanion ) +{ + DECLARE_ACTIVITY( ACT_PASSENGER_IDLE_AIM ) + DECLARE_ACTIVITY( ACT_PASSENGER_RELOAD ) + DECLARE_ACTIVITY( ACT_PASSENGER_OVERTURNED ) + DECLARE_ACTIVITY( ACT_PASSENGER_IMPACT ) + DECLARE_ACTIVITY( ACT_PASSENGER_IMPACT_WEAPON ) + DECLARE_ACTIVITY( ACT_PASSENGER_POINT ) + DECLARE_ACTIVITY( ACT_PASSENGER_POINT_BEHIND ) + DECLARE_ACTIVITY( ACT_PASSENGER_IDLE_READY ) + DECLARE_ACTIVITY( ACT_PASSENGER_GESTURE_JOSTLE_LARGE ) + DECLARE_ACTIVITY( ACT_PASSENGER_GESTURE_JOSTLE_SMALL ) + DECLARE_ACTIVITY( ACT_PASSENGER_GESTURE_JOSTLE_LARGE_STIMULATED ) + DECLARE_ACTIVITY( ACT_PASSENGER_GESTURE_JOSTLE_SMALL_STIMULATED ) + DECLARE_ACTIVITY( ACT_PASSENGER_COWER_IN ) + DECLARE_ACTIVITY( ACT_PASSENGER_COWER_LOOP ) + DECLARE_ACTIVITY( ACT_PASSENGER_COWER_OUT ) + DECLARE_ACTIVITY( ACT_PASSENGER_IDLE_FIDGET ) + + DECLARE_TASK( TASK_GET_PATH_TO_VEHICLE_ENTRY_POINT ) + DECLARE_TASK( TASK_GET_PATH_TO_NEAR_VEHICLE ) + DECLARE_TASK( TASK_PASSENGER_RELOAD ) + DECLARE_TASK( TASK_PASSENGER_EXIT_STUCK_VEHICLE ) + DECLARE_TASK( TASK_PASSENGER_OVERTURNED ) + DECLARE_TASK( TASK_PASSENGER_IMPACT ) + DECLARE_TASK( TASK_RUN_TO_VEHICLE_ENTRANCE ) + + DECLARE_CONDITION( COND_PASSENGER_VEHICLE_MOVED_FROM_MARK ) + DECLARE_CONDITION( COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE ) + DECLARE_CONDITION( COND_PASSENGER_WARN_OVERTURNED ) + DECLARE_CONDITION( COND_PASSENGER_WARN_COLLISION ) + DECLARE_CONDITION( COND_PASSENGER_CAN_FIDGET ) + DECLARE_CONDITION( COND_PASSENGER_CAN_ENTER_IMMEDIATELY ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE_FAILED" + " TASK_STOP_MOVING 0" + " TASK_SET_TOLERANCE_DISTANCE 36" // 3 ft + " TASK_SET_ROUTE_SEARCH_TIME 5" + " TASK_GET_PATH_TO_VEHICLE_ENTRY_POINT 0" + " TASK_RUN_TO_VEHICLE_ENTRANCE 0" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_PASSENGER_ENTER_VEHICLE" + "" + " Interrupts" + " COND_PASSENGER_CAN_ENTER_IMMEDIATELY" + " COND_PASSENGER_CANCEL_ENTER" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE_FAILED, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_PASSENGER_ENTER_VEHICLE_PAUSE" + " TASK_STOP_MOVING 0" + " TASK_SET_TOLERANCE_DISTANCE 36" + " TASK_SET_ROUTE_SEARCH_TIME 3" + " TASK_GET_PATH_TO_NEAR_VEHICLE 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + "" + " Interrupts" + " COND_PASSENGER_CANCEL_ENTER" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_ENTER_VEHICLE_PAUSE, + + " Tasks" + " TASK_STOP_MOVING 1" + " TASK_FACE_TARGET 0" + " TASK_WAIT 2" + "" + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_NEW_ENEMY" + " COND_PASSENGER_CANCEL_ENTER" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_RANGE_ATTACK1, + + " Tasks" + " TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack + " TASK_RANGE_ATTACK1 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_ENEMY_OCCLUDED" + " COND_NO_PRIMARY_AMMO" + " COND_HEAR_DANGER" + " COND_WEAPON_BLOCKED_BY_FRIEND" + " COND_WEAPON_SIGHT_OCCLUDED" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_EXIT_STUCK_VEHICLE, + + " Tasks" + " TASK_PASSENGER_EXIT_STUCK_VEHICLE 0" + "" + " Interrupts" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_RELOAD, + + " Tasks" + " TASK_PASSENGER_RELOAD 0" + "" + " Interrupts" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_OVERTURNED, + + " Tasks" + " TASK_PASSENGER_OVERTURNED 0" + "" + " Interrupts" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_IMPACT, + + " Tasks" + " TASK_PASSENGER_IMPACT 0" + "" + " Interrupts" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_ENTER_VEHICLE_IMMEDIATELY, + + " Tasks" + " TASK_PASSENGER_ATTACH_TO_VEHICLE 0" + " TASK_PASSENGER_ENTER_VEHICLE 0" + "" + " Interrupts" + " COND_NO_CUSTOM_INTERRUPTS" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_COWER, + + " Tasks" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_PASSENGER_COWER_IN" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_PASSENGER_COWER_LOOP" + " TASK_WAIT_UNTIL_NO_DANGER_SOUND 0" + " TASK_WAIT 2" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_PASSENGER_COWER_OUT" + "" + " Interrupts" + " COND_NO_CUSTOM_INTERRUPTS" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_FIDGET, + + " Tasks" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_PASSENGER_IDLE_FIDGET" + "" + " Interrupts" + " COND_NO_CUSTOM_INTERRUPTS" + ) + + AI_END_CUSTOM_SCHEDULE_PROVIDER() +} diff --git a/game/server/episodic/ai_behavior_passenger_companion.h b/game/server/episodic/ai_behavior_passenger_companion.h new file mode 100644 index 0000000..857254e --- /dev/null +++ b/game/server/episodic/ai_behavior_passenger_companion.h @@ -0,0 +1,168 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef AI_BEHAVIOR_PASSENGER_COMPANION_H +#define AI_BEHAVIOR_PASSENGER_COMPANION_H +#ifdef _WIN32 +#pragma once +#endif + +#include "ai_behavior_passenger.h" + +class CNPC_PlayerCompanion; + +struct VehicleAvoidParams_t +{ + Vector vecStartPos; + Vector vecGoalPos; + Vector *pNodePositions; + int nNumNodes; + int nDirection; + int nStartNode; + int nEndNode; +}; + +struct FailPosition_t +{ + Vector vecPosition; + float flTime; + + DECLARE_SIMPLE_DATADESC(); +}; + +class CAI_PassengerBehaviorCompanion : public CAI_PassengerBehavior +{ + DECLARE_CLASS( CAI_PassengerBehaviorCompanion, CAI_PassengerBehavior ); + DECLARE_DATADESC() + +public: + + CAI_PassengerBehaviorCompanion( void ); + + enum + { + // Schedules + SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE = BaseClass::NEXT_SCHEDULE, + SCHED_PASSENGER_RUN_TO_ENTER_VEHICLE_FAILED, + SCHED_PASSENGER_ENTER_VEHICLE_PAUSE, + SCHED_PASSENGER_RANGE_ATTACK1, + SCHED_PASSENGER_RELOAD, + SCHED_PASSENGER_EXIT_STUCK_VEHICLE, + SCHED_PASSENGER_OVERTURNED, + SCHED_PASSENGER_IMPACT, + SCHED_PASSENGER_ENTER_VEHICLE_IMMEDIATELY, + SCHED_PASSENGER_COWER, + SCHED_PASSENGER_FIDGET, + NEXT_SCHEDULE, + + // Tasks + TASK_GET_PATH_TO_VEHICLE_ENTRY_POINT = BaseClass::NEXT_TASK, + TASK_GET_PATH_TO_NEAR_VEHICLE, + TASK_PASSENGER_RELOAD, + TASK_PASSENGER_EXIT_STUCK_VEHICLE, + TASK_PASSENGER_OVERTURNED, + TASK_PASSENGER_IMPACT, + TASK_RUN_TO_VEHICLE_ENTRANCE, + NEXT_TASK, + + // Conditions + + COND_PASSENGER_CAN_LEAVE_STUCK_VEHICLE = BaseClass::NEXT_CONDITION, + COND_PASSENGER_WARN_OVERTURNED, + COND_PASSENGER_WARN_COLLISION, + COND_PASSENGER_VEHICLE_MOVED_FROM_MARK, + COND_PASSENGER_CAN_FIDGET, + COND_PASSENGER_CAN_ENTER_IMMEDIATELY, + NEXT_CONDITION, + }; + + virtual bool CanSelectSchedule( void ); + virtual void Enable( CPropJeepEpisodic *pVehicle, bool bImmediateEnter = false); + virtual void GatherConditions( void ); + virtual int SelectSchedule( void ); + virtual int SelectFailSchedule( int failedSchedule, int failedTask, AI_TaskFailureCode_t taskFailCode ); + virtual void StartTask( const Task_t *pTask ); + virtual void RunTask( const Task_t *pTask ); + virtual void AimGun( void ); + virtual void EnterVehicle( void ); + virtual void ExitVehicle( void ); + virtual void FinishEnterVehicle( void ); + virtual void FinishExitVehicle( void ); + virtual void BuildScheduleTestBits( void ); + virtual Activity NPC_TranslateActivity( Activity activity ); + virtual bool CanExitVehicle( void ); + virtual bool IsValidEnemy( CBaseEntity *pEntity ); + virtual void OnUpdateShotRegulator( void ); + virtual bool IsNavigationUrgent( void ); + virtual bool IsCurTaskContinuousMove( void ); + virtual bool IsCrouching( void ); + +private: + + void SpeakVehicleConditions( void ); + virtual void OnExitVehicleFailed( void ); + + bool CanFidget( void ); + bool UseRadialRouteToEntryPoint( const Vector &vecEntryPoint ); + float GetArcToEntryPoint( const Vector &vecCenterPoint, const Vector &vecEntryPoint, bool &bClockwise ); + int SelectScheduleInsideVehicle( void ); + int SelectScheduleOutsideVehicle( void ); + bool FindPathToVehicleEntryPoint( void ); + bool CanEnterVehicleImmediately( int *pResultSequence, Vector *pResultPos, QAngle *pResultAngles ); + void EnterVehicleImmediately( void ); + + // ------------------------------------------ + // Passenger sensing + // ------------------------------------------ + + virtual void GatherVehicleStateConditions( void ); + + float GetVehicleSpeed( void ); + void GatherVehicleCollisionConditions( const Vector &localVelocity ); + + // ------------------------------------------ + // Overturned tracking + // ------------------------------------------ + void UpdateStuckStatus( void ); + bool CanExitAtPosition( const Vector &vecTestPos ); + bool GetStuckExitPos( Vector *vecResult ); + bool ExitStuckVehicle( void ); + + bool UpdateVehicleEntrancePath( void ); + bool PointIsWithinEntryFailureRadius( const Vector &vecPosition ); + void ResetVehicleEntryFailedState( void ); + void MarkVehicleEntryFailed( const Vector &vecPosition ); + virtual int FindEntrySequence( bool bNearest = false ); + void CalculateBodyLean( void ); + + float m_flNextJostleTime; + float m_flNextOverturnWarning; // The next time the NPC may complained about being upside-down + float m_flOverturnedDuration; // Amount of time we've been stuck in the vehicle (unable to exit) + float m_flUnseenDuration; // Amount of time we've been hidden from the player's view + + float m_flEnterBeginTime; // Time the NPC started to try and enter the vehicle + int m_nExitAttempts; // Number of times we've attempted to exit the vehicle but failed + int m_nVisibleEnemies; // Keeps a record of how many enemies I know about + float m_flLastLateralLean; // Our last lean value + + CAI_MoveMonitor m_VehicleMonitor; // Used to keep track of the vehicle's movement relative to a mark + CUtlVector<FailPosition_t> m_FailedEntryPositions; // Used to keep track of the vehicle's movement relative to a mark + +protected: + virtual int SelectTransitionSchedule( void ); + + void ExtendFidgetDelay( float flDuration ); + bool CanPlayJostle( bool bLargeJostle ); + + float m_flEntraceUpdateTime; + float m_flNextEnterAttempt; + float m_flNextFidgetTime; + CHandle< CNPC_PlayerCompanion > m_hCompanion; + + DEFINE_CUSTOM_SCHEDULE_PROVIDER; +}; + +#endif // AI_BEHAVIOR_PASSENGER_COMPANION_H diff --git a/game/server/episodic/ai_behavior_passenger_zombie.cpp b/game/server/episodic/ai_behavior_passenger_zombie.cpp new file mode 100644 index 0000000..0667530 --- /dev/null +++ b/game/server/episodic/ai_behavior_passenger_zombie.cpp @@ -0,0 +1,878 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Zombies on cars! +// +//============================================================================= + +#include "cbase.h" +#include "npcevent.h" +#include "ai_motor.h" +#include "ai_senses.h" +#include "vehicle_jeep_episodic.h" +#include "npc_alyx_episodic.h" +#include "ai_behavior_passenger_zombie.h" + +#define JUMP_ATTACH_DIST_THRESHOLD 1000 +#define JUMP_ATTACH_FACING_THRESHOLD DOT_45DEGREE + +#define ATTACH_PREDICTION_INTERVAL 0.2f +#define ATTACH_PREDICTION_FACING_THRESHOLD 0.75f +#define ATTACH_PREDICTION_DIST_THRESHOLD 128 + +int ACT_PASSENGER_MELEE_ATTACK1; +int ACT_PASSENGER_THREATEN; +int ACT_PASSENGER_FLINCH; +int ACT_PASSENGER_ZOMBIE_LEAP_LOOP; + +BEGIN_DATADESC( CAI_PassengerBehaviorZombie ) + + DEFINE_FIELD( m_flLastVerticalLean, FIELD_FLOAT ), + DEFINE_FIELD( m_flLastLateralLean, FIELD_FLOAT ), + DEFINE_FIELD( m_flNextLeapTime, FIELD_TIME ), + +END_DATADESC(); + +extern int AE_PASSENGER_PHYSICS_PUSH; + +//============================================================================================== +// Passenger damage table +//============================================================================================== +static impactentry_t zombieLinearTable[] = +{ + { 200*200, 100 }, +}; + +static impactentry_t zombieAngularTable[] = +{ + { 100*100, 100 }, +}; + +impactdamagetable_t gZombiePassengerImpactDamageTable = +{ + zombieLinearTable, + zombieAngularTable, + + ARRAYSIZE(zombieLinearTable), + ARRAYSIZE(zombieAngularTable), + + 24*24, // minimum linear speed squared + 360*360, // minimum angular speed squared (360 deg/s to cause spin/slice damage) + 2, // can't take damage from anything under 2kg + + 5, // anything less than 5kg is "small" + 5, // never take more than 5 pts of damage from anything under 5kg + 36*36, // <5kg objects must go faster than 36 in/s to do damage + + VPHYSICS_LARGE_OBJECT_MASS, // large mass in kg + 4, // large mass scale (anything over 500kg does 4X as much energy to read from damage table) + 5, // large mass falling scale (emphasize falling/crushing damage over sideways impacts since the stress will kill you anyway) + 0.0f, // min vel +}; + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CAI_PassengerBehaviorZombie::CAI_PassengerBehaviorZombie( void ) : +m_flLastVerticalLean( 0.0f ), +m_flLastLateralLean( 0.0f ), +m_flNextLeapTime( 0.0f ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorZombie::CanEnterVehicle( void ) +{ + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Translate into vehicle passengers +//----------------------------------------------------------------------------- +int CAI_PassengerBehaviorZombie::TranslateSchedule( int scheduleType ) +{ + // We do different animations when inside the vehicle + if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) + { + if ( scheduleType == SCHED_MELEE_ATTACK1 ) + return SCHED_PASSENGER_ZOMBIE_MELEE_ATTACK1; + + if ( scheduleType == SCHED_RANGE_ATTACK1 ) + return SCHED_PASSENGER_ZOMBIE_RANGE_ATTACK1; + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : activity - +// Output : Activity +//----------------------------------------------------------------------------- +Activity CAI_PassengerBehaviorZombie::NPC_TranslateActivity( Activity activity ) +{ + Activity nNewActivity = BaseClass::NPC_TranslateActivity( activity ); + if ( activity == ACT_IDLE ) + return (Activity) ACT_PASSENGER_IDLE; + + return nNewActivity; +} + +//----------------------------------------------------------------------------- +// Purpose: Suppress melee attacks against enemies for the given duration +// Input : flDuration - Amount of time to suppress the attacks +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::SuppressAttack( float flDuration ) +{ + GetOuter()->SetNextAttack( gpGlobals->curtime + flDuration ); +} + +//----------------------------------------------------------------------------- +// Purpose: Determines if an enemy is inside a vehicle or not +// Output : Returns true if the enemy is outside the vehicle. +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorZombie::EnemyInVehicle( void ) +{ + // Obviously they're not... + if ( GetOuter()->GetEnemy() == NULL ) + return false; + + // See if they're in a vehicle, currently + CBaseCombatCharacter *pCCEnemy = GetOuter()->GetEnemy()->MyCombatCharacterPointer(); + if ( pCCEnemy && pCCEnemy->IsInAVehicle() ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Select a schedule when we're outside of the vehicle +//----------------------------------------------------------------------------- +int CAI_PassengerBehaviorZombie::SelectOutsideSchedule( void ) +{ + // Attaching to target + if ( HasCondition( COND_CAN_RANGE_ATTACK1 ) ) + return SCHED_PASSENGER_ZOMBIE_RANGE_ATTACK1; + + // Attack the player if we're able + if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) ) + return SCHED_MELEE_ATTACK1; + + // Attach to the vehicle + if ( HasCondition( COND_PASSENGER_ZOMBIE_CAN_ATTACH_TO_VEHICLE ) ) + return SCHED_PASSENGER_ZOMBIE_ATTACH; + + // Otherwise chase after him + return SCHED_PASSENGER_ZOMBIE_RUN_TO_VEHICLE; +} + +//----------------------------------------------------------------------------- +// Purpose: Pick a schedule for being "inside" the vehicle +//----------------------------------------------------------------------------- +int CAI_PassengerBehaviorZombie::SelectInsideSchedule( void ) +{ + // Attacking target + if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) ) + return SCHED_PASSENGER_ZOMBIE_MELEE_ATTACK1; + + return SCHED_IDLE_STAND; +} + +//----------------------------------------------------------------------------- +// Purpose: Move the zombie to the vehicle +//----------------------------------------------------------------------------- +int CAI_PassengerBehaviorZombie::SelectSchedule( void ) +{ + // See if our enemy got out + if ( GetOuter()->GetEnemy() != NULL && EnemyInVehicle() == false ) + { + if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) + { + // Exit the vehicle + SetCondition( COND_PASSENGER_EXITING ); + } + else if ( GetPassengerState() == PASSENGER_STATE_OUTSIDE ) + { + // Our target has left the vehicle and we're outside as well, so give up + Disable(); + return BaseClass::SelectSchedule(); + } + } + + // Entering schedule + if ( HasCondition( COND_PASSENGER_ENTERING ) ) + { + ClearCondition( COND_PASSENGER_ENTERING ); + return SCHED_PASSENGER_ZOMBIE_ENTER_VEHICLE; + } + + // Exiting schedule + if ( HasCondition( COND_PASSENGER_EXITING ) ) + { + ClearCondition( COND_PASSENGER_EXITING ); + return SCHED_PASSENGER_ZOMBIE_EXIT_VEHICLE; + } + + // Select different schedules based on our state + PassengerState_e nState = GetPassengerState(); + int nNewSchedule = SCHED_NONE; + + if ( nState == PASSENGER_STATE_INSIDE ) + { + nNewSchedule = SelectInsideSchedule(); + if ( nNewSchedule != SCHED_NONE ) + return nNewSchedule; + } + else if ( nState == PASSENGER_STATE_OUTSIDE ) + { + nNewSchedule = SelectOutsideSchedule(); + if ( nNewSchedule != SCHED_NONE ) + return nNewSchedule; + } + + // Worst case he just stands here + Assert(0); + return SCHED_IDLE_STAND; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorZombie::CanJumpToAttachToVehicle( void ) +{ + // FIXME: Probably move this up one level and out of this function + if ( m_flNextLeapTime > gpGlobals->curtime ) + return false; + + // Predict an attachment jump + CBaseEntity *pEnemy = GetOuter()->GetEnemy(); + + Vector vecPredictedPosition; + UTIL_PredictedPosition( pEnemy, 1.0f, &vecPredictedPosition ); + + float flDist = UTIL_DistApprox( vecPredictedPosition, GetOuter()->GetAbsOrigin() ); + + // If we're facing them enough, allow the jump + if ( ( flDist < JUMP_ATTACH_DIST_THRESHOLD ) && UTIL_IsFacingWithinTolerance( GetOuter(), pEnemy, JUMP_ATTACH_FACING_THRESHOLD ) ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Determine if we can jump to be on the enemy's vehicle +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +inline bool CAI_PassengerBehaviorZombie::CanBeOnEnemyVehicle( void ) +{ + CBaseCombatCharacter *pEnemy = ToBaseCombatCharacter( GetOuter()->GetEnemy() ); + if ( pEnemy != NULL ) + { + IServerVehicle *pVehicle = pEnemy->GetVehicle(); + if ( pVehicle && pVehicle->NPC_HasAvailableSeat( GetRoleName() ) ) + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::GatherConditions( void ) +{ + BaseClass::GatherConditions(); + + // Always clear the base conditions + ClearCondition( COND_CAN_MELEE_ATTACK1 ); + + // Behavior when outside the vehicle + if ( GetPassengerState() == PASSENGER_STATE_OUTSIDE ) + { + if ( CanBeOnEnemyVehicle() && CanJumpToAttachToVehicle() ) + { + SetCondition( COND_CAN_RANGE_ATTACK1 ); + } + + // Determine if we can latch on to the vehicle (out of sight) + ClearCondition( COND_PASSENGER_ZOMBIE_CAN_ATTACH_TO_VEHICLE ); + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + + if ( pPlayer != NULL && + GetOuter()->GetEnemy() == pPlayer && + pPlayer->GetVehicleEntity() == m_hVehicle ) + { + // Can't be visible to the player and must be close enough + bool bNotVisibleToPlayer = ( pPlayer->FInViewCone( GetOuter() ) == false ); + float flDistSqr = ( pPlayer->GetAbsOrigin() - GetOuter()->GetAbsOrigin() ).LengthSqr(); + bool bInRange = ( flDistSqr < Square(250.0f) ); + if ( bNotVisibleToPlayer && bInRange ) + { + // We can latch on and "enter" the vehicle + SetCondition( COND_PASSENGER_ZOMBIE_CAN_ATTACH_TO_VEHICLE ); + } + else if ( bNotVisibleToPlayer == false && flDistSqr < Square(128.0f) ) + { + // Otherwise just hit the vehicle in anger + SetCondition( COND_CAN_MELEE_ATTACK1 ); + } + } + } + + // Behavior when on the car + if ( GetPassengerState() == PASSENGER_STATE_INSIDE ) + { + // Check for melee attack + if ( GetOuter()->GetNextAttack() < gpGlobals->curtime ) + { + SetCondition( COND_CAN_MELEE_ATTACK1 ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Handle death case +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::Event_Killed( const CTakeDamageInfo &info ) +{ + if ( m_hVehicle ) + { + // Stop taking messages from the vehicle + m_hVehicle->RemovePhysicsChild( GetOuter() ); + m_hVehicle->NPC_RemovePassenger( GetOuter() ); + m_hVehicle->NPC_FinishedExitVehicle( GetOuter(), false ); + } + + BaseClass::Event_Killed( info ); +} + +//----------------------------------------------------------------------------- +// Purpose: Build our custom interrupt cases for the behavior +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::BuildScheduleTestBits( void ) +{ + // Always interrupt when we need to get in or out + if ( GetPassengerState() == PASSENGER_STATE_OUTSIDE ) + { + GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_CAN_RANGE_ATTACK1 ) ); + GetOuter()->SetCustomInterruptCondition( GetClassScheduleIdSpace()->ConditionLocalToGlobal( COND_PASSENGER_ENTERING ) ); + } + + BaseClass::BuildScheduleTestBits(); +} + +//----------------------------------------------------------------------------- +// Purpose: Get the absolute position of the desired attachment point +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::GetAttachmentPoint( Vector *vecPoint ) +{ + Vector vecEntryOffset, vecFinalOffset; + GetEntryTarget( &vecEntryOffset, NULL ); + VectorRotate( vecEntryOffset, m_hVehicle->GetAbsAngles(), vecFinalOffset ); + *vecPoint = ( m_hVehicle->GetAbsOrigin() + vecFinalOffset ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +int CAI_PassengerBehaviorZombie::FindExitSequence( void ) +{ + // Get a list of all our animations + const PassengerSeatAnims_t *pExitAnims = m_hVehicle->GetServerVehicle()->NPC_GetPassengerSeatAnims( GetOuter(), PASSENGER_SEAT_EXIT ); + if ( pExitAnims == NULL ) + return -1; + + // Test each animation (sorted by priority) for the best match + for ( int i = 0; i < pExitAnims->Count(); i++ ) + { + // Find the activity for this animation name + int nSequence = GetOuter()->LookupSequence( STRING( pExitAnims->Element(i).GetAnimationName() ) ); + Assert( nSequence != -1 ); + if ( nSequence == -1 ) + continue; + + return nSequence; + } + + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::StartDismount( void ) +{ + // Leap off the vehicle + int nSequence = FindExitSequence(); + Assert( nSequence != -1 ); + + SetTransitionSequence( nSequence ); + GetOuter()->SetIdealActivity( ACT_SCRIPT_CUSTOM_MOVE ); + + // This removes the NPC from the vehicle's handling and fires all necessary outputs + m_hVehicle->RemovePhysicsChild( GetOuter() ); + m_hVehicle->NPC_RemovePassenger( GetOuter() ); + m_hVehicle->NPC_FinishedExitVehicle( GetOuter(), (IsPassengerHostile()==false) ); + + // Detach from the parent + GetOuter()->SetParent( NULL ); + GetOuter()->SetMoveType( MOVETYPE_STEP ); + GetMotor()->SetYawLocked( false ); + + QAngle vecAngles = GetAbsAngles(); + vecAngles.z = 0.0f; + GetOuter()->SetAbsAngles( vecAngles ); + + // HACK: Will this work? + IPhysicsObject *pPhysObj = GetOuter()->VPhysicsGetObject(); + if ( pPhysObj != NULL ) + { + pPhysObj->EnableCollisions( true ); + } + + // Clear this + m_PassengerIntent = PASSENGER_INTENT_NONE; + SetPassengerState( PASSENGER_STATE_EXITING ); + + // Get the velocity + Vector vecUp, vecJumpDir; + GetOuter()->GetVectors( &vecJumpDir, NULL, &vecUp ); + + // Move back and up + vecJumpDir *= random->RandomFloat( -400.0f, -500.0f ); + vecJumpDir += vecUp * 150.0f; + GetOuter()->SetAbsVelocity( vecJumpDir ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::FinishDismount( void ) +{ + SetPassengerState( PASSENGER_STATE_OUTSIDE ); + Disable(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::StartTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_FACE_HINTNODE: + case TASK_FACE_LASTPOSITION: + case TASK_FACE_SAVEPOSITION: + case TASK_FACE_TARGET: + case TASK_FACE_IDEAL: + case TASK_FACE_SCRIPT: + case TASK_FACE_PATH: + TaskComplete(); + break; + + case TASK_PASSENGER_ZOMBIE_RANGE_ATTACK1: + break; + + case TASK_MELEE_ATTACK1: + { + // Only override this if we're "in" the vehicle + if ( GetPassengerState() != PASSENGER_STATE_INSIDE ) + { + BaseClass::StartTask( pTask ); + break; + } + + // Swipe + GetOuter()->SetIdealActivity( (Activity) ACT_PASSENGER_MELEE_ATTACK1 ); + + // Randomly attack again in the future + float flWait = random->RandomFloat( 0.0f, 1.0f ); + SuppressAttack( flWait ); + } + break; + + case TASK_PASSENGER_ZOMBIE_DISMOUNT: + { + // Start the process of dismounting from the vehicle + StartDismount(); + } + break; + + case TASK_PASSENGER_ZOMBIE_ATTACH: + { + if ( AttachToVehicle() ) + { + TaskComplete(); + return; + } + + TaskFail( "Unable to attach to vehicle!" ); + } + break; + + default: + BaseClass::StartTask( pTask ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Handle task running +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::RunTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_PASSENGER_ZOMBIE_RANGE_ATTACK1: + { + // Face the entry point + Vector vecAttachPoint; + GetAttachmentPoint( &vecAttachPoint ); + GetOuter()->GetMotor()->SetIdealYawToTarget( vecAttachPoint ); + + // All done when you touch the ground + if ( GetOuter()->GetFlags() & FL_ONGROUND ) + { + m_flNextLeapTime = gpGlobals->curtime + 2.0f; + TaskComplete(); + return; + } + } + break; + + case TASK_MELEE_ATTACK1: + + if ( GetOuter()->IsSequenceFinished() ) + { + TaskComplete(); + } + + break; + + case TASK_PASSENGER_ZOMBIE_DISMOUNT: + { + if ( GetOuter()->IsSequenceFinished() ) + { + // Completely separate from the vehicle + FinishDismount(); + TaskComplete(); + } + + break; + } + + default: + BaseClass::RunTask( pTask ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Find the relative cost of an entry point based on facing +// Input : &vecEntryPos - Position we're evaluating +// Output : Returns the cost as a modified distance value +//----------------------------------------------------------------------------- +float CAI_PassengerBehaviorZombie::GetEntryPointCost( const Vector &vecEntryPos ) +{ + // FIXME: We don't care about cost any longer! + return 1.0f; + + // Find the direction from us to the entry point + Vector vecEntryDir = ( vecEntryPos - GetAbsOrigin() ); + float flCost = VectorNormalize( vecEntryDir ); + + // Get our current facing + Vector vecDir; + GetOuter()->GetVectors( &vecDir, NULL, NULL ); + + // Scale our cost by how closely it matches our facing + float flDot = DotProduct( vecEntryDir, vecDir ); + if ( flDot < 0.0f ) + return FLT_MAX; + + flCost *= RemapValClamped( flDot, 1.0f, 0.0f, 1.0f, 2.0f ); + + return flCost; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : bNearest - +// Output : int +//----------------------------------------------------------------------------- +int CAI_PassengerBehaviorZombie::FindEntrySequence( bool bNearest /*= false*/ ) +{ + // Get a list of all our animations + const PassengerSeatAnims_t *pEntryAnims = m_hVehicle->GetServerVehicle()->NPC_GetPassengerSeatAnims( GetOuter(), PASSENGER_SEAT_ENTRY ); + if ( pEntryAnims == NULL ) + return -1; + + Vector vecStartPos; + const CPassengerSeatTransition *pTransition; + float flBestCost = FLT_MAX; + float flCost; + int nBestSequence = -1; + int nSequence = -1; + + // Test each animation (sorted by priority) for the best match + for ( int i = 0; i < pEntryAnims->Count(); i++ ) + { + // Find the activity for this animation name + pTransition = &pEntryAnims->Element(i); + nSequence = GetOuter()->LookupSequence( STRING( pTransition->GetAnimationName() ) ); + + Assert( nSequence != -1 ); + if ( nSequence == -1 ) + continue; + + // Test this entry for validity + GetEntryPoint( nSequence, &vecStartPos ); + + // Evaluate the cost + flCost = GetEntryPointCost( vecStartPos ); + if ( flCost < flBestCost ) + { + nBestSequence = nSequence; + flBestCost = flCost; + continue; + } + } + + return nBestSequence; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::ExitVehicle( void ) +{ + BaseClass::ExitVehicle(); + + // Remove us as a passenger + m_hVehicle->NPC_RemovePassenger( GetOuter() ); + m_hVehicle->NPC_FinishedExitVehicle( GetOuter(), false ); + +} + +//----------------------------------------------------------------------------- +// Purpose: Calculate our body lean based on our delta velocity +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::CalculateBodyLean( void ) +{ + // Calculate our lateral displacement from a perfectly centered start + float flLateralDisp = SimpleSplineRemapVal( m_vehicleState.m_vecLastAngles.z, 100.0f, -100.0f, -1.0f, 1.0f ); + flLateralDisp = clamp( flLateralDisp, -1.0f, 1.0f ); + + // FIXME: Framerate dependant! + m_flLastLateralLean = ( m_flLastLateralLean * 0.2f ) + ( flLateralDisp * 0.8f ); + + // Factor in a "stun" if the zombie was moved too far off course + if ( fabs( m_flLastLateralLean ) > 0.75f ) + { + SuppressAttack( 0.5f ); + } + + // Calc our vertical displacement + float flVerticalDisp = SimpleSplineRemapVal( m_vehicleState.m_vecDeltaVelocity.z, -50.0f, 50.0f, -1.0f, 1.0f ); + flVerticalDisp = clamp( flVerticalDisp, -1.0f, 1.0f ); + + // FIXME: Framerate dependant! + m_flLastVerticalLean = ( m_flLastVerticalLean * 0.75f ) + ( flVerticalDisp * 0.25f ); + + // Set these parameters + GetOuter()->SetPoseParameter( "lean_lateral", m_flLastLateralLean ); + GetOuter()->SetPoseParameter( "lean_vertical", m_flLastVerticalLean ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::GatherVehicleStateConditions( void ) +{ + // Call to the base + BaseClass::GatherVehicleStateConditions(); + + // Only do this if we're on the vehicle + if ( GetPassengerState() != PASSENGER_STATE_INSIDE ) + return; + + // Calculate how our body is leaning + CalculateBodyLean(); + + // The forward delta of the vehicle + float flLateralDelta = ( m_vehicleState.m_vecDeltaVelocity.x + m_vehicleState.m_vecDeltaVelocity.y ); + + // Detect a sudden stop + if ( flLateralDelta < -350.0f ) + { + if ( m_hVehicle ) + { + Vector vecDamageForce; + m_hVehicle->GetVelocity( &vecDamageForce, NULL ); + VectorNormalize( vecDamageForce ); + vecDamageForce *= random->RandomFloat( 50000.0f, 60000.0f ); + + //NDebugOverlay::HorzArrow( GetAbsOrigin(), GetAbsOrigin() + ( vecDamageForce * 256.0f ), 16.0f, 255, 0, 0, 16, true, 2.0f ); + + // Fake it! + CTakeDamageInfo info( m_hVehicle, m_hVehicle, vecDamageForce, GetOuter()->WorldSpaceCenter(), 200, (DMG_CRUSH|DMG_VEHICLE) ); + GetOuter()->TakeDamage( info ); + } + } + else if ( flLateralDelta < -150.0f ) + { + // FIXME: Realistically this should interrupt and play a schedule to do it + GetOuter()->SetIdealActivity( (Activity) ACT_PASSENGER_FLINCH ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEvent - +//----------------------------------------------------------------------------- +void CAI_PassengerBehaviorZombie::HandleAnimEvent( animevent_t *pEvent ) +{ + if ( pEvent->event == AE_PASSENGER_PHYSICS_PUSH ) + { + // Add a push into the vehicle + float flForce = (float) atof( pEvent->options ); + AddPhysicsPush( flForce * 0.75f ); + return; + } + + BaseClass::HandleAnimEvent( pEvent ); +} + +//----------------------------------------------------------------------------- +// Purpose: Attach to the vehicle if we're able +//----------------------------------------------------------------------------- +bool CAI_PassengerBehaviorZombie::AttachToVehicle( void ) +{ + // Must be able to enter the vehicle + if ( m_hVehicle->NPC_CanEnterVehicle( GetOuter(), false ) == false ) + return false; + + // Reserve the seat + if ( ReserveEntryPoint( VEHICLE_SEAT_ANY ) == false ) + return false; + + // Use the best one we've found + int nSequence = FindEntrySequence(); + if ( nSequence == -1 ) + return false; + + // Take the transition sequence + SetTransitionSequence( nSequence ); + + // Get in the vehicle + EnterVehicle(); + + // Start our scripted sequence with any other passengers + // Find Alyx + // TODO: Iterate through the list of passengers in the vehicle and find one we can interact with + CNPC_Alyx *pAlyx = CNPC_Alyx::GetAlyx(); + if ( pAlyx ) + { + // Tell Alyx to play along! + pAlyx->ForceVehicleInteraction( GetOuter()->GetSequenceName( nSequence ), GetOuter() ); + } + + return true; +} + +AI_BEGIN_CUSTOM_SCHEDULE_PROVIDER( CAI_PassengerBehaviorZombie ) +{ + DECLARE_ACTIVITY( ACT_PASSENGER_MELEE_ATTACK1 ) + DECLARE_ACTIVITY( ACT_PASSENGER_THREATEN ) + DECLARE_ACTIVITY( ACT_PASSENGER_FLINCH ) + DECLARE_ACTIVITY( ACT_PASSENGER_ZOMBIE_LEAP_LOOP ) + + DECLARE_TASK( TASK_PASSENGER_ZOMBIE_RANGE_ATTACK1 ) + DECLARE_TASK( TASK_PASSENGER_ZOMBIE_DISMOUNT ) + DECLARE_TASK( TASK_PASSENGER_ZOMBIE_ATTACH ) + + DECLARE_CONDITION( COND_PASSENGER_ZOMBIE_CAN_ATTACH_TO_VEHICLE ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_ZOMBIE_ENTER_VEHICLE, + + " Tasks" + " TASK_PASSENGER_ATTACH_TO_VEHICLE 0" + " TASK_PASSENGER_ENTER_VEHICLE 0" + "" + " Interrupts" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_ZOMBIE_EXIT_VEHICLE, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_PASSENGER_IDLE" + " TASK_STOP_MOVING 0" + " TASK_PASSENGER_ZOMBIE_DISMOUNT 0" + "" + " Interrupts" + " COND_TASK_FAILED" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_ZOMBIE_MELEE_ATTACK1, + + " Tasks" + " TASK_ANNOUNCE_ATTACK 1" + " TASK_MELEE_ATTACK1 0" + "" + " Interrupts" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_ZOMBIE_RANGE_ATTACK1, + + " Tasks" + " TASK_PLAY_SEQUENCE ACTIVITY:ACT_PASSENGER_RANGE_ATTACK1" + " TASK_SET_ACTIVITY ACTIVITY:ACT_PASSENGER_ZOMBIE_LEAP_LOOP" + " TASK_PASSENGER_ZOMBIE_RANGE_ATTACK1 0" + " " + " Interrupts" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_ZOMBIE_RUN_TO_VEHICLE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_CHASE_ENEMY_FAILED" + " TASK_GET_CHASE_PATH_TO_ENEMY 2400" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_ENEMY_UNREACHABLE" + " COND_TASK_FAILED" + " COND_LOST_ENEMY" + " COND_PASSENGER_ZOMBIE_CAN_ATTACH_TO_VEHICLE" + ) + + DEFINE_SCHEDULE + ( + SCHED_PASSENGER_ZOMBIE_ATTACH, + + " Tasks" + " TASK_PASSENGER_ZOMBIE_ATTACH 0" + "" + " Interrupts" + ) + + AI_END_CUSTOM_SCHEDULE_PROVIDER() +} diff --git a/game/server/episodic/ai_behavior_passenger_zombie.h b/game/server/episodic/ai_behavior_passenger_zombie.h new file mode 100644 index 0000000..98b2ea0 --- /dev/null +++ b/game/server/episodic/ai_behavior_passenger_zombie.h @@ -0,0 +1,97 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Zombies on cars! +// +//============================================================================= + +#ifndef AI_BEHAVIOR_PASSENGER_ZOMBIE_H +#define AI_BEHAVIOR_PASSENGER_ZOMBIE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "ai_behavior_passenger.h" +#include "ai_utils.h" +#include "vehicle_base.h" + +extern impactdamagetable_t gZombiePassengerImpactDamageTable; + +class CAI_PassengerBehaviorZombie : public CAI_PassengerBehavior +{ + DECLARE_CLASS( CAI_PassengerBehaviorZombie, CAI_PassengerBehavior ); + DECLARE_DATADESC() + +public: + + CAI_PassengerBehaviorZombie( void ); + + enum + { + // Schedules + SCHED_PASSENGER_ZOMBIE_ENTER_VEHICLE = BaseClass::NEXT_SCHEDULE, + SCHED_PASSENGER_ZOMBIE_EXIT_VEHICLE, + SCHED_PASSENGER_ZOMBIE_MELEE_ATTACK1, + SCHED_PASSENGER_ZOMBIE_RANGE_ATTACK1, + SCHED_PASSENGER_ZOMBIE_ATTACH, + SCHED_PASSENGER_ZOMBIE_RUN_TO_VEHICLE, + NEXT_SCHEDULE, + + // Tasks + TASK_PASSENGER_ZOMBIE_RANGE_ATTACK1 = BaseClass::NEXT_TASK, + TASK_PASSENGER_ZOMBIE_DISMOUNT, + TASK_PASSENGER_ZOMBIE_ATTACH, + NEXT_TASK, + + // Conditions + COND_PASSENGER_ZOMBIE_CAN_ATTACH_TO_VEHICLE = BaseClass::NEXT_CONDITION, + NEXT_CONDITION + }; + + virtual const char *GetName( void ) { return "ZombiePassenger"; } + virtual string_t GetRoleName( void ) { return MAKE_STRING( "passenger_zombie" ); } + virtual int SelectSchedule( void ); + virtual int TranslateSchedule( int scheduleType ); + virtual void GatherConditions( void ); + virtual void Event_Killed( const CTakeDamageInfo &info ); + virtual void BuildScheduleTestBits( void ); + virtual void RunTask( const Task_t *pTask ); + virtual void StartTask( const Task_t *pTask ); + virtual bool CanEnterVehicle( void ); + virtual void ExitVehicle( void ); + virtual void HandleAnimEvent( animevent_t *pEvent ); + virtual Activity NPC_TranslateActivity( Activity activity ); + + virtual bool AttachToVehicle( void ); + + void SuppressAttack( float flDuration ); + + DEFINE_CUSTOM_SCHEDULE_PROVIDER; + +protected: + + int SelectOutsideSchedule( void ); + int SelectInsideSchedule( void ); + virtual int FindExitSequence( void ); + void StartDismount( void ); + void FinishDismount( void ); + virtual void CalculateBodyLean( void ); + virtual void GatherVehicleStateConditions( void ); + virtual int FindEntrySequence( bool bNearest = false ); + +private: + + void VehicleLeapAttackTouch( CBaseEntity *pOther ); + void VehicleLeapAttack( void ); + bool CanBeOnEnemyVehicle( void ); + float GetEntryPointCost( const Vector &vecEntryPos ); + bool EnemyInVehicle( void ); + void GetAttachmentPoint( Vector *vecPoint ); + bool CanJumpToAttachToVehicle( void ); + //bool WithinAttachRange( void ); + + float m_flLastLateralLean; + float m_flLastVerticalLean; + float m_flNextLeapTime; +}; + +#endif // AI_BEHAVIOR_PASSENGER_ZOMBIE_H diff --git a/game/server/episodic/ep1_gamestats.cpp b/game/server/episodic/ep1_gamestats.cpp new file mode 100644 index 0000000..a82c27d --- /dev/null +++ b/game/server/episodic/ep1_gamestats.cpp @@ -0,0 +1,74 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +//Gamestats was built for ep1, so this file is going to be amazingly short seeing as how ep1 set the standard + +#include "cbase.h" +#include "ep1_gamestats.h" +#include "tier1/utlbuffer.h" +static CEP1GameStats s_CEP1GS_ThisJustSitsInMemory; + +// A bit of a hack to redirect the gamestats API for ep2 (ep3, etc.) +extern CBaseGameStats *g_pEP2GameStats; + +CEP1GameStats::CEP1GameStats( void ) +{ + gamestats = &s_CEP1GS_ThisJustSitsInMemory; +} + +CBaseGameStats *CEP1GameStats::OnInit( CBaseGameStats *pCurrentGameStats, char const *gamedir ) +{ + if ( !Q_stricmp( gamedir, "ep2" ) ) + { + return g_pEP2GameStats; + } + + return pCurrentGameStats; +} + +const char *CEP1GameStats::GetStatSaveFileName( void ) +{ + return "ep1_gamestats.dat"; //overriding the default for backwards compatibility with release stat tracking code +} + +const char *CEP1GameStats::GetStatUploadRegistryKeyName( void ) +{ + return "GameStatsUpload_Ep1"; //overriding the default for backwards compatibility with release stat tracking code +} + + +static char const *ep1Maps[] = +{ + "ep1_citadel_00", + "ep1_citadel_01", + "ep1_citadel_02", + "ep1_citadel_02b", + "ep1_citadel_03", + "ep1_citadel_04", + "ep1_c17_00", + "ep1_c17_00a", + "ep1_c17_01", + "ep1_c17_02", + "ep1_c17_02b", + "ep1_c17_02a", + "ep1_c17_05", + "ep1_c17_06", +}; + + +bool CEP1GameStats::UserPlayedAllTheMaps( void ) +{ + int c = ARRAYSIZE( ep1Maps ); + for ( int i = 0; i < c; ++i ) + { + int idx = m_BasicStats.m_MapTotals.Find( ep1Maps[ i ] ); + if( idx == m_BasicStats.m_MapTotals.InvalidIndex() ) + return false; + } + + return true; +} + diff --git a/game/server/episodic/ep1_gamestats.h b/game/server/episodic/ep1_gamestats.h new file mode 100644 index 0000000..5cd1356 --- /dev/null +++ b/game/server/episodic/ep1_gamestats.h @@ -0,0 +1,31 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef EP1_GAMESTATS_H +#define EP1_GAMESTATS_H +#ifdef _WIN32 +#pragma once +#endif + +#include "gamestats.h" + +class CEP1GameStats : public CBaseGameStats +{ + typedef CBaseGameStats BaseClass; + +public: + CEP1GameStats( void ); + + virtual CBaseGameStats *OnInit( CBaseGameStats *pCurrentGameStats, char const *gamedir ); + + virtual bool StatTrackingEnabledForMod( void ) { return true; } + virtual bool UserPlayedAllTheMaps( void ); + + virtual const char *GetStatSaveFileName( void ); + virtual const char *GetStatUploadRegistryKeyName( void ); +}; + +#endif // EP1_GAMESTATS_H diff --git a/game/server/episodic/ep2_gamestats.cpp b/game/server/episodic/ep2_gamestats.cpp new file mode 100644 index 0000000..af6d5e9 --- /dev/null +++ b/game/server/episodic/ep2_gamestats.cpp @@ -0,0 +1,585 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= +#if defined( GAME_DLL ) +#include "cbase.h" +#endif +#include "ep2_gamestats.h" +#include "tier1/utlbuffer.h" +#include "vehicle_base.h" +#include "tier1/utlstring.h" +#include "filesystem.h" +#include "icommandline.h" + +static CEP2GameStats s_CEP2GameStats_Singleton; +CBaseGameStats *g_pEP2GameStats = &s_CEP2GameStats_Singleton; + + +CEP2GameStats::CEP2GameStats( void ) +{ + Q_memset( m_flInchesRemainder, 0, sizeof( m_flInchesRemainder ) ); + m_pCurrentMap = NULL; + m_dictMapStats.Purge(); +} + +const char *CEP2GameStats::GetStatSaveFileName( void ) +{ + //overriding the default for backwards compatibility with release stat tracking code + return "ep2_gamestats.dat"; +} + +const char *CEP2GameStats::GetStatUploadRegistryKeyName( void ) +{ + //overriding the default for backwards compatibility with release stat tracking code + return "GameStatsUpload_Ep2"; +} + + +static char const *ep2Maps[] = +{ + "ep2_outland_01", + "ep2_outland_02", + "ep2_outland_03", + "ep2_outland_04", + "ep2_outland_05", + "ep2_outland_06", + "ep2_outland_06a", + "ep2_outland_07", + "ep2_outland_08", + "ep2_outland_09", + "ep2_outland_10", + "ep2_outland_10a", + "ep2_outland_11", + "ep2_outland_11a", + "ep2_outland_12", + "ep2_outland_12a" +}; + + +bool CEP2GameStats::UserPlayedAllTheMaps( void ) +{ + int c = ARRAYSIZE( ep2Maps ); + for ( int i = 0; i < c; ++i ) + { + int idx = m_BasicStats.m_MapTotals.Find( ep2Maps[ i ] ); + if( idx == m_BasicStats.m_MapTotals.InvalidIndex() ) + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Destructor +// Input : - +//----------------------------------------------------------------------------- +CEP2GameStats::~CEP2GameStats() +{ + m_pCurrentMap = NULL; + m_dictMapStats.Purge(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &SaveBuffer - +//----------------------------------------------------------------------------- +void CEP2GameStats::AppendCustomDataToSaveBuffer( CUtlBuffer &SaveBuffer ) +{ + // Save data per map. + for ( int iMap = m_dictMapStats.First(); iMap != m_dictMapStats.InvalidIndex(); iMap = m_dictMapStats.Next( iMap ) ) + { + // Get the current map. + Ep2LevelStats_t *pCurrentMap = &m_dictMapStats[iMap]; + Assert( pCurrentMap ); + pCurrentMap->AppendToBuffer( SaveBuffer ); + } +} + +void CEP2GameStats::LoadCustomDataFromBuffer( CUtlBuffer &LoadBuffer ) +{ + Ep2LevelStats_t::LoadData( m_dictMapStats, LoadBuffer ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEP2GameStats::Event_LevelInit( void ) +{ + BaseClass::Event_LevelInit(); + + char const *pchTag = NULL; + CommandLine()->CheckParm( "-gamestatstag", &pchTag ); + if ( !pchTag ) + { + pchTag = ""; + } + + m_pCurrentMap = FindOrAddMapStats( STRING( gpGlobals->mapname ) ); + m_pCurrentMap->Init( STRING( gpGlobals->mapname ), gpGlobals->curtime, pchTag, gpGlobals->mapversion ); +} + +Ep2LevelStats_t::EntityDeathsLump_t *CEP2GameStats::FindDeathsLump( char const *npcName ) +{ + if ( !m_pCurrentMap ) + return NULL; + + char const *name = npcName; + // Hack to fixup name + if ( !Q_stricmp( name, "npc_ministrider" ) ) + { + name = "npc_hunter"; + } + + if ( Q_strnicmp( name, "npc_", 4 ) ) + return NULL; + + int idx = m_pCurrentMap->m_dictEntityDeaths.Find( name ); + if ( idx == m_pCurrentMap->m_dictEntityDeaths.InvalidIndex() ) + { + idx = m_pCurrentMap->m_dictEntityDeaths.Insert( name ); + } + + return &m_pCurrentMap->m_dictEntityDeaths[ idx ]; +} + +Ep2LevelStats_t::WeaponLump_t *CEP2GameStats::FindWeaponsLump( char const *pchWeaponName, bool bPrimary ) +{ + if ( !m_pCurrentMap ) + return NULL; + + if ( !pchWeaponName ) + { + AssertOnce( !"FindWeaponsLump pchWeaponName == NULL" ); + return NULL; + } + + char lookup[ 512 ]; + Q_snprintf( lookup, sizeof( lookup ), "%s_%s", pchWeaponName, bPrimary ? "primary" : "secondary" ); + int idx = m_pCurrentMap->m_dictWeapons.Find( lookup ); + if ( idx == m_pCurrentMap->m_dictWeapons.InvalidIndex() ) + { + idx = m_pCurrentMap->m_dictWeapons.Insert( lookup ); + } + + return &m_pCurrentMap->m_dictWeapons[ idx ]; +} + +// Finds the generic stats lump +Ep2LevelStats_t::GenericStatsLump_t *CEP2GameStats::FindGenericLump( char const *pchStatName ) +{ + if ( !m_pCurrentMap ) + return NULL; + if ( !pchStatName || !*pchStatName ) + return NULL; + + int idx = m_pCurrentMap->m_dictGeneric.Find( pchStatName ); + if ( idx == m_pCurrentMap->m_dictGeneric.InvalidIndex() ) + { + idx = m_pCurrentMap->m_dictGeneric.Insert( pchStatName ); + } + + return &m_pCurrentMap->m_dictGeneric[ idx ]; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *szMapName - +// Output : Ep2LevelStats_t +//----------------------------------------------------------------------------- +Ep2LevelStats_t *CEP2GameStats::FindOrAddMapStats( const char *szMapName ) +{ + int iMap = m_dictMapStats.Find( szMapName ); + if( iMap == m_dictMapStats.InvalidIndex() ) + { + iMap = m_dictMapStats.Insert( szMapName ); + } + + return &m_dictMapStats[iMap]; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEP2GameStats::Event_PlayerDamage( CBasePlayer *pBasePlayer, const CTakeDamageInfo &info ) +{ + BaseClass::Event_PlayerDamage( pBasePlayer, info ); + + m_pCurrentMap->m_FloatCounters[ Ep2LevelStats_t::COUNTER_DAMAGETAKEN ] += info.GetDamage(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEP2GameStats::Event_PlayerKilledOther( CBasePlayer *pAttacker, CBaseEntity *pVictim, const CTakeDamageInfo &info ) +{ + BaseClass::Event_PlayerKilledOther( pAttacker, pVictim, info ); + + if ( pAttacker ) + { + StatsLog( "Attacker: %s\n", pAttacker->GetClassname() ); + } + + if ( !pVictim ) + { + return; + } + + char const *pchVictim = pVictim->GetClassname(); + Ep2LevelStats_t::EntityDeathsLump_t *lump = FindDeathsLump( pchVictim ); + if ( lump ) + { + ++lump->m_nBodyCount; + StatsLog( "Player has killed %d %s's\n", lump->m_nBodyCount, pchVictim ); + + CPropVehicleDriveable *veh = dynamic_cast< CPropVehicleDriveable * >( pAttacker ); + if ( !veh ) + veh = dynamic_cast< CPropVehicleDriveable * >( info.GetInflictor() ); + if ( veh ) + { + CBaseEntity *driver = veh->GetDriver(); + if ( driver && driver->IsPlayer() ) + { + ++m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_VEHICULARHOMICIDES ]; + StatsLog( " Vehicular homicide [%I64d] of %s's\n", m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_VEHICULARHOMICIDES ], pchVictim ); + } + } + } + else + { + StatsLog( "Player killed %s (not tracked)\n", pchVictim ); + } +} + +void CEP2GameStats::Event_Punted( CBaseEntity *pObject ) +{ + BaseClass::Event_Punted( pObject ); + ++m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_OBJECTSPUNTED ]; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CEP2GameStats::Event_PlayerKilled( CBasePlayer *pPlayer, const CTakeDamageInfo &info ) +{ + BaseClass::Event_PlayerKilled( pPlayer, info ); + + if ( info.GetDamageType() & DMG_FALL ) + { + ++m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_FALLINGDEATHS ]; + } + + Ep2LevelStats_t::PlayerDeathsLump_t death; + + // set the location where the target died + const Vector &org = pPlayer->GetAbsOrigin(); + death.nPosition[ 0 ] = static_cast<short>( org.x ); + death.nPosition[ 1 ] = static_cast<short>( org.y ); + death.nPosition[ 2 ] = static_cast<short>( org.z ); + + StatsLog( "CEP2GameStats::Event_PlayerKilled at location [%d %d %d]\n", (int)death.nPosition[ 0 ], (int)death.nPosition[ 1 ], (int)death.nPosition[ 2 ] ); + + // set the class of the attacker + CBaseEntity *pInflictor = info.GetInflictor(); + CBaseEntity *pKiller = info.GetAttacker(); + + if ( pInflictor ) + { + StatsLog( "Inflictor: %s\n", pInflictor->GetClassname() ); + } + + if ( pKiller ) + { + char const *pchKiller = pKiller->GetClassname(); + Ep2LevelStats_t::EntityDeathsLump_t *lump = FindDeathsLump( pchKiller ); + if ( lump ) + { + ++lump->m_nKilledPlayer; + StatsLog( "Player has been killed %d times by %s's\n", lump->m_nKilledPlayer, pchKiller ); + } + else + { + StatsLog( "Player killed by %s (not tracked)\n", pchKiller ); + } + } + + // add it to the list of deaths + Ep2LevelStats_t *map = FindOrAddMapStats( STRING( gpGlobals->mapname ) ); + int slot = map->m_aPlayerDeaths.AddToTail( death ); + + Ep2LevelStats_t::SaveGameInfoRecord2_t *rec = map->m_SaveGameInfo.m_pCurrentRecord; + if ( rec ) + { + if ( rec->m_nFirstDeathIndex == -1 ) + { + rec->m_nFirstDeathIndex = slot; + } + ++rec->m_nNumDeaths; + + StatsLog( "Player has died %d times since last save/load\n", rec->m_nNumDeaths ); + } +} + +void CEP2GameStats::Event_CrateSmashed() +{ + BaseClass::Event_CrateSmashed(); + + ++m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_CRATESSMASHED ]; +} + +void CEP2GameStats::Event_PlayerTraveled( CBasePlayer *pBasePlayer, float distanceInInches, bool bInVehicle, bool bSprinting ) +{ + BaseClass::Event_PlayerTraveled( pBasePlayer, distanceInInches, bInVehicle, bSprinting ); + + int iIndex = INVEHICLE; + if ( !bInVehicle ) + { + iIndex = bSprinting ? ONFOOTSPRINTING : ONFOOT; + } + + m_flInchesRemainder[ iIndex ] += distanceInInches; + uint64 intPart = (uint64)m_flInchesRemainder[ iIndex ]; + m_flInchesRemainder[ iIndex ] -= intPart; + if ( intPart > 0 ) + { + if ( bInVehicle ) + { + m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_DISTANCE_INVEHICLE ] += intPart; + } + else + { + if ( bSprinting ) + { + m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_DISTANCE_ONFOOTSPRINTING ] += intPart; + } + else + { + m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_DISTANCE_ONFOOT ] += intPart; + } + } + } + + Ep2LevelStats_t *map = m_pCurrentMap; + if ( !map ) + return; + + Ep2LevelStats_t::SaveGameInfoRecord2_t *rec = map->m_SaveGameInfo.m_pCurrentRecord; + + if ( rec && + rec->m_nSaveHealth == -1 ) + { + Vector pos = pBasePlayer->GetAbsOrigin(); + rec->m_nSavePos[ 0 ] = (short)pos.x; + rec->m_nSavePos[ 1 ] = (short)pos.y; + rec->m_nSavePos[ 2 ] = (short)pos.z; + rec->m_nSaveHealth = clamp( pBasePlayer->GetHealth(), 0, 100 ); + } +} + +void CEP2GameStats::Event_WeaponFired( CBasePlayer *pShooter, bool bPrimary, char const *pchWeaponName ) +{ + BaseClass::Event_WeaponFired( pShooter, bPrimary, pchWeaponName ); + + Ep2LevelStats_t::WeaponLump_t *lump = FindWeaponsLump( pchWeaponName, bPrimary ); + if ( lump ) + { + ++lump->m_nShots; + } +} + +void CEP2GameStats::Event_WeaponHit( CBasePlayer *pShooter, bool bPrimary, char const *pchWeaponName, const CTakeDamageInfo &info ) +{ + BaseClass::Event_WeaponHit( pShooter, bPrimary, pchWeaponName, info ); + Ep2LevelStats_t::WeaponLump_t *lump = FindWeaponsLump( pchWeaponName, bPrimary ); + if ( lump ) + { + ++lump->m_nHits; + lump->m_flDamageInflicted += info.GetDamage(); + } +} + +void CEP2GameStats::Event_SaveGame( void ) +{ + BaseClass::Event_SaveGame(); + + Ep2LevelStats_t *map = m_pCurrentMap; + if ( !map ) + return; + + ++map->m_IntCounters[ Ep2LevelStats_t::COUNTER_SAVES ]; + StatsLog( " %I64uth save on this map\n", map->m_IntCounters[ Ep2LevelStats_t::COUNTER_SAVES ] ); + + char const *pchSaveFile = engine->GetSaveFileName(); + if ( !pchSaveFile || !pchSaveFile[ 0 ] ) + return; + + char name[ 512 ]; + Q_strncpy( name, pchSaveFile, sizeof( name ) ); + Q_strlower( name ); + Q_FixSlashes( name ); + + unsigned int uFileTime = filesystem->GetFileTime( name, "GAME" ); + // Latch off previous + map->m_SaveGameInfo.Latch( name, uFileTime ); + + Ep2LevelStats_t::SaveGameInfoRecord2_t *rec = map->m_SaveGameInfo.m_pCurrentRecord; + + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if ( pPlayer ) + { + Vector pos = pPlayer->GetAbsOrigin(); + rec->m_nSavePos[ 0 ] = (short)pos.x; + rec->m_nSavePos[ 1 ] = (short)pos.y; + rec->m_nSavePos[ 2 ] = (short)pos.z; + rec->m_nSaveHealth = clamp( pPlayer->GetHealth(), 0, 100 ); + rec->m_SaveType = Q_stristr( pchSaveFile, "autosave" ) ? + Ep2LevelStats_t::SaveGameInfoRecord2_t::TYPE_AUTOSAVE : Ep2LevelStats_t::SaveGameInfoRecord2_t::TYPE_USERSAVE; + + StatsLog( "save pos %i %i %i w/ health %d\n", + rec->m_nSavePos[ 0 ], + rec->m_nSavePos[ 1 ], + rec->m_nSavePos[ 2 ], + rec->m_nSaveHealth ); + + } +} + +void CEP2GameStats::Event_LoadGame( void ) +{ + BaseClass::Event_LoadGame(); + + Ep2LevelStats_t *map = m_pCurrentMap; + if ( !map ) + return; + + ++map->m_IntCounters[ Ep2LevelStats_t::COUNTER_LOADS ]; + StatsLog( " %I64uth load on this map\n", map->m_IntCounters[ Ep2LevelStats_t::COUNTER_LOADS ] ); + + char const *pchSaveFile = engine->GetMostRecentlyLoadedFileName(); + if ( !pchSaveFile || !pchSaveFile[ 0 ] ) + return; + + char name[ 512 ]; + Q_snprintf( name, sizeof( name ), "save/%s", pchSaveFile ); + Q_DefaultExtension( name, IsX360() ? ".360.sav" : ".sav", sizeof( name ) ); + Q_FixSlashes( name ); + Q_strlower( name ); + + Ep2LevelStats_t::SaveGameInfo_t *pSaveGameInfo = &map->m_SaveGameInfo; + + if ( pSaveGameInfo->m_nCurrentSaveFileTime == 0 || + pSaveGameInfo->m_sCurrentSaveFile != name ) + { + unsigned int uFileTime = filesystem->GetFileTime( name, "GAME" ); + + // Latch off previous + StatsLog( "Relatching save game file due to time or filename change (%s : %u)\n", name, uFileTime ); + pSaveGameInfo->Latch( name, uFileTime ); + } +} + +void CEP2GameStats::Event_FlippedVehicle( CBasePlayer *pDriver, CPropVehicleDriveable *pVehicle ) +{ + BaseClass::Event_FlippedVehicle( pDriver, pVehicle ); + ++m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_VEHICLE_OVERTURNED ]; + StatsLog( "%I64u time vehicle overturned\n", m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_VEHICLE_OVERTURNED ] ); +} + +void CEP2GameStats::Event_PreSaveGameLoaded( char const *pSaveName, bool bInGame ) +{ + BaseClass::Event_PreSaveGameLoaded( pSaveName, bInGame ); + + // Not currently in a level + if ( !bInGame ) + return; + + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if ( !pPlayer ) + return; + + // We're loading a saved game while the player is still alive (are they stuck?) + if ( pPlayer->IsAlive() ) + { + ++m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_LOADGAME_STILLALIVE ]; + StatsLog( "%I64u game loaded with living player\n", m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_LOADGAME_STILLALIVE ] ); + } +} + +void CEP2GameStats::Event_PlayerEnteredGodMode( CBasePlayer *pBasePlayer ) +{ + BaseClass::Event_PlayerEnteredGodMode( pBasePlayer ); + ++m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_GODMODES ]; + StatsLog( "%I64u time entering godmode\n", m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_GODMODES ] ); +} + +void CEP2GameStats::Event_PlayerEnteredNoClip( CBasePlayer *pBasePlayer ) +{ + BaseClass::Event_PlayerEnteredNoClip( pBasePlayer ); + ++m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_NOCLIPS ]; + StatsLog( "%I64u time entering NOCLIP\n", m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_NOCLIPS ] ); +} + +void CEP2GameStats::Event_DecrementPlayerEnteredNoClip( CBasePlayer *pBasePlayer ) +{ + BaseClass::Event_DecrementPlayerEnteredNoClip( pBasePlayer ); + if ( m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_NOCLIPS ] > 0 ) + { + --m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_NOCLIPS ]; + } + StatsLog( "%I64u decrement entering NOCLIP (entering vehicle doesn't count)\n", m_pCurrentMap->m_IntCounters[ Ep2LevelStats_t::COUNTER_NOCLIPS ] ); +} + +// Generic statistics lump +void CEP2GameStats::Event_IncrementCountedStatistic( const Vector& vecAbsOrigin, char const *pchStatisticName, float flIncrementAmount ) +{ + BaseClass::Event_IncrementCountedStatistic( vecAbsOrigin, pchStatisticName, flIncrementAmount ); + + // Find the generic lump + Ep2LevelStats_t::GenericStatsLump_t *lump = FindGenericLump( pchStatisticName ); + if ( lump ) + { + lump->m_Pos[ 0 ] = (short)vecAbsOrigin.x; + lump->m_Pos[ 1 ] = (short)vecAbsOrigin.y; + lump->m_Pos[ 2 ] = (short)vecAbsOrigin.z; + lump->m_flCurrentValue += (double)flIncrementAmount; + ++lump->m_unCount; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +static void CC_ListDeaths( const CCommand &args ) +{ + Ep2LevelStats_t *map = s_CEP2GameStats_Singleton.FindOrAddMapStats( STRING( gpGlobals->mapname ) ); + if ( !map ) + return; + + int nRendered = 0; + for ( int i = map->m_aPlayerDeaths.Count() - 1; i >= 0 ; --i, ++nRendered ) + { + Vector org( map->m_aPlayerDeaths[ i ].nPosition[ 0 ], + map->m_aPlayerDeaths[ i ].nPosition[ 1 ], + map->m_aPlayerDeaths[ i ].nPosition[ 2 ] + 36.0f ); + + // FIXME: This might overflow + NDebugOverlay::Box( org, Vector( -8, -8, -8 ), Vector( 8, 8, 8 ), 0, 255, 0, 128, 10.0f ); + + /* + Msg( "%s killed %s with %s at (%d,%d,%d)\n", + g_aClassNames[ map->m_aPlayerDeaths[ i ].iAttackClass ], + g_aClassNames[ map->m_aPlayerDeaths[ i ].iTargetClass ], + WeaponIdToAlias( map->m_aPlayerDeaths[ i ].iWeapon ), + map->m_aPlayerDeaths[ i ].nPosition[ 0 ], + map->m_aPlayerDeaths[ i ].nPosition[ 1 ], + map->m_aPlayerDeaths[ i ].nPosition[ 2 ] ); + */ + + if ( nRendered > 150 ) + break; + } + Msg( "\nlisted %d deaths\n", map->m_aPlayerDeaths.Count() ); +} + +static ConCommand listDeaths("listdeaths", CC_ListDeaths, "lists player deaths", 0 );
\ No newline at end of file diff --git a/game/server/episodic/ep2_gamestats.h b/game/server/episodic/ep2_gamestats.h new file mode 100644 index 0000000..cef1f83 --- /dev/null +++ b/game/server/episodic/ep2_gamestats.h @@ -0,0 +1,532 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef EP2_GAMESTATS_H +#define EP2_GAMESTATS_H +#ifdef _WIN32 +#pragma once +#endif + +#include "ep1_gamestats.h" +#include "tier1/utlstring.h" + +// EP2 Game Stats +enum Ep2GameStatsVersions_t +{ + EP2_GAMESTATS_FILE_VERSION_01 = 001, + EP2_GAMESTATS_FILE_VERSION_02 = 002, + + EP2_GAMESTATS_CURRENT_VERSION = EP2_GAMESTATS_FILE_VERSION_02, +}; + +enum Ep2GameStatsLumpIds_t +{ + EP2STATS_LUMP_HEADER = 1, + EP2STATS_LUMP_DEATH, + EP2STATS_LUMP_NPC, + EP2STATS_LUMP_WEAPON, + EP2STATS_LUMP_SAVEGAMEINFO, + EP2STATS_LUMP_TAG, + EP2STATS_LUMP_GENERIC, + EP2_MAX_LUMP_COUNT +}; + +// EP2 Game Level Stats Data +struct Ep2LevelStats_t +{ +public: + enum FloatCounterTypes_t + { + COUNTER_DAMAGETAKEN = 0, + + NUM_FLOATCOUNTER_TYPES, + }; + + enum IntCounterTypes_t + { + COUNTER_CRATESSMASHED = 0, + COUNTER_OBJECTSPUNTED, + COUNTER_VEHICULARHOMICIDES, + COUNTER_DISTANCE_INVEHICLE, + COUNTER_DISTANCE_ONFOOT, + COUNTER_DISTANCE_ONFOOTSPRINTING, + COUNTER_FALLINGDEATHS, + COUNTER_VEHICLE_OVERTURNED, + COUNTER_LOADGAME_STILLALIVE, + COUNTER_LOADS, + COUNTER_SAVES, + COUNTER_GODMODES, + COUNTER_NOCLIPS, + + NUM_INTCOUNTER_TYPES, + }; + + Ep2LevelStats_t() : + m_bInitialized( false ), + m_flLevelStartTime( 0.0f ) + { + Q_memset( m_IntCounters, 0, sizeof( m_IntCounters ) ); + Q_memset( m_FloatCounters, 0, sizeof( m_FloatCounters ) ); + } + ~Ep2LevelStats_t() + { + } + + Ep2LevelStats_t( const Ep2LevelStats_t &other ) + { + m_bInitialized = other.m_bInitialized; + m_flLevelStartTime = other.m_flLevelStartTime; + m_Header = other.m_Header; + m_aPlayerDeaths = other.m_aPlayerDeaths; + Q_memcpy( m_IntCounters, other.m_IntCounters, sizeof( m_IntCounters ) ); + Q_memcpy( m_FloatCounters, other.m_FloatCounters, sizeof( m_FloatCounters ) ); + int i; + for ( i = other.m_dictEntityDeaths.First(); i != other.m_dictEntityDeaths.InvalidIndex(); i = other.m_dictEntityDeaths.Next( i ) ) + { + m_dictEntityDeaths.Insert( other.m_dictEntityDeaths.GetElementName( i ), other.m_dictEntityDeaths[ i ] ); + } + for ( i = other.m_dictWeapons.First(); i != other.m_dictWeapons.InvalidIndex(); i = other.m_dictWeapons.Next( i ) ) + { + m_dictWeapons.Insert( other.m_dictWeapons.GetElementName( i ), other.m_dictWeapons[ i ] ); + } + m_SaveGameInfo = other.m_SaveGameInfo; + } + + // Create and destroy. + void Init( const char *pszMapName, float flStartTime, char const *pchTag, int nMapVersion ) + { + // Initialize. + m_Header.m_iVersion = EP2_GAMESTATS_CURRENT_VERSION; + Q_strncpy( m_Header.m_szMapName, pszMapName, sizeof( m_Header.m_szMapName ) ); + m_Header.m_flTime = 0.0f; + + // Start the level timer. + m_flLevelStartTime = flStartTime; + + Q_strncpy( m_Tag.m_szTagText, pchTag, sizeof( m_Tag.m_szTagText ) ); + m_Tag.m_nMapVersion = nMapVersion; + } + + void Shutdown( float flEndTime ) + { + m_Header.m_flTime = flEndTime - m_flLevelStartTime; + } + + void AppendToBuffer( CUtlBuffer &SaveBuffer ) + { + // Always write out as current version + m_Header.m_iVersion = EP2_GAMESTATS_CURRENT_VERSION; + + // Write out the lumps. + CBaseGameStats::AppendLump( EP2_MAX_LUMP_COUNT, SaveBuffer, EP2STATS_LUMP_HEADER, 1, sizeof( Ep2LevelStats_t::LevelHeader_t ), static_cast<void*>( &m_Header ) ); + CBaseGameStats::AppendLump( EP2_MAX_LUMP_COUNT, SaveBuffer, EP2STATS_LUMP_TAG, 1, sizeof( Ep2LevelStats_t::Tag_t ), static_cast< void * >( &m_Tag ) ); + CBaseGameStats::AppendLump( EP2_MAX_LUMP_COUNT, SaveBuffer, EP2STATS_LUMP_DEATH, m_aPlayerDeaths.Count(), sizeof( Ep2LevelStats_t::PlayerDeathsLump_t ), static_cast<void*>( m_aPlayerDeaths.Base() ) ); + { + CUtlBuffer buf; + buf.Put( (const void *)m_IntCounters, sizeof( m_IntCounters ) ); + buf.Put( (const void *)m_FloatCounters, sizeof( m_FloatCounters ) ); + buf.PutInt( m_dictEntityDeaths.Count() ); + for ( int i = m_dictEntityDeaths.First(); i != m_dictEntityDeaths.InvalidIndex(); i = m_dictEntityDeaths.Next( i ) ) + { + buf.PutString( m_dictEntityDeaths.GetElementName( i ) ); + buf.Put( (const void *)&m_dictEntityDeaths[ i ], sizeof( Ep2LevelStats_t::EntityDeathsLump_t ) ); + } + CBaseGameStats::AppendLump( EP2_MAX_LUMP_COUNT, SaveBuffer, EP2STATS_LUMP_NPC, 1, buf.TellPut(), buf.Base() ); + } + { + CUtlBuffer buf; + buf.PutInt( m_dictWeapons.Count() ); + for ( int i = m_dictWeapons.First(); i != m_dictWeapons.InvalidIndex(); i = m_dictWeapons.Next( i ) ) + { + buf.PutString( m_dictWeapons.GetElementName( i ) ); + buf.Put( (const void *)&m_dictWeapons[ i ], sizeof( Ep2LevelStats_t::WeaponLump_t ) ); + } + CBaseGameStats::AppendLump( EP2_MAX_LUMP_COUNT, SaveBuffer, EP2STATS_LUMP_WEAPON, 1, buf.TellPut(), buf.Base() ); + } + { + CUtlBuffer buf; + buf.PutString( m_SaveGameInfo.m_sCurrentSaveFile.String() ); + buf.PutInt( m_SaveGameInfo.m_nCurrentSaveFileTime ); + buf.PutInt( m_SaveGameInfo.m_Records.Count() ); + for ( int i = 0 ; i < m_SaveGameInfo.m_Records.Count(); ++i ) + { + buf.Put( (const void *)&m_SaveGameInfo.m_Records[ i ], sizeof( Ep2LevelStats_t::SaveGameInfoRecord2_t ) ); + } + CBaseGameStats::AppendLump( EP2_MAX_LUMP_COUNT, SaveBuffer, EP2STATS_LUMP_SAVEGAMEINFO, 1, buf.TellPut(), buf.Base() ); + } + { + CUtlBuffer buf; + buf.PutShort( Ep2LevelStats_t::GenericStatsLump_t::LumpVersion ); + buf.PutInt( m_dictGeneric.Count() ); + for ( int i = m_dictGeneric.First(); i != m_dictGeneric.InvalidIndex(); i = m_dictGeneric.Next( i ) ) + { + buf.PutString( m_dictGeneric.GetElementName( i ) ); + buf.Put( (const void *)&m_dictGeneric[ i ], sizeof( Ep2LevelStats_t::GenericStatsLump_t ) ); + } + CBaseGameStats::AppendLump( EP2_MAX_LUMP_COUNT, SaveBuffer, EP2STATS_LUMP_GENERIC, 1, buf.TellPut(), buf.Base() ); + } + } + + static void LoadData( CUtlDict<Ep2LevelStats_t, unsigned short>& items, CUtlBuffer &LoadBuffer ) + { + // Read the next lump. + unsigned short iLump = 0; + unsigned short iLumpCount = 0; + + Ep2LevelStats_t *pItem = NULL; + + while( CBaseGameStats::GetLumpHeader( EP2_MAX_LUMP_COUNT, LoadBuffer, iLump, iLumpCount, true ) ) + { + switch ( iLump ) + { + case EP2STATS_LUMP_HEADER: + { + Ep2LevelStats_t::LevelHeader_t header; + CBaseGameStats::LoadLump( LoadBuffer, iLumpCount, sizeof( Ep2LevelStats_t::LevelHeader_t ), &header ); + pItem = &items[ items.Insert( header.m_szMapName ) ]; + pItem->m_Header = header; + pItem->m_Tag.Clear(); + Assert( pItem ); + } + break; + case EP2STATS_LUMP_TAG: + { + Assert( pItem ); + CBaseGameStats::LoadLump( LoadBuffer, iLumpCount, sizeof( Ep2LevelStats_t::Tag_t ), &pItem->m_Tag ); + } + break; + case EP2STATS_LUMP_DEATH: + { + Assert( pItem ); + pItem->m_aPlayerDeaths.SetCount( iLumpCount ); + CBaseGameStats::LoadLump( LoadBuffer, iLumpCount, sizeof( Ep2LevelStats_t::PlayerDeathsLump_t ), static_cast<void*>( pItem->m_aPlayerDeaths.Base() ) ); + } + break; + case EP2STATS_LUMP_NPC: + { + Assert( pItem ); + LoadBuffer.Get( ( void * )pItem->m_IntCounters, sizeof( pItem->m_IntCounters ) ); + LoadBuffer.Get( ( void * )pItem->m_FloatCounters, sizeof( pItem->m_FloatCounters ) ); + int c = LoadBuffer.GetInt(); + for ( int i = 0 ; i < c; ++i ) + { + Ep2LevelStats_t::EntityDeathsLump_t data; + char npcName[ 512 ]; + LoadBuffer.GetString( npcName ); + LoadBuffer.Get( &data, sizeof( data ) ); + pItem->m_dictEntityDeaths.Insert( npcName, data ); + } + } + break; + case EP2STATS_LUMP_WEAPON: + { + Assert( pItem ); + int c = LoadBuffer.GetInt(); + for ( int i = 0 ; i < c; ++i ) + { + Ep2LevelStats_t::WeaponLump_t data; + char weaponName[ 512 ]; + LoadBuffer.GetString( weaponName ); + LoadBuffer.Get( &data, sizeof( data ) ); + pItem->m_dictWeapons.Insert( weaponName, data ); + } + } + break; + case EP2STATS_LUMP_SAVEGAMEINFO: + { + Assert( pItem ); + Ep2LevelStats_t::SaveGameInfo_t *info = &pItem->m_SaveGameInfo; + char sz[ 512 ]; + LoadBuffer.GetString( sz ); + info->m_sCurrentSaveFile = sz; + info->m_nCurrentSaveFileTime = LoadBuffer.GetInt(); + int c = LoadBuffer.GetInt(); + for ( int i = 0 ; i < c; ++i ) + { + Ep2LevelStats_t::SaveGameInfoRecord2_t rec; + if ( pItem->m_Header.m_iVersion >= EP2_GAMESTATS_FILE_VERSION_02 ) + { + LoadBuffer.Get( &rec, sizeof( rec ) ); + } + else + { + size_t s = sizeof( Ep2LevelStats_t::SaveGameInfoRecord_t ); + LoadBuffer.Get( &rec, s ); + } + info->m_Records.AddToTail( rec ); + } + info->m_pCurrentRecord = NULL; + if ( info->m_Records.Count() > 0 ) + { + info->m_pCurrentRecord = &info->m_Records[ info->m_Records.Count() - 1 ]; + } + } + break; + case EP2STATS_LUMP_GENERIC: + { + Assert( pItem ); + int version = LoadBuffer.GetShort(); + if ( version == Ep2LevelStats_t::GenericStatsLump_t::LumpVersion ) + { + int c = LoadBuffer.GetInt(); + Assert( c < 2 * 1024 * 1024 ); + for ( int i = 0 ; i < c; ++i ) + { + Ep2LevelStats_t::GenericStatsLump_t data; + char pchStatName[ 512 ]; + LoadBuffer.GetString( pchStatName ); + LoadBuffer.Get( &data, sizeof( data ) ); + pItem->m_dictGeneric.Insert( pchStatName, data ); + } + } + else + { + Error( "Unsupported GenericStatsLump_t::LumpVersion" ); + } + } + break; + } + } + } + +public: + // Level header data. + struct LevelHeader_t + { + static const unsigned short LumpId = EP2STATS_LUMP_HEADER; // Lump ids. + byte m_iVersion; // Version of the game stats file. + char m_szMapName[64]; // Name of the map. + float m_flTime; // Time spent in level. + }; + + // Simple "tag" applied to all data in database (e.g., "PLAYTEST") + struct Tag_t + { + static const unsigned short LumpId = EP2STATS_LUMP_TAG; + + void Clear() + { + Q_memset( m_szTagText, 0, sizeof( m_szTagText ) ); + m_nMapVersion = 0; + } + + char m_szTagText[ 8 ]; + int m_nMapVersion; + }; + + // Player deaths. + struct PlayerDeathsLump_t + { + static const unsigned short LumpId = EP2STATS_LUMP_DEATH; // Lump ids. + short nPosition[3]; // Position of death. +// short iWeapon; // Weapon that killed the player. +// byte iAttackClass; // Class that killed the player. +// byte iTargetClass; // Class of the player killed. + }; + + struct EntityDeathsLump_t + { + static const unsigned short LumpId = EP2STATS_LUMP_NPC; + + EntityDeathsLump_t() : + m_nBodyCount( 0u ), + m_nKilledPlayer( 0u ) + { + } + + EntityDeathsLump_t( const EntityDeathsLump_t &other ) + { + m_nBodyCount = other.m_nBodyCount; + m_nKilledPlayer = other.m_nKilledPlayer; + } + + unsigned int m_nBodyCount; // Number killed by player + unsigned int m_nKilledPlayer; // Number of times entity killed player + }; + + struct WeaponLump_t + { + static const unsigned short LumpId = EP2STATS_LUMP_WEAPON; + + WeaponLump_t() : + m_nShots( 0 ), + m_nHits( 0 ), + m_flDamageInflicted( 0.0 ) + { + } + + WeaponLump_t( const WeaponLump_t &other ) + { + m_nShots = other.m_nShots; + m_nHits = other.m_nHits; + m_flDamageInflicted = other.m_flDamageInflicted; + } + + unsigned int m_nShots; + unsigned int m_nHits; + double m_flDamageInflicted; + }; + + struct SaveGameInfoRecord_t + { + SaveGameInfoRecord_t() : + m_nFirstDeathIndex( -1 ), + m_nNumDeaths( 0 ), + m_nSaveHealth( -1 ) + { + Q_memset( m_nSavePos, 0, sizeof( m_nSavePos ) ); + } + + int m_nFirstDeathIndex; + int m_nNumDeaths; + // Health and player pos from the save file + short m_nSavePos[ 3 ]; + short m_nSaveHealth; + }; + +#pragma pack( 1 ) + // Adds save game type + struct SaveGameInfoRecord2_t : public SaveGameInfoRecord_t + { + enum SaveType_t + { + TYPE_UNKNOWN = 0, + TYPE_AUTOSAVE, + TYPE_USERSAVE + }; + + SaveGameInfoRecord2_t() : + m_SaveType( (byte)TYPE_UNKNOWN ) + { + } + + byte m_SaveType; + }; +#pragma pack() + + struct SaveGameInfo_t + { + static const unsigned short LumpId = EP2STATS_LUMP_SAVEGAMEINFO; + + SaveGameInfo_t() : + m_nCurrentSaveFileTime( 0 ), + m_pCurrentRecord( NULL ) + { + } + + void Latch( char const *pchSaveName, unsigned int uFileTime ) + { + m_pCurrentRecord = &m_Records[ m_Records.AddToTail() ]; + m_nCurrentSaveFileTime = uFileTime; + m_sCurrentSaveFile = pchSaveName; + } + + CUtlVector< SaveGameInfoRecord2_t > m_Records; + SaveGameInfoRecord2_t *m_pCurrentRecord; + unsigned int m_nCurrentSaveFileTime; + CUtlString m_sCurrentSaveFile; + }; + + struct GenericStatsLump_t + { + static const unsigned short LumpId = EP2STATS_LUMP_GENERIC; + static const unsigned short LumpVersion = 1; + + GenericStatsLump_t() : + m_unCount( 0u ), + m_flCurrentValue( 0.0 ) + { + m_Pos[ 0 ] = m_Pos[ 1 ] = m_Pos[ 2 ] = 0; + } + + short m_Pos[ 3 ]; + unsigned int m_unCount; + double m_flCurrentValue; + }; + + // Data. + LevelHeader_t m_Header; // Level header. + Tag_t m_Tag; + CUtlVector<PlayerDeathsLump_t> m_aPlayerDeaths; // List of player deaths. + CUtlDict< EntityDeathsLump_t, int > m_dictEntityDeaths; + CUtlDict< WeaponLump_t, int > m_dictWeapons; + CUtlDict< GenericStatsLump_t, int > m_dictGeneric; + + SaveGameInfo_t m_SaveGameInfo; + float m_FloatCounters[ NUM_FLOATCOUNTER_TYPES ]; + uint64 m_IntCounters[ NUM_INTCOUNTER_TYPES ]; + + // Temporary data. + bool m_bInitialized; // Has the map Map Stat Data been initialized. + float m_flLevelStartTime; +}; + +#if defined( GAME_DLL ) +class CEP2GameStats : public CEP1GameStats +{ + typedef CEP1GameStats BaseClass; + +public: + CEP2GameStats(); + virtual ~CEP2GameStats(); + + virtual CBaseGameStats *OnInit( CBaseGameStats *pCurrentGameStats, char const *gamedir ) { return pCurrentGameStats; } + + virtual bool UserPlayedAllTheMaps( void ); + virtual const char *GetStatSaveFileName( void ); + virtual const char *GetStatUploadRegistryKeyName( void ); + + // Buffers. + virtual void AppendCustomDataToSaveBuffer( CUtlBuffer &SaveBuffer ); + virtual void LoadCustomDataFromBuffer( CUtlBuffer &LoadBuffer ); + + // Events + virtual void Event_LevelInit( void ); + virtual void Event_PlayerKilled( CBasePlayer *pPlayer, const CTakeDamageInfo &info ); + virtual void Event_PlayerDamage( CBasePlayer *pBasePlayer, const CTakeDamageInfo &info ); + virtual void Event_PlayerKilledOther( CBasePlayer *pAttacker, CBaseEntity *pVictim, const CTakeDamageInfo &info ); + virtual void Event_CrateSmashed(); + virtual void Event_Punted( CBaseEntity *pObject ); + virtual void Event_PlayerTraveled( CBasePlayer *pBasePlayer, float distanceInInches, bool bInVehicle, bool bSprinting ); + virtual void Event_WeaponFired( CBasePlayer *pShooter, bool bPrimary, char const *pchWeaponName ); + virtual void Event_WeaponHit( CBasePlayer *pShooter, bool bPrimary, char const *pchWeaponName, const CTakeDamageInfo &info ); + virtual void Event_SaveGame( void ); + virtual void Event_LoadGame( void ); + virtual void Event_FlippedVehicle( CBasePlayer *pDriver, CPropVehicleDriveable *pVehicle ); + // Called before .sav file is actually loaded (player should still be in previous level, if any) + virtual void Event_PreSaveGameLoaded( char const *pSaveName, bool bInGame ); + virtual void Event_PlayerEnteredGodMode( CBasePlayer *pBasePlayer ); + virtual void Event_PlayerEnteredNoClip( CBasePlayer *pBasePlayer ); + virtual void Event_DecrementPlayerEnteredNoClip( CBasePlayer *pBasePlayer ); + // Generic statistics lump + virtual void Event_IncrementCountedStatistic( const Vector& vecAbsOrigin, char const *pchStatisticName, float flIncrementAmount ); + +public: //FIXME: temporary used for CC_ListDeaths command + Ep2LevelStats_t *FindOrAddMapStats( const char *szMapName ); + +public: + + Ep2LevelStats_t::EntityDeathsLump_t *FindDeathsLump( char const *npcName ); + Ep2LevelStats_t::WeaponLump_t *FindWeaponsLump( char const *pchWeaponName, bool bPrimary ); + Ep2LevelStats_t::GenericStatsLump_t *FindGenericLump( char const *pchStatName ); + // Utilities. + Ep2LevelStats_t *GetCurrentMap( void ) { return m_pCurrentMap; } + + Ep2LevelStats_t *m_pCurrentMap; + CUtlDict<Ep2LevelStats_t, unsigned short> m_dictMapStats; + enum + { + INVEHICLE = 0, + ONFOOT, + ONFOOTSPRINTING, + + NUM_TRAVEL_TYPES + }; + float m_flInchesRemainder[ NUM_TRAVEL_TYPES ]; +}; +#endif + +#endif // EP2_GAMESTATS_H diff --git a/game/server/episodic/grenade_hopwire.cpp b/game/server/episodic/grenade_hopwire.cpp new file mode 100644 index 0000000..7010c4b --- /dev/null +++ b/game/server/episodic/grenade_hopwire.cpp @@ -0,0 +1,581 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Gravity well device +// +//=====================================================================================// + +#include "cbase.h" +#include "grenade_hopwire.h" +#include "rope.h" +#include "rope_shared.h" +#include "beam_shared.h" +#include "physics.h" +#include "physics_saverestore.h" +#include "explode.h" +#include "physics_prop_ragdoll.h" +#include "movevars_shared.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +ConVar hopwire_vortex( "hopwire_vortex", "0" ); +ConVar hopwire_trap( "hopwire_trap", "1" ); +ConVar hopwire_strider_kill_dist_h( "hopwire_strider_kill_dist_h", "300" ); +ConVar hopwire_strider_kill_dist_v( "hopwire_strider_kill_dist_v", "256" ); +ConVar hopwire_strider_hits( "hopwire_strider_hits", "1" ); +ConVar hopwire_hopheight( "hopwire_hopheight", "400" ); + +ConVar g_debug_hopwire( "g_debug_hopwire", "0" ); + +#define DENSE_BALL_MODEL "models/props_junk/metal_paintcan001b.mdl" + +#define MAX_HOP_HEIGHT (hopwire_hopheight.GetFloat()) // Maximum amount the grenade will "hop" upwards when detonated + +class CGravityVortexController : public CBaseEntity +{ + DECLARE_CLASS( CGravityVortexController, CBaseEntity ); + DECLARE_DATADESC(); + +public: + + CGravityVortexController( void ) : m_flEndTime( 0.0f ), m_flRadius( 256 ), m_flStrength( 256 ), m_flMass( 0.0f ) {} + float GetConsumedMass( void ) const; + + static CGravityVortexController *Create( const Vector &origin, float radius, float strength, float duration ); + +private: + + void ConsumeEntity( CBaseEntity *pEnt ); + void PullPlayersInRange( void ); + bool KillNPCInRange( CBaseEntity *pVictim, IPhysicsObject **pPhysObj ); + void CreateDenseBall( void ); + void PullThink( void ); + void StartPull( const Vector &origin, float radius, float strength, float duration ); + + float m_flMass; // Mass consumed by the vortex + float m_flEndTime; // Time when the vortex will stop functioning + float m_flRadius; // Area of effect for the vortex + float m_flStrength; // Pulling strength of the vortex +}; + +//----------------------------------------------------------------------------- +// Purpose: Returns the amount of mass consumed by the vortex +//----------------------------------------------------------------------------- +float CGravityVortexController::GetConsumedMass( void ) const +{ + return m_flMass; +} + +//----------------------------------------------------------------------------- +// Purpose: Adds the entity's mass to the aggregate mass consumed +//----------------------------------------------------------------------------- +void CGravityVortexController::ConsumeEntity( CBaseEntity *pEnt ) +{ + // Get our base physics object + IPhysicsObject *pPhysObject = pEnt->VPhysicsGetObject(); + if ( pPhysObject == NULL ) + return; + + // Ragdolls need to report the sum of all their parts + CRagdollProp *pRagdoll = dynamic_cast< CRagdollProp* >( pEnt ); + if ( pRagdoll != NULL ) + { + // Find the aggregate mass of the whole ragdoll + ragdoll_t *pRagdollPhys = pRagdoll->GetRagdoll(); + for ( int j = 0; j < pRagdollPhys->listCount; ++j ) + { + m_flMass += pRagdollPhys->list[j].pObject->GetMass(); + } + } + else + { + // Otherwise we just take the normal mass + m_flMass += pPhysObject->GetMass(); + } + + // Destroy the entity + UTIL_Remove( pEnt ); +} + +//----------------------------------------------------------------------------- +// Purpose: Causes players within the radius to be sucked in +//----------------------------------------------------------------------------- +void CGravityVortexController::PullPlayersInRange( void ) +{ + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + + Vector vecForce = GetAbsOrigin() - pPlayer->WorldSpaceCenter(); + float dist = VectorNormalize( vecForce ); + + // FIXME: Need a more deterministic method here + if ( dist < 128.0f ) + { + // Kill the player (with falling death sound and effects) + CTakeDamageInfo deathInfo( this, this, GetAbsOrigin(), GetAbsOrigin(), 200, DMG_FALL ); + pPlayer->TakeDamage( deathInfo ); + + if ( pPlayer->IsAlive() == false ) + { + color32 black = { 0, 0, 0, 255 }; + UTIL_ScreenFade( pPlayer, black, 0.1f, 0.0f, (FFADE_OUT|FFADE_STAYOUT) ); + return; + } + } + + // Must be within the radius + if ( dist > m_flRadius ) + return; + + float mass = pPlayer->VPhysicsGetObject()->GetMass(); + float playerForce = m_flStrength * 0.05f; + + // Find the pull force + // NOTE: We might want to make this non-linear to give more of a "grace distance" + vecForce *= ( 1.0f - ( dist / m_flRadius ) ) * playerForce * mass; + vecForce[2] *= 0.025f; + + pPlayer->SetBaseVelocity( vecForce ); + pPlayer->AddFlag( FL_BASEVELOCITY ); + + // Make sure the player moves + if ( vecForce.z > 0 && ( pPlayer->GetFlags() & FL_ONGROUND) ) + { + pPlayer->SetGroundEntity( NULL ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Attempts to kill an NPC if it's within range and other criteria +// Input : *pVictim - NPC to assess +// **pPhysObj - pointer to the ragdoll created if the NPC is killed +// Output : bool - whether or not the NPC was killed and the returned pointer is valid +//----------------------------------------------------------------------------- +bool CGravityVortexController::KillNPCInRange( CBaseEntity *pVictim, IPhysicsObject **pPhysObj ) +{ + CBaseCombatCharacter *pBCC = pVictim->MyCombatCharacterPointer(); + + // See if we can ragdoll + if ( pBCC != NULL && pBCC->CanBecomeRagdoll() ) + { + // Don't bother with striders + if ( FClassnameIs( pBCC, "npc_strider" ) ) + return false; + + // TODO: Make this an interaction between the NPC and the vortex + + // Become ragdoll + CTakeDamageInfo info( this, this, 1.0f, DMG_GENERIC ); + CBaseEntity *pRagdoll = CreateServerRagdoll( pBCC, 0, info, COLLISION_GROUP_INTERACTIVE_DEBRIS, true ); + pRagdoll->SetCollisionBounds( pVictim->CollisionProp()->OBBMins(), pVictim->CollisionProp()->OBBMaxs() ); + + // Necessary to cause it to do the appropriate death cleanup + CTakeDamageInfo ragdollInfo( this, this, 10000.0, DMG_GENERIC | DMG_REMOVENORAGDOLL ); + pVictim->TakeDamage( ragdollInfo ); + + // Return the pointer to the ragdoll + *pPhysObj = pRagdoll->VPhysicsGetObject(); + return true; + } + + // Wasn't able to ragdoll this target + *pPhysObj = NULL; + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Creates a dense ball with a mass equal to the aggregate mass consumed by the vortex +//----------------------------------------------------------------------------- +void CGravityVortexController::CreateDenseBall( void ) +{ + CBaseEntity *pBall = CreateEntityByName( "prop_physics" ); + + pBall->SetModel( DENSE_BALL_MODEL ); + pBall->SetAbsOrigin( GetAbsOrigin() ); + pBall->Spawn(); + + IPhysicsObject *pObj = pBall->VPhysicsGetObject(); + if ( pObj != NULL ) + { + pObj->SetMass( GetConsumedMass() ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Pulls physical objects towards the vortex center, killing them if they come too near +//----------------------------------------------------------------------------- +void CGravityVortexController::PullThink( void ) +{ + // Pull any players close enough to us + PullPlayersInRange(); + + Vector mins, maxs; + mins = GetAbsOrigin() - Vector( m_flRadius, m_flRadius, m_flRadius ); + maxs = GetAbsOrigin() + Vector( m_flRadius, m_flRadius, m_flRadius ); + + // Draw debug information + if ( g_debug_hopwire.GetBool() ) + { + NDebugOverlay::Box( GetAbsOrigin(), mins - GetAbsOrigin(), maxs - GetAbsOrigin(), 0, 255, 0, 16, 4.0f ); + } + + CBaseEntity *pEnts[128]; + int numEnts = UTIL_EntitiesInBox( pEnts, 128, mins, maxs, 0 ); + + for ( int i = 0; i < numEnts; i++ ) + { + IPhysicsObject *pPhysObject = NULL; + + // Attempt to kill and ragdoll any victims in range + if ( KillNPCInRange( pEnts[i], &pPhysObject ) == false ) + { + // If we didn't have a valid victim, see if we can just get the vphysics object + pPhysObject = pEnts[i]->VPhysicsGetObject(); + if ( pPhysObject == NULL ) + continue; + } + + float mass; + + CRagdollProp *pRagdoll = dynamic_cast< CRagdollProp* >( pEnts[i] ); + if ( pRagdoll != NULL ) + { + ragdoll_t *pRagdollPhys = pRagdoll->GetRagdoll(); + mass = 0.0f; + + // Find the aggregate mass of the whole ragdoll + for ( int j = 0; j < pRagdollPhys->listCount; ++j ) + { + mass += pRagdollPhys->list[j].pObject->GetMass(); + } + } + else + { + mass = pPhysObject->GetMass(); + } + + Vector vecForce = GetAbsOrigin() - pEnts[i]->WorldSpaceCenter(); + Vector vecForce2D = vecForce; + vecForce2D[2] = 0.0f; + float dist2D = VectorNormalize( vecForce2D ); + float dist = VectorNormalize( vecForce ); + + // FIXME: Need a more deterministic method here + if ( dist < 48.0f ) + { + ConsumeEntity( pEnts[i] ); + continue; + } + + // Must be within the radius + if ( dist > m_flRadius ) + continue; + + // Find the pull force + vecForce *= ( 1.0f - ( dist2D / m_flRadius ) ) * m_flStrength * mass; + + if ( pEnts[i]->VPhysicsGetObject() ) + { + // Pull the object in + pEnts[i]->VPhysicsTakeDamage( CTakeDamageInfo( this, this, vecForce, GetAbsOrigin(), m_flStrength, DMG_BLAST ) ); + } + } + + // Keep going if need-be + if ( m_flEndTime > gpGlobals->curtime ) + { + SetThink( &CGravityVortexController::PullThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } + else + { + //Msg( "Consumed %.2f kilograms\n", m_flMass ); + //CreateDenseBall(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Starts the vortex working +//----------------------------------------------------------------------------- +void CGravityVortexController::StartPull( const Vector &origin, float radius, float strength, float duration ) +{ + SetAbsOrigin( origin ); + m_flEndTime = gpGlobals->curtime + duration; + m_flRadius = radius; + m_flStrength= strength; + + SetThink( &CGravityVortexController::PullThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + +//----------------------------------------------------------------------------- +// Purpose: Creation utility +//----------------------------------------------------------------------------- +CGravityVortexController *CGravityVortexController::Create( const Vector &origin, float radius, float strength, float duration ) +{ + // Create an instance of the vortex + CGravityVortexController *pVortex = (CGravityVortexController *) CreateEntityByName( "vortex_controller" ); + if ( pVortex == NULL ) + return NULL; + + // Start the vortex working + pVortex->StartPull( origin, radius, strength, duration ); + + return pVortex; +} + +BEGIN_DATADESC( CGravityVortexController ) + DEFINE_FIELD( m_flMass, FIELD_FLOAT ), + DEFINE_FIELD( m_flEndTime, FIELD_TIME ), + DEFINE_FIELD( m_flRadius, FIELD_FLOAT ), + DEFINE_FIELD( m_flStrength, FIELD_FLOAT ), + + DEFINE_THINKFUNC( PullThink ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( vortex_controller, CGravityVortexController ); + +#define GRENADE_MODEL_CLOSED "models/roller.mdl" +#define GRENADE_MODEL_OPEN "models/roller_spikes.mdl" + +BEGIN_DATADESC( CGrenadeHopwire ) + DEFINE_FIELD( m_hVortexController, FIELD_EHANDLE ), + + DEFINE_THINKFUNC( EndThink ), + DEFINE_THINKFUNC( CombatThink ), +END_DATADESC() + +LINK_ENTITY_TO_CLASS( npc_grenade_hopwire, CGrenadeHopwire ); + +IMPLEMENT_SERVERCLASS_ST( CGrenadeHopwire, DT_GrenadeHopwire ) +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGrenadeHopwire::Spawn( void ) +{ + Precache(); + + SetModel( GRENADE_MODEL_CLOSED ); + SetCollisionGroup( COLLISION_GROUP_PROJECTILE ); + + CreateVPhysics(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CGrenadeHopwire::CreateVPhysics() +{ + // Create the object in the physics system + VPhysicsInitNormal( SOLID_BBOX, 0, false ); + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGrenadeHopwire::Precache( void ) +{ + // FIXME: Replace + //PrecacheSound("NPC_Strider.Shoot"); + //PrecacheSound("d3_citadel.weapon_zapper_beam_loop2"); + + PrecacheModel( GRENADE_MODEL_OPEN ); + PrecacheModel( GRENADE_MODEL_CLOSED ); + + PrecacheModel( DENSE_BALL_MODEL ); + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : timer - +//----------------------------------------------------------------------------- +void CGrenadeHopwire::SetTimer( float timer ) +{ + SetThink( &CBaseGrenade::PreDetonate ); + SetNextThink( gpGlobals->curtime + timer ); +} + +#define MAX_STRIDER_KILL_DISTANCE_HORZ (hopwire_strider_kill_dist_h.GetFloat()) // Distance a Strider will be killed if within +#define MAX_STRIDER_KILL_DISTANCE_VERT (hopwire_strider_kill_dist_v.GetFloat()) // Distance a Strider will be killed if within + +#define MAX_STRIDER_STUN_DISTANCE_HORZ (MAX_STRIDER_KILL_DISTANCE_HORZ*2) // Distance a Strider will be stunned if within +#define MAX_STRIDER_STUN_DISTANCE_VERT (MAX_STRIDER_KILL_DISTANCE_VERT*2) // Distance a Strider will be stunned if within + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGrenadeHopwire::KillStriders( void ) +{ + CBaseEntity *pEnts[128]; + Vector mins, maxs; + + ClearBounds( mins, maxs ); + AddPointToBounds( -Vector( MAX_STRIDER_STUN_DISTANCE_HORZ, MAX_STRIDER_STUN_DISTANCE_HORZ, MAX_STRIDER_STUN_DISTANCE_HORZ ), mins, maxs ); + AddPointToBounds( Vector( MAX_STRIDER_STUN_DISTANCE_HORZ, MAX_STRIDER_STUN_DISTANCE_HORZ, MAX_STRIDER_STUN_DISTANCE_HORZ ), mins, maxs ); + AddPointToBounds( -Vector( MAX_STRIDER_STUN_DISTANCE_VERT, MAX_STRIDER_STUN_DISTANCE_VERT, MAX_STRIDER_STUN_DISTANCE_VERT ), mins, maxs ); + AddPointToBounds( Vector( MAX_STRIDER_STUN_DISTANCE_VERT, MAX_STRIDER_STUN_DISTANCE_VERT, MAX_STRIDER_STUN_DISTANCE_VERT ), mins, maxs ); + + // FIXME: It's probably much faster to simply iterate over the striders in the map, rather than any entity in the radius - jdw + + // Find any striders in range of us + int numTargets = UTIL_EntitiesInBox( pEnts, ARRAYSIZE( pEnts ), GetAbsOrigin()+mins, GetAbsOrigin()+maxs, FL_NPC ); + float targetDistHorz, targetDistVert; + + for ( int i = 0; i < numTargets; i++ ) + { + // Only affect striders + if ( FClassnameIs( pEnts[i], "npc_strider" ) == false ) + continue; + + // We categorize our spatial relation to the strider in horizontal and vertical terms, so that we can specify both parameters separately + targetDistHorz = UTIL_DistApprox2D( pEnts[i]->GetAbsOrigin(), GetAbsOrigin() ); + targetDistVert = fabs( pEnts[i]->GetAbsOrigin()[2] - GetAbsOrigin()[2] ); + + if ( targetDistHorz < MAX_STRIDER_KILL_DISTANCE_HORZ && targetDistHorz < MAX_STRIDER_KILL_DISTANCE_VERT ) + { + // Kill the strider + float fracDamage = ( pEnts[i]->GetMaxHealth() / hopwire_strider_hits.GetFloat() ) + 1.0f; + CTakeDamageInfo killInfo( this, this, fracDamage, DMG_GENERIC ); + Vector killDir = pEnts[i]->GetAbsOrigin() - GetAbsOrigin(); + VectorNormalize( killDir ); + + killInfo.SetDamageForce( killDir * -1000.0f ); + killInfo.SetDamagePosition( GetAbsOrigin() ); + + pEnts[i]->TakeDamage( killInfo ); + } + else if ( targetDistHorz < MAX_STRIDER_STUN_DISTANCE_HORZ && targetDistHorz < MAX_STRIDER_STUN_DISTANCE_VERT ) + { + // Stun the strider + CTakeDamageInfo killInfo( this, this, 200.0f, DMG_GENERIC ); + pEnts[i]->TakeDamage( killInfo ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGrenadeHopwire::EndThink( void ) +{ + if ( hopwire_vortex.GetBool() ) + { + EntityMessageBegin( this, true ); + WRITE_BYTE( 1 ); + MessageEnd(); + } + + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 1.0f ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGrenadeHopwire::CombatThink( void ) +{ + // Stop the grenade from moving + AddEFlags( EF_NODRAW ); + AddFlag( FSOLID_NOT_SOLID ); + VPhysicsDestroyObject(); + SetAbsVelocity( vec3_origin ); + SetMoveType( MOVETYPE_NONE ); + + // Do special behaviors if there are any striders in the area + KillStriders(); + + // FIXME: Replace + //EmitSound("NPC_Strider.Shoot"); + //EmitSound("d3_citadel.weapon_zapper_beam_loop2"); + + // Quick screen flash + CBasePlayer *pPlayer = ToBasePlayer( GetThrower() ); + color32 white = { 255,255,255,255 }; + UTIL_ScreenFade( pPlayer, white, 0.2f, 0.0f, FFADE_IN ); + + // Create the vortex controller to pull entities towards us + if ( hopwire_vortex.GetBool() ) + { + m_hVortexController = CGravityVortexController::Create( GetAbsOrigin(), 512, 150, 3.0f ); + + // Start our client-side effect + EntityMessageBegin( this, true ); + WRITE_BYTE( 0 ); + MessageEnd(); + + // Begin to stop in two seconds + SetThink( &CGrenadeHopwire::EndThink ); + SetNextThink( gpGlobals->curtime + 2.0f ); + } + else + { + // Remove us immediately + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CGrenadeHopwire::SetVelocity( const Vector &velocity, const AngularImpulse &angVelocity ) +{ + IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); + + if ( pPhysicsObject != NULL ) + { + pPhysicsObject->AddVelocity( &velocity, &angVelocity ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Hop off the ground to start deployment +//----------------------------------------------------------------------------- +void CGrenadeHopwire::Detonate( void ) +{ + SetModel( GRENADE_MODEL_OPEN ); + + AngularImpulse hopAngle = RandomAngularImpulse( -300, 300 ); + + //Find out how tall the ceiling is and always try to hop halfway + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, MAX_HOP_HEIGHT*2 ), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + + // Jump half the height to the found ceiling + float hopHeight = MIN( MAX_HOP_HEIGHT, (MAX_HOP_HEIGHT*tr.fraction) ); + + //Add upwards velocity for the "hop" + Vector hopVel( 0.0f, 0.0f, hopHeight ); + SetVelocity( hopVel, hopAngle ); + + // Get the time until the apex of the hop + float apexTime = sqrt( hopHeight / GetCurrentGravity() ); + + // Explode at the apex + SetThink( &CGrenadeHopwire::CombatThink ); + SetNextThink( gpGlobals->curtime + apexTime); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CBaseGrenade *HopWire_Create( const Vector &position, const QAngle &angles, const Vector &velocity, const AngularImpulse &angVelocity, CBaseEntity *pOwner, float timer ) +{ + CGrenadeHopwire *pGrenade = (CGrenadeHopwire *) CBaseEntity::Create( "npc_grenade_hopwire", position, angles, pOwner ); + + // Only set ourselves to detonate on a timer if we're not a trap hopwire + if ( hopwire_trap.GetBool() == false ) + { + pGrenade->SetTimer( timer ); + } + + pGrenade->SetVelocity( velocity, angVelocity ); + pGrenade->SetThrower( ToBaseCombatCharacter( pOwner ) ); + + return pGrenade; +} diff --git a/game/server/episodic/grenade_hopwire.h b/game/server/episodic/grenade_hopwire.h new file mode 100644 index 0000000..c8cd71e --- /dev/null +++ b/game/server/episodic/grenade_hopwire.h @@ -0,0 +1,46 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef GRENADE_HOPWIRE_H +#define GRENADE_HOPWIRE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "basegrenade_shared.h" +#include "Sprite.h" + +extern ConVar hopwire_trap; + +class CGravityVortexController; + +class CGrenadeHopwire : public CBaseGrenade +{ + DECLARE_CLASS( CGrenadeHopwire, CBaseGrenade ); + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + +public: + void Spawn( void ); + void Precache( void ); + bool CreateVPhysics( void ); + void SetTimer( float timer ); + void SetVelocity( const Vector &velocity, const AngularImpulse &angVelocity ); + void Detonate( void ); + + void EndThink( void ); // Last think before going away + void CombatThink( void ); // Makes the main explosion go off + +protected: + + void KillStriders( void ); + + CHandle<CGravityVortexController> m_hVortexController; +}; + +extern CBaseGrenade *HopWire_Create( const Vector &position, const QAngle &angles, const Vector &velocity, const AngularImpulse &angVelocity, CBaseEntity *pOwner, float timer ); + +#endif // GRENADE_HOPWIRE_H 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 diff --git a/game/server/episodic/npc_combine_cannon.cpp b/game/server/episodic/npc_combine_cannon.cpp new file mode 100644 index 0000000..18ae490 --- /dev/null +++ b/game/server/episodic/npc_combine_cannon.cpp @@ -0,0 +1,1335 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "ai_basenpc.h" +#include "ammodef.h" +#include "ai_memory.h" +#include "weapon_rpg.h" +#include "effect_color_tables.h" +#include "te_effect_dispatch.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern const char* g_pModelNameLaser; + +// No model, impervious to damage. +#define SF_STARTDISABLED (1 << 19) + +#define CANNON_PAINT_ENEMY_TIME 1.0f +#define CANNON_SUBSEQUENT_PAINT_TIME 0.4f +#define CANNON_PAINT_NPC_TIME_NOISE 1.0f + +#define NUM_ANCILLARY_BEAMS 4 + +int gHaloTexture = 0; + +//----------------------------------------------------------------------------- +// +// Combine Cannon +// +//----------------------------------------------------------------------------- +class CNPC_Combine_Cannon : public CAI_BaseNPC +{ + DECLARE_CLASS( CNPC_Combine_Cannon, CAI_BaseNPC ); + +public: + CNPC_Combine_Cannon( void ); + virtual void Precache( void ); + virtual void Spawn( void ); + virtual Class_T Classify( void ); + virtual float MaxYawSpeed( void ); + virtual Vector EyePosition( void ); + virtual void UpdateOnRemove( void ); + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + virtual bool QuerySeeEntity( CBaseEntity *pEntity, bool bOnlyHateOrFearIfNPC = false ); + virtual void StartTask( const Task_t *pTask ); + virtual void RunTask( const Task_t *pTask ); + virtual int RangeAttack1Conditions( float flDot, float flDist ); + virtual int SelectSchedule( void ); + virtual int TranslateSchedule( int scheduleType ); + virtual void PrescheduleThink( void ); + virtual bool FCanCheckAttacks ( void ); + virtual int Restore( IRestore &restore ); + virtual void OnScheduleChange( void ); + virtual bool FVisible( CBaseEntity *pEntity, int traceMask = MASK_BLOCKLOS, CBaseEntity **ppBlocker = NULL ); + + virtual bool WeaponLOSCondition( const Vector &ownerPos, const Vector &targetPos, bool bSetConditions) { return true; } + + virtual int GetSoundInterests( void ) { return (SOUND_PLAYER|SOUND_COMBAT|SOUND_DANGER); } + + virtual bool ShouldNotDistanceCull( void ) { return true; } + + virtual void UpdateEfficiency( bool bInPVS ) { SetEfficiency( ( GetSleepState() != AISS_AWAKE ) ? AIE_DORMANT : AIE_NORMAL ); SetMoveEfficiency( AIME_NORMAL ); } + + virtual const char *GetTracerType( void ) { return "HelicopterTracer"; } + +private: + + void ScopeGlint( void ); + void AdjustShotPosition( CBaseEntity *pTarget, Vector *vecIn ); + + float GetRefireTime( void ) { return 0.1f; } + + bool IsLaserOn( void ) { return m_pBeam != NULL; } + bool FireBullet( const Vector &vecTarget, bool bDirectShot ); + Vector DesiredBodyTarget( CBaseEntity *pTarget ); + Vector LeadTarget( CBaseEntity *pTarget ); + Vector GetBulletOrigin( void ); + + static const char *pAttackSounds[]; + + void ClearTargetGroup( void ); + + float GetWaitTimePercentage( float flTime, bool fLinear ); + + void GetPaintAim( const Vector &vecStart, const Vector &vecGoal, float flParameter, Vector *pProgress ); + + bool VerifyShot( CBaseEntity *pTarget ); + + void SetSweepTarget( const char *pszTarget ); + + // Inputs + void InputEnableSniper( inputdata_t &inputdata ); + void InputDisableSniper( inputdata_t &inputdata ); + + void LaserOff( void ); + void LaserOn( const Vector &vecTarget, const Vector &vecDeviance ); + + void PaintTarget( const Vector &vecTarget, float flPaintTime ); + +private: + + void CreateLaser( void ); + void CreateAncillaryBeams( void ); + void UpdateAncillaryBeams( float flConvergencePerc, const Vector &vecOrigin, const Vector &vecBasis ); + + int m_iAmmoType; + float m_flBarrageDuration; + Vector m_vecPaintCursor; + float m_flPaintTime; + + CHandle<CBeam> m_pBeam; + CHandle<CBeam> m_pAncillaryBeams[NUM_ANCILLARY_BEAMS]; + EHANDLE m_hBarrageTarget; + + bool m_fEnabled; + Vector m_vecPaintStart; // used to track where a sweep starts for the purpose of interpolating. + float m_flTimeLastAttackedPlayer; + float m_flTimeLastShotMissed; + float m_flSightDist; + + DEFINE_CUSTOM_AI; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( npc_combine_cannon, CNPC_Combine_Cannon ); + +//========================================================= +//========================================================= +BEGIN_DATADESC( CNPC_Combine_Cannon ) + + DEFINE_FIELD( m_fEnabled, FIELD_BOOLEAN ), + DEFINE_FIELD( m_vecPaintStart, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_flPaintTime, FIELD_TIME ), + DEFINE_FIELD( m_vecPaintCursor, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_pBeam, FIELD_EHANDLE ), + DEFINE_FIELD( m_flTimeLastAttackedPlayer, FIELD_TIME ), + DEFINE_FIELD( m_flTimeLastShotMissed, FIELD_TIME ), + DEFINE_FIELD( m_iAmmoType, FIELD_INTEGER ), + DEFINE_FIELD( m_flBarrageDuration, FIELD_TIME ), + DEFINE_FIELD( m_hBarrageTarget, FIELD_EHANDLE ), + + DEFINE_ARRAY( m_pAncillaryBeams, FIELD_EHANDLE, NUM_ANCILLARY_BEAMS ), + + DEFINE_KEYFIELD( m_flSightDist, FIELD_FLOAT, "sightdist" ), + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "EnableSniper", InputEnableSniper ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableSniper", InputDisableSniper ), + +END_DATADESC() + + +//========================================================= +// Private conditions +//========================================================= +enum Sniper_Conds +{ + COND_CANNON_ENABLED = LAST_SHARED_CONDITION, + COND_CANNON_DISABLED, + COND_CANNON_NO_SHOT, +}; + + +//========================================================= +// schedules +//========================================================= +enum +{ + SCHED_CANNON_CAMP = LAST_SHARED_SCHEDULE, + SCHED_CANNON_ATTACK, + SCHED_CANNON_DISABLEDWAIT, + SCHED_CANNON_SNAPATTACK, +}; + +//========================================================= +// tasks +//========================================================= +enum +{ + TASK_CANNON_PAINT_ENEMY = LAST_SHARED_TASK, + TASK_CANNON_PAINT_DECOY, + TASK_CANNON_ATTACK_CURSOR, +}; + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CNPC_Combine_Cannon::CNPC_Combine_Cannon( void ) : + m_pBeam( NULL ), + m_hBarrageTarget( NULL ) +{ +#ifdef _DEBUG + m_vecPaintCursor.Init(); + m_vecPaintStart.Init(); +#endif +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Combine_Cannon::QuerySeeEntity( CBaseEntity *pEntity, bool bOnlyHateOrFearIfNPC ) +{ + Disposition_t disp = IRelationType(pEntity); + if ( disp != D_HT ) + { + // Don't bother with anything I wouldn't shoot. + return false; + } + + if ( !FInViewCone(pEntity) ) + { + // Yes, this does call FInViewCone twice a frame for all entities checked for + // visibility, but doing this allows us to cut out a bunch of traces that would + // be done by VerifyShot for entities that aren't even in our viewcone. + return false; + } + + if ( VerifyShot( pEntity ) ) + return BaseClass::QuerySeeEntity( pEntity, bOnlyHateOrFearIfNPC ); + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Hide the beams +//----------------------------------------------------------------------------- +void CNPC_Combine_Cannon::LaserOff( void ) +{ + if ( m_pBeam != NULL ) + { + m_pBeam->TurnOn(); + } + + for ( int i = 0; i < NUM_ANCILLARY_BEAMS; i++ ) + { + if ( m_pAncillaryBeams[i] == NULL ) + continue; + + m_pAncillaryBeams[i]->TurnOn(); + } + + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Switch on the laser and point it at a direction +//----------------------------------------------------------------------------- +void CNPC_Combine_Cannon::LaserOn( const Vector &vecTarget, const Vector &vecDeviance ) +{ + if ( m_pBeam != NULL ) + { + m_pBeam->TurnOff(); + + // Don't aim right at the guy right now. + Vector vecInitialAim; + + if( vecDeviance == vec3_origin ) + { + // Start the aim where it last left off! + vecInitialAim = m_vecPaintCursor; + } + else + { + vecInitialAim = vecTarget; + } + + vecInitialAim.x += random->RandomFloat( -vecDeviance.x, vecDeviance.x ); + vecInitialAim.y += random->RandomFloat( -vecDeviance.y, vecDeviance.y ); + vecInitialAim.z += random->RandomFloat( -vecDeviance.z, vecDeviance.z ); + + m_pBeam->SetStartPos( GetBulletOrigin() ); + m_pBeam->SetEndPos( vecInitialAim ); + + m_vecPaintStart = vecInitialAim; + } + + for ( int i = 0; i < NUM_ANCILLARY_BEAMS; i++ ) + { + if ( m_pAncillaryBeams[i] == NULL ) + continue; + + m_pAncillaryBeams[i]->TurnOff(); + } +} + +//----------------------------------------------------------------------------- +// Crikey! +//----------------------------------------------------------------------------- +float CNPC_Combine_Cannon::GetWaitTimePercentage( float flTime, bool fLinear ) +{ + float flElapsedTime; + float flTimeParameter; + + flElapsedTime = flTime - (GetWaitFinishTime() - gpGlobals->curtime); + + flTimeParameter = ( flElapsedTime / flTime ); + + if( fLinear ) + { + return flTimeParameter; + } + else + { + return (1 + sin( (M_PI * flTimeParameter) - (M_PI / 2) ) ) / 2; + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Combine_Cannon::GetPaintAim( const Vector &vecStart, const Vector &vecGoal, float flParameter, Vector *pProgress ) +{ + // Quaternions + Vector vecIdealDir; + QAngle vecIdealAngles; + QAngle vecCurrentAngles; + Vector vecCurrentDir; + Vector vecBulletOrigin = GetBulletOrigin(); + + // vecIdealDir is where the gun should be aimed when the painting + // time is up. This can be approximate. This is only for drawing the + // laser, not actually aiming the weapon. A large discrepancy will look + // bad, though. + vecIdealDir = vecGoal - vecBulletOrigin; + VectorNormalize(vecIdealDir); + + // Now turn vecIdealDir into angles! + VectorAngles( vecIdealDir, vecIdealAngles ); + + // This is the vector of the beam's current aim. + vecCurrentDir = m_vecPaintStart - vecBulletOrigin; + VectorNormalize(vecCurrentDir); + + // Turn this to angles, too. + VectorAngles( vecCurrentDir, vecCurrentAngles ); + + Quaternion idealQuat; + Quaternion currentQuat; + Quaternion aimQuat; + + AngleQuaternion( vecIdealAngles, idealQuat ); + AngleQuaternion( vecCurrentAngles, currentQuat ); + + QuaternionSlerp( currentQuat, idealQuat, flParameter, aimQuat ); + + QuaternionAngles( aimQuat, vecCurrentAngles ); + + // Rebuild the current aim vector. + AngleVectors( vecCurrentAngles, &vecCurrentDir ); + + *pProgress = vecCurrentDir; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Combine_Cannon::CreateLaser( void ) +{ + if ( m_pBeam != NULL ) + return; + + m_pBeam = CBeam::BeamCreate( g_pModelNameLaser, 2.0f ); + + m_pBeam->SetColor( 0, 100, 255 ); + + m_pBeam->PointsInit( vec3_origin, GetBulletOrigin() ); + m_pBeam->SetBrightness( 255 ); + m_pBeam->SetNoise( 0 ); + m_pBeam->SetWidth( 1.0f ); + m_pBeam->SetEndWidth( 0 ); + m_pBeam->SetScrollRate( 0 ); + m_pBeam->SetFadeLength( 0 ); + m_pBeam->SetHaloTexture( gHaloTexture ); + m_pBeam->SetHaloScale( 16.0f ); + + // Think faster while painting + SetNextThink( gpGlobals->curtime + 0.02f ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Combine_Cannon::CreateAncillaryBeams( void ) +{ + for ( int i = 0; i < NUM_ANCILLARY_BEAMS; i++ ) + { + if ( m_pAncillaryBeams[i] != NULL ) + continue; + + m_pAncillaryBeams[i] = CBeam::BeamCreate( g_pModelNameLaser, 2.0f ); + m_pAncillaryBeams[i]->SetColor( 0, 100, 255 ); + + m_pAncillaryBeams[i]->PointsInit( vec3_origin, GetBulletOrigin() ); + m_pAncillaryBeams[i]->SetBrightness( 255 ); + m_pAncillaryBeams[i]->SetNoise( 0 ); + m_pAncillaryBeams[i]->SetWidth( 1.0f ); + m_pAncillaryBeams[i]->SetEndWidth( 0 ); + m_pAncillaryBeams[i]->SetScrollRate( 0 ); + m_pAncillaryBeams[i]->SetFadeLength( 0 ); + m_pAncillaryBeams[i]->SetHaloTexture( gHaloTexture ); + m_pAncillaryBeams[i]->SetHaloScale( 16.0f ); + m_pAncillaryBeams[i]->TurnOff(); + } +} + +#define LINE_LENGTH 1600.0f + +//----------------------------------------------------------------------------- +// Purpose: +// Input : flConvergencePerc - +// vecBasis - +//----------------------------------------------------------------------------- +void CNPC_Combine_Cannon::UpdateAncillaryBeams( float flConvergencePerc, const Vector &vecOrigin, const Vector &vecBasis ) +{ + // Multiple beams deviate from the basis direction by a certain number of degrees and "converge" + // at the basis vector over a duration of time, the position in that duration expressed by + // flConvergencePerc. The beams are most deviated at 0 and fully converged at 1. + + float flRotationOffset = (2*M_PI)/(float)NUM_ANCILLARY_BEAMS; // Degrees separating each beam, in radians + float flDeviation = DEG2RAD(90) * ( 1.0f - flConvergencePerc ); + float flOffset; + Vector vecFinal; + Vector vecOffset; + + matrix3x4_t matRotate; + QAngle vecAngles; + VectorAngles( vecBasis, vecAngles ); + vecAngles[PITCH] += 90.0f; + AngleMatrix( vecAngles, vecOrigin, matRotate ); + + trace_t tr; + + float flScale = LINE_LENGTH * flDeviation; + + // For each beam, find its offset and trace outwards to place its endpoint + for ( int i = 0; i < NUM_ANCILLARY_BEAMS; i++ ) + { + if ( flConvergencePerc >= 0.99f ) + { + m_pAncillaryBeams[i]->TurnOn(); + continue; + } + + m_pAncillaryBeams[i]->TurnOff(); + + // Find the number of radians offset we are + flOffset = (float) i * flRotationOffset + DEG2RAD( 30.0f ); + flOffset += (M_PI/8.0f) * sin( gpGlobals->curtime * 3.0f ); + + // Construct a circle that's also offset by the line's length + vecOffset.x = cos( flOffset ) * flScale; + vecOffset.y = sin( flOffset ) * flScale; + vecOffset.z = LINE_LENGTH; + + // Rotate this whole thing into the space of the basis vector + VectorRotate( vecOffset, matRotate, vecFinal ); + VectorNormalize( vecFinal ); + + // Trace a line down that vector to find where we'll eventually stop our line + UTIL_TraceLine( vecOrigin, vecOrigin + ( vecFinal * LINE_LENGTH ), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + // Move the beam to that position + m_pAncillaryBeams[i]->SetBrightness( 255.0f * flConvergencePerc ); + m_pAncillaryBeams[i]->SetEndPos( tr.startpos ); + m_pAncillaryBeams[i]->SetStartPos( tr.endpos ); + } +} + +//----------------------------------------------------------------------------- +// Sweep the laser sight towards the point where the gun should be aimed +//----------------------------------------------------------------------------- +void CNPC_Combine_Cannon::PaintTarget( const Vector &vecTarget, float flPaintTime ) +{ + // vecStart is the barrel of the gun (or the laser sight) + Vector vecStart = GetBulletOrigin(); + + // keep painttime from hitting 0 exactly. + flPaintTime = MAX( flPaintTime, 0.000001f ); + + // Find out where we are in the arc of the paint duration + float flPaintPerc = GetWaitTimePercentage( flPaintTime, false ); + + ScopeGlint(); + + // Find out where along our line we're painting + Vector vecCurrentDir; + float flInterp = RemapValClamped( flPaintPerc, 0.0f, 0.5f, 0.0f, 1.0f ); + flInterp = clamp( flInterp, 0.0f, 1.0f ); + GetPaintAim( m_vecPaintStart, vecTarget, flInterp, &vecCurrentDir ); + +#define THRESHOLD 0.9f + float flNoiseScale; + + if ( flPaintPerc >= THRESHOLD ) + { + flNoiseScale = 1 - (1 / (1 - THRESHOLD)) * ( flPaintPerc - THRESHOLD ); + } + else if ( flPaintPerc <= 1 - THRESHOLD ) + { + flNoiseScale = flPaintPerc / (1 - THRESHOLD); + } + else + { + flNoiseScale = 1; + } + + // mult by P + vecCurrentDir.x += flNoiseScale * ( sin( 3 * M_PI * gpGlobals->curtime ) * 0.0006 ); + vecCurrentDir.y += flNoiseScale * ( sin( 2 * M_PI * gpGlobals->curtime + 0.5 * M_PI ) * 0.0006 ); + vecCurrentDir.z += flNoiseScale * ( sin( 1.5 * M_PI * gpGlobals->curtime + M_PI ) * 0.0006 ); + + // Find where our center is + trace_t tr; + UTIL_TraceLine( vecStart, vecStart + vecCurrentDir * 8192, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + m_vecPaintCursor = tr.endpos; + + // Update our beam position + m_pBeam->SetEndPos( tr.startpos ); + m_pBeam->SetStartPos( tr.endpos ); + m_pBeam->SetBrightness( 255.0f * flPaintPerc ); + m_pBeam->RelinkBeam(); + + // Find points around that center point and make our designators converge at that point over time + UpdateAncillaryBeams( flPaintPerc, vecStart, vecCurrentDir ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Combine_Cannon::OnScheduleChange( void ) +{ + LaserOff(); + + m_hBarrageTarget = NULL; + + BaseClass::OnScheduleChange(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Combine_Cannon::Precache( void ) +{ + PrecacheModel("models/combine_soldier.mdl"); + PrecacheModel("effects/bluelaser1.vmt"); + + gHaloTexture = PrecacheModel("sprites/light_glow03.vmt"); + + PrecacheScriptSound( "NPC_Combine_Cannon.FireBullet" ); + + BaseClass::Precache(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Combine_Cannon::Spawn( void ) +{ + Precache(); + + /// HACK: + SetModel( "models/combine_soldier.mdl" ); + + // Setup our ancillary beams but keep them hidden for now + CreateLaser(); + CreateAncillaryBeams(); + + m_iAmmoType = GetAmmoDef()->Index( "CombineHeavyCannon" ); + + SetHullType( HULL_HUMAN ); + SetHullSizeNormal(); + + UTIL_SetSize( this, Vector( -16, -16 , 0 ), Vector( 16, 16, 64 ) ); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_FLY ); + m_bloodColor = DONT_BLEED; + m_iHealth = 10; + m_flFieldOfView = DOT_45DEGREE; + m_NPCState = NPC_STATE_NONE; + + if( HasSpawnFlags( SF_STARTDISABLED ) ) + { + m_fEnabled = false; + } + else + { + m_fEnabled = true; + } + + CapabilitiesClear(); + CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_SIMPLE_RADIUS_DAMAGE ); + + m_HackedGunPos = Vector ( 0, 0, 0 ); + + AddSpawnFlags( SF_NPC_LONG_RANGE | SF_NPC_ALWAYSTHINK ); + + NPCInit(); + + // Limit our look distance + SetDistLook( m_flSightDist ); + + AddEffects( EF_NODRAW ); + AddSolidFlags( FSOLID_NOT_SOLID ); + + // Point the cursor straight ahead so that the sniper's + // first sweep of the laser doesn't look weird. + Vector vecForward; + AngleVectors( GetLocalAngles(), &vecForward ); + m_vecPaintCursor = GetBulletOrigin() + vecForward * 1024; + + // none! + GetEnemies()->SetFreeKnowledgeDuration( 0.0f ); + GetEnemies()->SetEnemyDiscardTime( 2.0f ); + + m_flTimeLastAttackedPlayer = 0.0f; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +Class_T CNPC_Combine_Cannon::Classify( void ) +{ + if ( m_fEnabled ) + return CLASS_COMBINE; + + return CLASS_NONE; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +Vector CNPC_Combine_Cannon::GetBulletOrigin( void ) +{ + return GetAbsOrigin(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Nothing kills the cannon but entity I/O +//----------------------------------------------------------------------------- +int CNPC_Combine_Cannon::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + // We are invulnerable to normal attacks for the moment + return 0; +} + +//--------------------------------------------------------- +// Purpose: +//--------------------------------------------------------- +void CNPC_Combine_Cannon::UpdateOnRemove( void ) +{ + // Remove the main laser + if ( m_pBeam != NULL ) + { + UTIL_Remove( m_pBeam); + m_pBeam = NULL; + } + + // Remove our ancillary beams + for ( int i = 0; i < NUM_ANCILLARY_BEAMS; i++ ) + { + if ( m_pAncillaryBeams[i] == NULL ) + continue; + + UTIL_Remove( m_pAncillaryBeams[i] ); + m_pAncillaryBeams[i] = NULL; + } + + BaseClass::UpdateOnRemove(); +} + +//--------------------------------------------------------- +// Purpose: +//--------------------------------------------------------- +int CNPC_Combine_Cannon::SelectSchedule ( void ) +{ + // Fire at our target + if( GetEnemy() && HasCondition( COND_CAN_RANGE_ATTACK1 ) ) + return SCHED_RANGE_ATTACK1; + + // Wait for a target + // TODO: Sweep like a sniper? + return SCHED_COMBAT_STAND; +} + +//--------------------------------------------------------- +// Purpose: +//--------------------------------------------------------- +bool CNPC_Combine_Cannon::FCanCheckAttacks ( void ) +{ + return true; +} + + +//--------------------------------------------------------- +//--------------------------------------------------------- +bool CNPC_Combine_Cannon::VerifyShot( CBaseEntity *pTarget ) +{ + trace_t tr; + + Vector vecTarget = DesiredBodyTarget( pTarget ); + UTIL_TraceLine( GetBulletOrigin(), vecTarget, MASK_SHOT, pTarget, COLLISION_GROUP_NONE, &tr ); + + if( tr.fraction != 1.0 ) + { + if( pTarget->IsPlayer() ) + { + // if the target is the player, do another trace to see if we can shoot his eyeposition. This should help + // improve sniper responsiveness in cases where the player is hiding his chest from the sniper with his + // head in full view. + UTIL_TraceLine( GetBulletOrigin(), pTarget->EyePosition(), MASK_SHOT, pTarget, COLLISION_GROUP_NONE, &tr ); + + if( tr.fraction == 1.0 ) + { + return true; + } + } + + // Trace hit something. + if( tr.m_pEnt ) + { + if( tr.m_pEnt->m_takedamage == DAMAGE_YES ) + { + // Just shoot it if I can hurt it. Probably a breakable or glass pane. + return true; + } + } + + return false; + } + else + { + return true; + } +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +int CNPC_Combine_Cannon::RangeAttack1Conditions( float flDot, float flDist ) +{ + if ( GetNextAttack() > gpGlobals->curtime ) + return COND_NONE; + + if ( HasCondition( COND_SEE_ENEMY ) && !HasCondition( COND_ENEMY_OCCLUDED ) ) + { + if ( VerifyShot( GetEnemy() ) ) + { + // Can see the enemy, have a clear shot to his midsection + ClearCondition( COND_CANNON_NO_SHOT ); + return COND_CAN_RANGE_ATTACK1; + } + else + { + // Can see the enemy, but can't take a shot at his midsection + SetCondition( COND_CANNON_NO_SHOT ); + return COND_NONE; + } + } + + return COND_NONE; +} + + +//--------------------------------------------------------- +//--------------------------------------------------------- +int CNPC_Combine_Cannon::TranslateSchedule( int scheduleType ) +{ + switch( scheduleType ) + { + case SCHED_RANGE_ATTACK1: + return SCHED_CANNON_ATTACK; + break; + } + return BaseClass::TranslateSchedule( scheduleType ); +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +void CNPC_Combine_Cannon::ScopeGlint( void ) +{ + CEffectData data; + + data.m_vOrigin = GetAbsOrigin(); + data.m_vNormal = vec3_origin; + data.m_vAngles = vec3_angle; + data.m_nColor = COMMAND_POINT_BLUE; + + DispatchEffect( "CommandPointer", data ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *vecIn - +//----------------------------------------------------------------------------- +void CNPC_Combine_Cannon::AdjustShotPosition( CBaseEntity *pTarget, Vector *vecIn ) +{ + if ( pTarget == NULL || vecIn == NULL ) + return; + + Vector low = pTarget->WorldSpaceCenter() - ( pTarget->WorldSpaceCenter() - pTarget->GetAbsOrigin() ) * .25; + Vector high = pTarget->EyePosition(); + Vector delta = high - low; + Vector result = low + delta * 0.5; + + // Only take the height + (*vecIn)[2] = result[2]; +} + +//--------------------------------------------------------- +// This starts the bullet state machine. The actual effects +// of the bullet will happen later. This function schedules +// those effects. +// +// fDirectShot indicates whether the bullet is a "direct shot" +// that is - fired with the intent that it will strike the +// enemy. Otherwise, the bullet is intended to strike a +// decoy object or nothing at all in particular. +//--------------------------------------------------------- +bool CNPC_Combine_Cannon::FireBullet( const Vector &vecTarget, bool bDirectShot ) +{ + Vector vecBulletOrigin = GetBulletOrigin(); + Vector vecDir = ( vecTarget - vecBulletOrigin ); + VectorNormalize( vecDir ); + + FireBulletsInfo_t info; + info.m_iShots = 1; + info.m_iTracerFreq = 1.0f; + info.m_vecDirShooting = vecDir; + info.m_vecSrc = vecBulletOrigin; + info.m_flDistance = MAX_TRACE_LENGTH; + info.m_pAttacker = this; + info.m_iAmmoType = m_iAmmoType; + info.m_iPlayerDamage = 20.0f; + info.m_vecSpread = Vector( 0.015f, 0.015f, 0.015f ); // medium cone + + FireBullets( info ); + + EmitSound( "NPC_Combine_Cannon.FireBullet" ); + + // Don't attack for a certain amount of time + SetNextAttack( gpGlobals->curtime + GetRefireTime() ); + + // Sniper had to be aiming here to fire here, so make it the cursor + m_vecPaintCursor = vecTarget; + + LaserOff(); + + return true; +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +void CNPC_Combine_Cannon::StartTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_CANNON_ATTACK_CURSOR: + break; + + case TASK_RANGE_ATTACK1: + // Setup the information for this barrage + m_flBarrageDuration = gpGlobals->curtime + random->RandomFloat( 0.25f, 0.5f ); + m_hBarrageTarget = GetEnemy(); + break; + + case TASK_CANNON_PAINT_ENEMY: + { + if ( GetEnemy()->IsPlayer() ) + { + float delay = random->RandomFloat( 0.0f, 0.3f ); + + if ( ( gpGlobals->curtime - m_flTimeLastAttackedPlayer ) < 1.0f ) + { + SetWait( CANNON_SUBSEQUENT_PAINT_TIME ); + m_flPaintTime = CANNON_SUBSEQUENT_PAINT_TIME; + } + else + { + SetWait( CANNON_PAINT_ENEMY_TIME + delay ); + m_flPaintTime = CANNON_PAINT_ENEMY_TIME + delay; + } + } + else + { + // Use a random time + m_flPaintTime = CANNON_PAINT_ENEMY_TIME + random->RandomFloat( 0, CANNON_PAINT_NPC_TIME_NOISE ); + SetWait( m_flPaintTime ); + } + + // Try to start the laser where the player can't miss seeing it! + Vector vecCursor; + AngleVectors( GetEnemy()->GetLocalAngles(), &vecCursor ); + vecCursor *= 300; + vecCursor += GetEnemy()->EyePosition(); + LaserOn( vecCursor, Vector( 16, 16, 16 ) ); + } + break; + + default: + BaseClass::StartTask( pTask ); + break; + } +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +void CNPC_Combine_Cannon::RunTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_CANNON_ATTACK_CURSOR: + if( FireBullet( m_vecPaintCursor, true ) ) + { + TaskComplete(); + } + break; + + case TASK_RANGE_ATTACK1: + { + // Where we're focusing our fire + Vector vecTarget = ( m_hBarrageTarget == NULL ) ? m_vecPaintCursor : LeadTarget( m_hBarrageTarget ); + + // Fire at enemy + if ( FireBullet( vecTarget, true ) ) + { + bool bPlayerIsEnemy = ( m_hBarrageTarget && m_hBarrageTarget->IsPlayer() ); + bool bBarrageFinished = m_flBarrageDuration < gpGlobals->curtime; + bool bNoShot = ( QuerySeeEntity( m_hBarrageTarget ) == false ); // FIXME: Store this info off better + bool bSeePlayer = HasCondition( COND_SEE_PLAYER ); + + // Treat the player differently to normal NPCs + if ( bPlayerIsEnemy ) + { + // Store the last time we shot for doing an abbreviated attack telegraph + m_flTimeLastAttackedPlayer = gpGlobals->curtime; + + // If we've got no shot and we're done with our current barrage + if ( bNoShot && bBarrageFinished ) + { + TaskComplete(); + } + } + else if ( bBarrageFinished || bSeePlayer ) + { + // Done with the barrage or we saw the player as a better target + TaskComplete(); + } + } + } + break; + + case TASK_CANNON_PAINT_ENEMY: + { + // See if we're done painting our target + if ( IsWaitFinished() ) + { + TaskComplete(); + } + + // Continue to paint the target + PaintTarget( LeadTarget( GetEnemy() ), m_flPaintTime ); + } + break; + + default: + BaseClass::RunTask( pTask ); + break; + } +} + + +//----------------------------------------------------------------------------- +// The sniper throws away the circular list of old decoys when we restore. +//----------------------------------------------------------------------------- +int CNPC_Combine_Cannon::Restore( IRestore &restore ) +{ + return BaseClass::Restore( restore ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// +// +//----------------------------------------------------------------------------- +float CNPC_Combine_Cannon::MaxYawSpeed( void ) +{ + return 60; +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +void CNPC_Combine_Cannon::PrescheduleThink( void ) +{ + BaseClass::PrescheduleThink(); + + // NOTE: We'll deal with this on the client + // Think faster if the beam is on, this gives the beam higher resolution. + if( m_pBeam ) + { + SetNextThink( gpGlobals->curtime + 0.03 ); + } + else + { + SetNextThink( gpGlobals->curtime + 0.1f ); + } + + // If the enemy has just stepped into view, or we've acquired a new enemy, + // Record the last time we've seen the enemy as right now. + // + // If the enemy has been out of sight for a full second, mark him eluded. + if( GetEnemy() != NULL ) + { + if( gpGlobals->curtime - GetEnemies()->LastTimeSeen( GetEnemy() ) > 30 ) + { + // Stop pestering enemies after 30 seconds of frustration. + GetEnemies()->ClearMemory( GetEnemy() ); + SetEnemy(NULL); + } + } +} + + +//--------------------------------------------------------- +//--------------------------------------------------------- +Vector CNPC_Combine_Cannon::EyePosition( void ) +{ + return GetAbsOrigin(); +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +Vector CNPC_Combine_Cannon::DesiredBodyTarget( CBaseEntity *pTarget ) +{ + // By default, aim for the center + Vector vecTarget = pTarget->WorldSpaceCenter(); + + float flTimeSinceLastMiss = gpGlobals->curtime - m_flTimeLastShotMissed; + + if( pTarget->GetFlags() & FL_CLIENT ) + { + if( !BaseClass::FVisible( vecTarget ) ) + { + // go to the player's eyes if his center is concealed. + // Bump up an inch so the player's not looking straight down a beam. + vecTarget = pTarget->EyePosition() + Vector( 0, 0, 1 ); + } + } + else + { + if( pTarget->Classify() == CLASS_HEADCRAB ) + { + // Headcrabs are tiny inside their boxes. + vecTarget = pTarget->GetAbsOrigin(); + vecTarget.z += 4.0; + } + else if( pTarget->Classify() == CLASS_ZOMBIE ) + { + if( flTimeSinceLastMiss > 0.0f && flTimeSinceLastMiss < 4.0f && hl2_episodic.GetBool() ) + { + vecTarget = pTarget->BodyTarget( GetBulletOrigin(), false ); + } + else + { + // Shoot zombies in the headcrab + vecTarget = pTarget->HeadTarget( GetBulletOrigin() ); + } + } + else if( pTarget->Classify() == CLASS_ANTLION ) + { + // Shoot about a few inches above the origin. This makes it easy to hit antlions + // even if they are on their backs. + vecTarget = pTarget->GetAbsOrigin(); + vecTarget.z += 18.0f; + } + else if( pTarget->Classify() == CLASS_EARTH_FAUNA ) + { + // Shoot birds in the center + } + else + { + // Shoot NPCs in the chest + vecTarget.z += 8.0f; + } + } + + return vecTarget; +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +Vector CNPC_Combine_Cannon::LeadTarget( CBaseEntity *pTarget ) +{ + if ( pTarget != NULL ) + { + Vector vecFuturePos; + UTIL_PredictedPosition( pTarget, 0.05f, &vecFuturePos ); + AdjustShotPosition( pTarget, &vecFuturePos ); + + return vecFuturePos; + } + + return vec3_origin; +} + +//--------------------------------------------------------- +//--------------------------------------------------------- +void CNPC_Combine_Cannon::InputEnableSniper( inputdata_t &inputdata ) +{ + ClearCondition( COND_CANNON_DISABLED ); + SetCondition( COND_CANNON_ENABLED ); + + m_fEnabled = true; +} + + +//--------------------------------------------------------- +//--------------------------------------------------------- +void CNPC_Combine_Cannon::InputDisableSniper( inputdata_t &inputdata ) +{ + ClearCondition( COND_CANNON_ENABLED ); + SetCondition( COND_CANNON_DISABLED ); + + m_fEnabled = false; +} + +//--------------------------------------------------------- +// See all NPC's easily. +// +// Only see the player if you can trace to both of his +// eyeballs. That is, allow the player to peek around corners. +// This is a little more expensive than the base class' check! +//--------------------------------------------------------- +#define CANNON_EYE_DIST 0.75 +#define CANNON_TARGET_VERTICAL_OFFSET Vector( 0, 0, 5 ); +bool CNPC_Combine_Cannon::FVisible( CBaseEntity *pEntity, int traceMask, CBaseEntity **ppBlocker ) +{ + // NPC + if ( pEntity->IsPlayer() == false ) + return BaseClass::FVisible( pEntity, traceMask, ppBlocker ); + + if ( pEntity->GetFlags() & FL_NOTARGET ) + return false; + + Vector vecVerticalOffset; + Vector vecRight; + Vector vecEye; + trace_t tr; + + if( fabs( GetAbsOrigin().z - pEntity->WorldSpaceCenter().z ) <= 120.f ) + { + // If the player is around the same elevation, look straight at his eyes. + // At the same elevation, the vertical peeking allowance makes it too easy + // for a player to dispatch the sniper from cover. + vecVerticalOffset = vec3_origin; + } + else + { + // Otherwise, look at a spot below his eyes. This allows the player to back away + // from his cover a bit and have a peek at the sniper without being detected. + vecVerticalOffset = CANNON_TARGET_VERTICAL_OFFSET; + } + + AngleVectors( pEntity->GetLocalAngles(), NULL, &vecRight, NULL ); + + vecEye = vecRight * CANNON_EYE_DIST - vecVerticalOffset; + UTIL_TraceLine( EyePosition(), pEntity->EyePosition() + vecEye, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + +#if 0 + NDebugOverlay::Line(EyePosition(), tr.endpos, 0,255,0, true, 0.1); +#endif + + bool fCheckFailed = false; + + if ( tr.fraction != 1.0 && tr.m_pEnt != pEntity ) + { + fCheckFailed = true; + } + + // Don't check the other eye if the first eye failed. + if( !fCheckFailed ) + { + vecEye = -vecRight * CANNON_EYE_DIST - vecVerticalOffset; + UTIL_TraceLine( EyePosition(), pEntity->EyePosition() + vecEye, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + +#if 0 + NDebugOverlay::Line(EyePosition(), tr.endpos, 0,255,0, true, 0.1); +#endif + + if ( tr.fraction != 1.0 && tr.m_pEnt != pEntity ) + { + fCheckFailed = true; + } + } + + if( !fCheckFailed ) + { + // Can see the player. + return true; + } + + // Now, if the check failed, see if the player is ducking and has recently + // fired a muzzleflash. If yes, see if you'd be able to see the player if + // they were standing in their current position instead of ducking. Since + // the sniper doesn't have a clear shot in this situation, he will harrass + // near the player. + CBasePlayer *pPlayer; + + pPlayer = ToBasePlayer( pEntity ); + + if( (pPlayer->GetFlags() & FL_DUCKING) && pPlayer->MuzzleFlashTime() > gpGlobals->curtime ) + { + vecEye = pPlayer->EyePosition() + Vector( 0, 0, 32 ); + UTIL_TraceLine( EyePosition(), vecEye, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + if( tr.fraction != 1.0 ) + { + // Everything failed. + if (ppBlocker) + { + *ppBlocker = tr.m_pEnt; + } + return false; + } + else + { + // Fake being able to see the player. + return true; + } + } + + if (ppBlocker) + { + *ppBlocker = tr.m_pEnt; + } + + return false; +} + + +//----------------------------------------------------------------------------- +// +// Schedules +// +//----------------------------------------------------------------------------- + +AI_BEGIN_CUSTOM_NPC( npc_combine_cannon, CNPC_Combine_Cannon ) + + DECLARE_CONDITION( COND_CANNON_ENABLED ); + DECLARE_CONDITION( COND_CANNON_DISABLED ); + DECLARE_CONDITION( COND_CANNON_NO_SHOT ); + + DECLARE_TASK( TASK_CANNON_PAINT_ENEMY ); + DECLARE_TASK( TASK_CANNON_ATTACK_CURSOR ); + + //========================================================= + // CAMP + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_CANNON_CAMP, + + " Tasks" + " TASK_WAIT 1" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_CAN_RANGE_ATTACK1" + " COND_HEAR_DANGER" + " COND_CANNON_DISABLED" + ) + + //========================================================= + // ATTACK + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_CANNON_ATTACK, + + " Tasks" + " TASK_CANNON_PAINT_ENEMY 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + " COND_HEAR_DANGER" + " COND_CANNON_DISABLED" + ) + + //========================================================= + // ATTACK + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_CANNON_SNAPATTACK, + + " Tasks" + " TASK_CANNON_ATTACK_CURSOR 0" + " " + " Interrupts" + " COND_ENEMY_OCCLUDED" + " COND_ENEMY_DEAD" + " COND_NEW_ENEMY" + " COND_HEAR_DANGER" + " COND_CANNON_DISABLED" + ) + + //========================================================= + // Sniper is allowed to process a couple conditions while + // disabled, but mostly he waits until he's enabled. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_CANNON_DISABLEDWAIT, + + " Tasks" + " TASK_WAIT 0.5" + " " + " Interrupts" + " COND_CANNON_ENABLED" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + ) + +AI_END_CUSTOM_NPC() diff --git a/game/server/episodic/npc_hunter.cpp b/game/server/episodic/npc_hunter.cpp new file mode 100644 index 0000000..d55053b --- /dev/null +++ b/game/server/episodic/npc_hunter.cpp @@ -0,0 +1,7764 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Small, fast version of the strider. Goes where striders cannot, such +// as into buildings. Best killed with physics objects and explosives. +// +//============================================================================= + +#include "cbase.h" +#include "npc_strider.h" +#include "npc_hunter.h" +#include "ai_behavior_follow.h" +#include "ai_moveprobe.h" +#include "ai_senses.h" +#include "ai_speech.h" +#include "ai_task.h" +#include "ai_default.h" +#include "ai_schedule.h" +#include "ai_hull.h" +#include "ai_baseactor.h" +#include "ai_waypoint.h" +#include "ai_link.h" +#include "ai_hint.h" +#include "ai_squadslot.h" +#include "ai_squad.h" +#include "ai_tacticalservices.h" +#include "beam_shared.h" +#include "datacache/imdlcache.h" +#include "eventqueue.h" +#include "gib.h" +#include "globalstate.h" +#include "hierarchy.h" +#include "movevars_shared.h" +#include "npcevent.h" +#include "saverestore_utlvector.h" +#include "particle_parse.h" +#include "te_particlesystem.h" +#include "sceneentity.h" +#include "shake.h" +#include "soundenvelope.h" +#include "soundent.h" +#include "SpriteTrail.h" +#include "IEffects.h" +#include "engine/IEngineSound.h" +#include "bone_setup.h" +#include "studio.h" +#include "ai_route.h" +#include "ammodef.h" +#include "npc_bullseye.h" +#include "physobj.h" +#include "ai_memory.h" +#include "collisionutils.h" +#include "shot_manipulator.h" +#include "steamjet.h" +#include "physics_prop_ragdoll.h" +#include "vehicle_base.h" +#include "coordsize.h" +#include "hl2_shareddefs.h" +#include "te_effect_dispatch.h" +#include "beam_flags.h" +#include "prop_combine_ball.h" +#include "explode.h" +#include "weapon_physcannon.h" +#include "weapon_striderbuster.h" +#include "monstermaker.h" +#include "weapon_rpg.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +class CNPC_Hunter; + + +static const char *HUNTER_FLECHETTE_MODEL = "models/weapons/hunter_flechette.mdl"; + +// Think contexts +static const char *HUNTER_BLEED_THINK = "HunterBleed"; +static const char *HUNTER_ZAP_THINK = "HunterZap"; +static const char *HUNTER_JOSTLE_VEHICLE_THINK = "HunterJostle"; + + +ConVar sk_hunter_health( "sk_hunter_health", "210" ); + +// Melee attacks +ConVar sk_hunter_dmg_one_slash( "sk_hunter_dmg_one_slash", "20" ); +ConVar sk_hunter_dmg_charge( "sk_hunter_dmg_charge", "20" ); + +// Flechette volley attack +ConVar hunter_flechette_max_range( "hunter_flechette_max_range", "1200" ); +ConVar hunter_flechette_min_range( "hunter_flechette_min_range", "100" ); +ConVar hunter_flechette_volley_size( "hunter_flechette_volley_size", "8" ); +ConVar hunter_flechette_speed( "hunter_flechette_speed", "2000" ); +ConVar sk_hunter_dmg_flechette( "sk_hunter_dmg_flechette", "4.0" ); +ConVar sk_hunter_flechette_explode_dmg( "sk_hunter_flechette_explode_dmg", "12.0" ); +ConVar sk_hunter_flechette_explode_radius( "sk_hunter_flechette_explode_radius", "128.0" ); +ConVar hunter_flechette_explode_delay( "hunter_flechette_explode_delay", "2.5" ); +ConVar hunter_flechette_delay( "hunter_flechette_delay", "0.1" ); +ConVar hunter_first_flechette_delay( "hunter_first_flechette_delay", "0.5" ); +ConVar hunter_flechette_max_concurrent_volleys( "hunter_flechette_max_concurrent_volleys", "2" ); +ConVar hunter_flechette_volley_start_min_delay( "hunter_flechette_volley_start_min_delay", ".25" ); +ConVar hunter_flechette_volley_start_max_delay( "hunter_flechette_volley_start_max_delay", ".95" ); +ConVar hunter_flechette_volley_end_min_delay( "hunter_flechette_volley_end_min_delay", "1" ); +ConVar hunter_flechette_volley_end_max_delay( "hunter_flechette_volley_end_max_delay", "2" ); +ConVar hunter_flechette_test( "hunter_flechette_test", "0" ); +ConVar hunter_clamp_shots( "hunter_clamp_shots", "1" ); +ConVar hunter_cheap_explosions( "hunter_cheap_explosions", "1" ); + +// Damage received +ConVar sk_hunter_bullet_damage_scale( "sk_hunter_bullet_damage_scale", "0.6" ); +ConVar sk_hunter_charge_damage_scale( "sk_hunter_charge_damage_scale", "2.0" ); +ConVar sk_hunter_buckshot_damage_scale( "sk_hunter_buckshot_damage_scale", "0.5" ); +ConVar sk_hunter_vehicle_damage_scale( "sk_hunter_vehicle_damage_scale", "2.2" ); +ConVar sk_hunter_dmg_from_striderbuster( "sk_hunter_dmg_from_striderbuster", "150" ); +ConVar sk_hunter_citizen_damage_scale( "sk_hunter_citizen_damage_scale", "0.3" ); + +ConVar hunter_allow_dissolve( "hunter_allow_dissolve", "1" ); +ConVar hunter_random_expressions( "hunter_random_expressions", "0" ); +ConVar hunter_show_weapon_los_z( "hunter_show_weapon_los_z", "0" ); +ConVar hunter_show_weapon_los_condition( "hunter_show_weapon_los_condition", "0" ); + +ConVar hunter_melee_delay( "hunter_melee_delay", "2.0" ); + +// Bullrush charge. +ConVar hunter_charge( "hunter_charge", "1" ); +ConVar hunter_charge_min_delay( "hunter_charge_min_delay", "10.0" ); +ConVar hunter_charge_pct( "hunter_charge_pct", "25" ); +ConVar hunter_charge_test( "hunter_charge_test", "0" ); + +// Vehicle dodging. +ConVar hunter_dodge_warning( "hunter_dodge_warning", "1.1" ); +ConVar hunter_dodge_warning_width( "hunter_dodge_warning_width", "180" ); +ConVar hunter_dodge_warning_cone( "hunter_dodge_warning_cone", ".5" ); +ConVar hunter_dodge_debug( "hunter_dodge_debug", "0" ); + +// Jostle vehicles when hit by them +ConVar hunter_jostle_car_min_speed( "hunter_jostle_car_min_speed", "100" ); // If hit by a car going at least this fast, jostle the car +ConVar hunter_jostle_car_max_speed( "hunter_jostle_car_max_speed", "600" ); // Used for determining jostle scale + +ConVar hunter_free_knowledge( "hunter_free_knowledge", "10.0" ); +ConVar hunter_plant_adjust_z( "hunter_plant_adjust_z", "12" ); + +ConVar hunter_disable_patrol( "hunter_disable_patrol", "0" ); + +// Dealing with striderbusters +ConVar hunter_hate_held_striderbusters( "hunter_hate_held_striderbusters", "1" ); +ConVar hunter_hate_thrown_striderbusters( "hunter_hate_thrown_striderbusters", "1" ); +ConVar hunter_hate_attached_striderbusters( "hunter_hate_attached_striderbusters", "1" ); +ConVar hunter_hate_held_striderbusters_delay( "hunter_hate_held_striderbusters_delay", "0.5" ); +ConVar hunter_hate_held_striderbusters_tolerance( "hunter_hate_held_striderbusters_tolerance", "2000.0" ); +ConVar hunter_hate_thrown_striderbusters_tolerance( "hunter_hate_thrown_striderbusters_tolerance", "300.0" ); +ConVar hunter_seek_thrown_striderbusters_tolerance( "hunter_seek_thrown_striderbusters_tolerance", "400.0" ); +ConVar hunter_retreat_striderbusters( "hunter_retreat_striderbusters", "1", FCVAR_NONE, "If true, the hunter will retreat when a buster is glued to him." ); + +ConVar hunter_allow_nav_jump( "hunter_allow_nav_jump", "0" ); +ConVar g_debug_hunter_charge( "g_debug_hunter_charge", "0" ); + +ConVar hunter_stand_still( "hunter_stand_still", "0" ); // used for debugging, keeps them rooted in place + +ConVar hunter_siege_frequency( "hunter_siege_frequency", "12" ); + +#define HUNTER_FOV_DOT 0.0 // 180 degree field of view +#define HUNTER_CHARGE_MIN 256 +#define HUNTER_CHARGE_MAX 1024 +#define HUNTER_FACE_ENEMY_DIST 512.0f +#define HUNTER_MELEE_REACH 80 +#define HUNTER_BLOOD_LEFT_FOOT 0 +#define HUNTER_IGNORE_ENEMY_TIME 5 // How long the hunter will ignore another enemy when distracted by the player. + +#define HUNTER_FACING_DOT 0.8 // The angle within which we start shooting +#define HUNTER_SHOOT_MAX_YAW_DEG 60.0f // Once shooting, clamp to +/- these degrees of yaw deflection as our target moves +#define HUNTER_SHOOT_MAX_YAW_COS 0.5f // The cosine of the above angle + +#define HUNTER_FLECHETTE_WARN_TIME 1.0f + +#define HUNTER_SEE_ENEMY_TIME_INVALID -1 + +#define NUM_FLECHETTE_VOLLEY_ON_FOLLOW 4 + +#define HUNTER_SIEGE_MAX_DIST_MODIFIER 2.0f + +//----------------------------------------------------------------------------- +// Animation events +//----------------------------------------------------------------------------- +int AE_HUNTER_FOOTSTEP_LEFT; +int AE_HUNTER_FOOTSTEP_RIGHT; +int AE_HUNTER_FOOTSTEP_BACK; +int AE_HUNTER_MELEE_ANNOUNCE; +int AE_HUNTER_MELEE_ATTACK_LEFT; +int AE_HUNTER_MELEE_ATTACK_RIGHT; +int AE_HUNTER_DIE; +int AE_HUNTER_SPRAY_BLOOD; +int AE_HUNTER_START_EXPRESSION; +int AE_HUNTER_END_EXPRESSION; + + +//----------------------------------------------------------------------------- +// Interactions. +//----------------------------------------------------------------------------- +int g_interactionHunterFoundEnemy = 0; + + +//----------------------------------------------------------------------------- +// Local stuff. +//----------------------------------------------------------------------------- +static string_t s_iszStriderClassname; +static string_t s_iszStriderBusterClassname; +static string_t s_iszMagnadeClassname; +static string_t s_iszPhysPropClassname; +static string_t s_iszHuntersToRunOver; + + +//----------------------------------------------------------------------------- +// Custom Activities +//----------------------------------------------------------------------------- +Activity ACT_HUNTER_DEPLOYRA2; +Activity ACT_HUNTER_DODGER; +Activity ACT_HUNTER_DODGEL; +Activity ACT_HUNTER_GESTURE_SHOOT; +Activity ACT_HUNTER_FLINCH_STICKYBOMB; +Activity ACT_HUNTER_STAGGER; +Activity ACT_HUNTER_MELEE_ATTACK1_VS_PLAYER; +Activity ACT_DI_HUNTER_MELEE; +Activity ACT_DI_HUNTER_THROW; +Activity ACT_HUNTER_ANGRY; +Activity ACT_HUNTER_WALK_ANGRY; +Activity ACT_HUNTER_FOUND_ENEMY; +Activity ACT_HUNTER_FOUND_ENEMY_ACK; +Activity ACT_HUNTER_CHARGE_START; +Activity ACT_HUNTER_CHARGE_RUN; +Activity ACT_HUNTER_CHARGE_STOP; +Activity ACT_HUNTER_CHARGE_CRASH; +Activity ACT_HUNTER_CHARGE_HIT; +Activity ACT_HUNTER_RANGE_ATTACK2_UNPLANTED; +Activity ACT_HUNTER_IDLE_PLANTED; +Activity ACT_HUNTER_FLINCH_N; +Activity ACT_HUNTER_FLINCH_S; +Activity ACT_HUNTER_FLINCH_E; +Activity ACT_HUNTER_FLINCH_W; + + +//----------------------------------------------------------------------------- +// Squad slots +//----------------------------------------------------------------------------- +enum SquadSlot_t +{ + SQUAD_SLOT_HUNTER_CHARGE = LAST_SHARED_SQUADSLOT, + SQUAD_SLOT_HUNTER_FLANK_FIRST, + SQUAD_SLOT_HUNTER_FLANK_LAST = SQUAD_SLOT_HUNTER_FLANK_FIRST, + SQUAD_SLOT_RUN_SHOOT, +}; + +#define HUNTER_FOLLOW_DISTANCE 2000.0f +#define HUNTER_FOLLOW_DISTANCE_SQR (HUNTER_FOLLOW_DISTANCE * HUNTER_FOLLOW_DISTANCE) + +#define HUNTER_RUNDOWN_SQUADDATA 0 + + +//----------------------------------------------------------------------------- +// We're doing this quite a lot, so this makes the check a lot faster since +// we don't have to compare strings. +//----------------------------------------------------------------------------- +bool IsStriderBuster( CBaseEntity *pEntity ) +{ + if ( !pEntity ) + return false; + + if( pEntity->m_iClassname == s_iszStriderBusterClassname || + pEntity->m_iClassname == s_iszMagnadeClassname) + return true; + + return false; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool HateThisStriderBuster( CBaseEntity *pTarget ) +{ + if ( StriderBuster_WasKnockedOffStrider(pTarget) ) + return false; + + if ( pTarget->VPhysicsGetObject() ) + { + if ( hunter_hate_held_striderbusters.GetBool() || + hunter_hate_thrown_striderbusters.GetBool() || + hunter_hate_attached_striderbusters.GetBool() ) + { + if ( ( pTarget->VPhysicsGetObject()->GetGameFlags() & ( FVPHYSICS_PLAYER_HELD | FVPHYSICS_WAS_THROWN ) ) ) + { + return true; + } + + if ( StriderBuster_IsAttachedStriderBuster( pTarget ) ) + { + return true; + } + } + } + + return false; +} + + +//----------------------------------------------------------------------------- +// The hunter can fire a volley of explosive flechettes. +//----------------------------------------------------------------------------- +static const char *s_szHunterFlechetteBubbles = "HunterFlechetteBubbles"; +static const char *s_szHunterFlechetteSeekThink = "HunterFlechetteSeekThink"; +static const char *s_szHunterFlechetteDangerSoundThink = "HunterFlechetteDangerSoundThink"; +static const char *s_szHunterFlechetteSpriteTrail = "sprites/bluelaser1.vmt"; +static int s_nHunterFlechetteImpact = -2; +static int s_nFlechetteFuseAttach = -1; + +#define FLECHETTE_AIR_VELOCITY 2500 + +class CHunterFlechette : public CPhysicsProp, public IParentPropInteraction +{ + DECLARE_CLASS( CHunterFlechette, CPhysicsProp ); + +public: + + CHunterFlechette(); + ~CHunterFlechette(); + + Class_T Classify() { return CLASS_NONE; } + + bool WasThrownBack() + { + return m_bThrownBack; + } + +public: + + void Spawn(); + void Activate(); + void Precache(); + void Shoot( Vector &vecVelocity, bool bBright ); + void SetSeekTarget( CBaseEntity *pTargetEntity ); + void Explode(); + + bool CreateVPhysics(); + + unsigned int PhysicsSolidMaskForEntity() const; + static CHunterFlechette *FlechetteCreate( const Vector &vecOrigin, const QAngle &angAngles, CBaseEntity *pentOwner = NULL ); + + // IParentPropInteraction + void OnParentCollisionInteraction( parentCollisionInteraction_t eType, int index, gamevcollisionevent_t *pEvent ); + void OnParentPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ); + +protected: + + void SetupGlobalModelData(); + + void StickTo( CBaseEntity *pOther, trace_t &tr ); + + void BubbleThink(); + void DangerSoundThink(); + void ExplodeThink(); + void DopplerThink(); + void SeekThink(); + + bool CreateSprites( bool bBright ); + + void FlechetteTouch( CBaseEntity *pOther ); + + Vector m_vecShootPosition; + EHANDLE m_hSeekTarget; + bool m_bThrownBack; + + DECLARE_DATADESC(); + //DECLARE_SERVERCLASS(); +}; + +LINK_ENTITY_TO_CLASS( hunter_flechette, CHunterFlechette ); + +BEGIN_DATADESC( CHunterFlechette ) + + DEFINE_THINKFUNC( BubbleThink ), + DEFINE_THINKFUNC( DangerSoundThink ), + DEFINE_THINKFUNC( ExplodeThink ), + DEFINE_THINKFUNC( DopplerThink ), + DEFINE_THINKFUNC( SeekThink ), + + DEFINE_ENTITYFUNC( FlechetteTouch ), + + DEFINE_FIELD( m_vecShootPosition, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_hSeekTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_bThrownBack, FIELD_BOOLEAN ), + +END_DATADESC() + +//IMPLEMENT_SERVERCLASS_ST( CHunterFlechette, DT_HunterFlechette ) +//END_SEND_TABLE() + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +CHunterFlechette *CHunterFlechette::FlechetteCreate( const Vector &vecOrigin, const QAngle &angAngles, CBaseEntity *pentOwner ) +{ + // Create a new entity with CHunterFlechette private data + CHunterFlechette *pFlechette = (CHunterFlechette *)CreateEntityByName( "hunter_flechette" ); + UTIL_SetOrigin( pFlechette, vecOrigin ); + pFlechette->SetAbsAngles( angAngles ); + pFlechette->Spawn(); + pFlechette->Activate(); + pFlechette->SetOwnerEntity( pentOwner ); + + return pFlechette; +} + + +//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +void CC_Hunter_Shoot_Flechette( const CCommand& args ) +{ + MDLCACHE_CRITICAL_SECTION(); + + bool allowPrecache = CBaseEntity::IsPrecacheAllowed(); + CBaseEntity::SetAllowPrecache( true ); + + CBasePlayer *pPlayer = UTIL_GetCommandClient(); + + QAngle angEye = pPlayer->EyeAngles(); + CHunterFlechette *entity = CHunterFlechette::FlechetteCreate( pPlayer->EyePosition(), angEye, pPlayer ); + if ( entity ) + { + entity->Precache(); + DispatchSpawn( entity ); + + // Shoot the flechette. + Vector forward; + pPlayer->EyeVectors( &forward ); + forward *= 2000.0f; + entity->Shoot( forward, false ); + } + + CBaseEntity::SetAllowPrecache( allowPrecache ); +} + +static ConCommand ent_create("hunter_shoot_flechette", CC_Hunter_Shoot_Flechette, "Fires a hunter flechette where the player is looking.", FCVAR_GAMEDLL | FCVAR_CHEAT); + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +CHunterFlechette::CHunterFlechette() +{ + UseClientSideAnimation(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +CHunterFlechette::~CHunterFlechette() +{ +} + + +//----------------------------------------------------------------------------- +// If set, the flechette will seek unerringly toward the target as it flies. +//----------------------------------------------------------------------------- +void CHunterFlechette::SetSeekTarget( CBaseEntity *pTargetEntity ) +{ + if ( pTargetEntity ) + { + m_hSeekTarget = pTargetEntity; + SetContextThink( &CHunterFlechette::SeekThink, gpGlobals->curtime, s_szHunterFlechetteSeekThink ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CHunterFlechette::CreateVPhysics() +{ + // Create the object in the physics system + VPhysicsInitNormal( SOLID_BBOX, FSOLID_NOT_STANDABLE, false ); + + return true; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +unsigned int CHunterFlechette::PhysicsSolidMaskForEntity() const +{ + return ( BaseClass::PhysicsSolidMaskForEntity() | CONTENTS_HITBOX ) & ~CONTENTS_GRATE; +} + + +//----------------------------------------------------------------------------- +// Called from CPropPhysics code when we're attached to a physics object. +//----------------------------------------------------------------------------- +void CHunterFlechette::OnParentCollisionInteraction( parentCollisionInteraction_t eType, int index, gamevcollisionevent_t *pEvent ) +{ + if ( eType == COLLISIONINTER_PARENT_FIRST_IMPACT ) + { + m_bThrownBack = true; + Explode(); + } +} + +void CHunterFlechette::OnParentPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) +{ + m_bThrownBack = true; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CHunterFlechette::CreateSprites( bool bBright ) +{ + if ( bBright ) + { + DispatchParticleEffect( "hunter_flechette_trail_striderbuster", PATTACH_ABSORIGIN_FOLLOW, this ); + } + else + { + DispatchParticleEffect( "hunter_flechette_trail", PATTACH_ABSORIGIN_FOLLOW, this ); + } + + return true; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHunterFlechette::Spawn() +{ + Precache( ); + + SetModel( HUNTER_FLECHETTE_MODEL ); + SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_CUSTOM ); + UTIL_SetSize( this, -Vector(1,1,1), Vector(1,1,1) ); + SetSolid( SOLID_BBOX ); + SetGravity( 0.05f ); + SetCollisionGroup( COLLISION_GROUP_PROJECTILE ); + + // Make sure we're updated if we're underwater + UpdateWaterState(); + + SetTouch( &CHunterFlechette::FlechetteTouch ); + + // Make us glow until we've hit the wall + m_nSkin = 1; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHunterFlechette::Activate() +{ + BaseClass::Activate(); + SetupGlobalModelData(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHunterFlechette::SetupGlobalModelData() +{ + if ( s_nHunterFlechetteImpact == -2 ) + { + s_nHunterFlechetteImpact = LookupSequence( "impact" ); + s_nFlechetteFuseAttach = LookupAttachment( "attach_fuse" ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHunterFlechette::Precache() +{ + PrecacheModel( HUNTER_FLECHETTE_MODEL ); + PrecacheModel( "sprites/light_glow02_noz.vmt" ); + + PrecacheScriptSound( "NPC_Hunter.FlechetteNearmiss" ); + PrecacheScriptSound( "NPC_Hunter.FlechetteHitBody" ); + PrecacheScriptSound( "NPC_Hunter.FlechetteHitWorld" ); + PrecacheScriptSound( "NPC_Hunter.FlechettePreExplode" ); + PrecacheScriptSound( "NPC_Hunter.FlechetteExplode" ); + + PrecacheParticleSystem( "hunter_flechette_trail_striderbuster" ); + PrecacheParticleSystem( "hunter_flechette_trail" ); + PrecacheParticleSystem( "hunter_projectile_explosion_1" ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHunterFlechette::StickTo( CBaseEntity *pOther, trace_t &tr ) +{ + EmitSound( "NPC_Hunter.FlechetteHitWorld" ); + + SetMoveType( MOVETYPE_NONE ); + + if ( !pOther->IsWorld() ) + { + SetParent( pOther ); + SetSolid( SOLID_NONE ); + SetSolidFlags( FSOLID_NOT_SOLID ); + } + + // Do an impact effect. + //Vector vecDir = GetAbsVelocity(); + //float speed = VectorNormalize( vecDir ); + + //Vector vForward; + //AngleVectors( GetAbsAngles(), &vForward ); + //VectorNormalize ( vForward ); + + //CEffectData data; + //data.m_vOrigin = tr.endpos; + //data.m_vNormal = vForward; + //data.m_nEntIndex = 0; + //DispatchEffect( "BoltImpact", data ); + + Vector vecVelocity = GetAbsVelocity(); + bool bAttachedToBuster = StriderBuster_OnFlechetteAttach( pOther, vecVelocity ); + + SetTouch( NULL ); + + // We're no longer flying. Stop checking for water volumes. + SetContextThink( NULL, 0, s_szHunterFlechetteBubbles ); + + // Stop seeking. + m_hSeekTarget = NULL; + SetContextThink( NULL, 0, s_szHunterFlechetteSeekThink ); + + // Get ready to explode. + if ( !bAttachedToBuster ) + { + SetThink( &CHunterFlechette::DangerSoundThink ); + SetNextThink( gpGlobals->curtime + (hunter_flechette_explode_delay.GetFloat() - HUNTER_FLECHETTE_WARN_TIME) ); + } + else + { + DangerSoundThink(); + } + + // Play our impact animation. + ResetSequence( s_nHunterFlechetteImpact ); + + static int s_nImpactCount = 0; + s_nImpactCount++; + if ( s_nImpactCount & 0x01 ) + { + UTIL_ImpactTrace( &tr, DMG_BULLET ); + + // Shoot some sparks + if ( UTIL_PointContents( GetAbsOrigin() ) != CONTENTS_WATER) + { + g_pEffects->Sparks( GetAbsOrigin() ); + } + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHunterFlechette::FlechetteTouch( CBaseEntity *pOther ) +{ + if ( pOther->IsSolidFlagSet(FSOLID_VOLUME_CONTENTS | FSOLID_TRIGGER) ) + { + // Some NPCs are triggers that can take damage (like antlion grubs). We should hit them. + if ( ( pOther->m_takedamage == DAMAGE_NO ) || ( pOther->m_takedamage == DAMAGE_EVENTS_ONLY ) ) + return; + } + + if ( FClassnameIs( pOther, "hunter_flechette" ) ) + return; + + trace_t tr; + tr = BaseClass::GetTouchTrace(); + + if ( pOther->m_takedamage != DAMAGE_NO ) + { + Vector vecNormalizedVel = GetAbsVelocity(); + + ClearMultiDamage(); + VectorNormalize( vecNormalizedVel ); + + float flDamage = sk_hunter_dmg_flechette.GetFloat(); + CBreakable *pBreak = dynamic_cast <CBreakable *>(pOther); + if ( pBreak && ( pBreak->GetMaterialType() == matGlass ) ) + { + flDamage = MAX( pOther->GetHealth(), flDamage ); + } + + CTakeDamageInfo dmgInfo( this, GetOwnerEntity(), flDamage, DMG_DISSOLVE | DMG_NEVERGIB ); + CalculateMeleeDamageForce( &dmgInfo, vecNormalizedVel, tr.endpos, 0.7f ); + dmgInfo.SetDamagePosition( tr.endpos ); + pOther->DispatchTraceAttack( dmgInfo, vecNormalizedVel, &tr ); + + ApplyMultiDamage(); + + // Keep going through breakable glass. + if ( pOther->GetCollisionGroup() == COLLISION_GROUP_BREAKABLE_GLASS ) + return; + + SetAbsVelocity( Vector( 0, 0, 0 ) ); + + // play body "thwack" sound + EmitSound( "NPC_Hunter.FlechetteHitBody" ); + + StopParticleEffects( this ); + + Vector vForward; + AngleVectors( GetAbsAngles(), &vForward ); + VectorNormalize ( vForward ); + + trace_t tr2; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + vForward * 128, MASK_BLOCKLOS, pOther, COLLISION_GROUP_NONE, &tr2 ); + + if ( tr2.fraction != 1.0f ) + { + //NDebugOverlay::Box( tr2.endpos, Vector( -16, -16, -16 ), Vector( 16, 16, 16 ), 0, 255, 0, 0, 10 ); + //NDebugOverlay::Box( GetAbsOrigin(), Vector( -16, -16, -16 ), Vector( 16, 16, 16 ), 0, 0, 255, 0, 10 ); + + if ( tr2.m_pEnt == NULL || ( tr2.m_pEnt && tr2.m_pEnt->GetMoveType() == MOVETYPE_NONE ) ) + { + CEffectData data; + + data.m_vOrigin = tr2.endpos; + data.m_vNormal = vForward; + data.m_nEntIndex = tr2.fraction != 1.0f; + + //DispatchEffect( "BoltImpact", data ); + } + } + + if ( ( ( pOther->GetMoveType() == MOVETYPE_VPHYSICS ) || ( pOther->GetMoveType() == MOVETYPE_PUSH ) ) && ( ( pOther->GetHealth() > 0 ) || ( pOther->m_takedamage == DAMAGE_EVENTS_ONLY ) ) ) + { + CPhysicsProp *pProp = dynamic_cast<CPhysicsProp *>( pOther ); + if ( pProp ) + { + pProp->SetInteraction( PROPINTER_PHYSGUN_NOTIFY_CHILDREN ); + } + + // We hit a physics object that survived the impact. Stick to it. + StickTo( pOther, tr ); + } + else + { + SetTouch( NULL ); + SetThink( NULL ); + SetContextThink( NULL, 0, s_szHunterFlechetteBubbles ); + + UTIL_Remove( this ); + } + } + else + { + // See if we struck the world + if ( pOther->GetMoveType() == MOVETYPE_NONE && !( tr.surface.flags & SURF_SKY ) ) + { + // We hit a physics object that survived the impact. Stick to it. + StickTo( pOther, tr ); + } + else if( pOther->GetMoveType() == MOVETYPE_PUSH && FClassnameIs(pOther, "func_breakable") ) + { + // We hit a func_breakable, stick to it. + // The MOVETYPE_PUSH is a micro-optimization to cut down on the classname checks. + StickTo( pOther, tr ); + } + else + { + // Put a mark unless we've hit the sky + if ( ( tr.surface.flags & SURF_SKY ) == false ) + { + UTIL_ImpactTrace( &tr, DMG_BULLET ); + } + + UTIL_Remove( this ); + } + } +} + + +//----------------------------------------------------------------------------- +// Fixup flechette position when seeking towards a striderbuster. +//----------------------------------------------------------------------------- +void CHunterFlechette::SeekThink() +{ + if ( m_hSeekTarget ) + { + Vector vecBodyTarget = m_hSeekTarget->BodyTarget( GetAbsOrigin() ); + + Vector vecClosest; + CalcClosestPointOnLineSegment( GetAbsOrigin(), m_vecShootPosition, vecBodyTarget, vecClosest, NULL ); + + Vector vecDelta = vecBodyTarget - m_vecShootPosition; + VectorNormalize( vecDelta ); + + QAngle angShoot; + VectorAngles( vecDelta, angShoot ); + + float flSpeed = hunter_flechette_speed.GetFloat(); + if ( !flSpeed ) + { + flSpeed = 2500.0f; + } + + Vector vecVelocity = vecDelta * flSpeed; + Teleport( &vecClosest, &angShoot, &vecVelocity ); + + SetNextThink( gpGlobals->curtime, s_szHunterFlechetteSeekThink ); + } +} + + +//----------------------------------------------------------------------------- +// Play a near miss sound as we travel past the player. +//----------------------------------------------------------------------------- +void CHunterFlechette::DopplerThink() +{ + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + if ( !pPlayer ) + return; + + Vector vecVelocity = GetAbsVelocity(); + VectorNormalize( vecVelocity ); + + float flMyDot = DotProduct( vecVelocity, GetAbsOrigin() ); + float flPlayerDot = DotProduct( vecVelocity, pPlayer->GetAbsOrigin() ); + + if ( flPlayerDot <= flMyDot ) + { + EmitSound( "NPC_Hunter.FlechetteNearMiss" ); + + // We've played the near miss sound and we're not seeking. Stop thinking. + SetThink( NULL ); + } + else + { + SetNextThink( gpGlobals->curtime ); + } +} + + +//----------------------------------------------------------------------------- +// Think every 0.1 seconds to make bubbles if we're flying through water. +//----------------------------------------------------------------------------- +void CHunterFlechette::BubbleThink() +{ + SetNextThink( gpGlobals->curtime + 0.1f, s_szHunterFlechetteBubbles ); + + if ( GetWaterLevel() == 0 ) + return; + + UTIL_BubbleTrail( GetAbsOrigin() - GetAbsVelocity() * 0.1f, GetAbsOrigin(), 5 ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHunterFlechette::Shoot( Vector &vecVelocity, bool bBrightFX ) +{ + CreateSprites( bBrightFX ); + + m_vecShootPosition = GetAbsOrigin(); + + SetAbsVelocity( vecVelocity ); + + SetThink( &CHunterFlechette::DopplerThink ); + SetNextThink( gpGlobals->curtime ); + + SetContextThink( &CHunterFlechette::BubbleThink, gpGlobals->curtime + 0.1, s_szHunterFlechetteBubbles ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHunterFlechette::DangerSoundThink() +{ + EmitSound( "NPC_Hunter.FlechettePreExplode" ); + + CSoundEnt::InsertSound( SOUND_DANGER|SOUND_CONTEXT_EXCLUDE_COMBINE, GetAbsOrigin(), 150.0f, 0.5, this ); + SetThink( &CHunterFlechette::ExplodeThink ); + SetNextThink( gpGlobals->curtime + HUNTER_FLECHETTE_WARN_TIME ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHunterFlechette::ExplodeThink() +{ + Explode(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CHunterFlechette::Explode() +{ + SetSolid( SOLID_NONE ); + + // Don't catch self in own explosion! + m_takedamage = DAMAGE_NO; + + EmitSound( "NPC_Hunter.FlechetteExplode" ); + + // Move the explosion effect to the tip to reduce intersection with the world. + Vector vecFuse; + GetAttachment( s_nFlechetteFuseAttach, vecFuse ); + DispatchParticleEffect( "hunter_projectile_explosion_1", vecFuse, GetAbsAngles(), NULL ); + + int nDamageType = DMG_DISSOLVE; + + // Perf optimization - only every other explosion makes a physics force. This is + // hardly noticeable since flechettes usually explode in clumps. + static int s_nExplosionCount = 0; + s_nExplosionCount++; + if ( ( s_nExplosionCount & 0x01 ) && hunter_cheap_explosions.GetBool() ) + { + nDamageType |= DMG_PREVENT_PHYSICS_FORCE; + } + + RadiusDamage( CTakeDamageInfo( this, GetOwnerEntity(), sk_hunter_flechette_explode_dmg.GetFloat(), nDamageType ), GetAbsOrigin(), sk_hunter_flechette_explode_radius.GetFloat(), CLASS_NONE, NULL ); + + AddEffects( EF_NODRAW ); + + SetThink( &CBaseEntity::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); +} + + +//----------------------------------------------------------------------------- +// Calculate & apply damage & force for a charge to a target. +// Done outside of the hunter because we need to do this inside a trace filter. +//----------------------------------------------------------------------------- +void Hunter_ApplyChargeDamage( CBaseEntity *pHunter, CBaseEntity *pTarget, float flDamage ) +{ + Vector attackDir = ( pTarget->WorldSpaceCenter() - pHunter->WorldSpaceCenter() ); + VectorNormalize( attackDir ); + Vector offset = RandomVector( -32, 32 ) + pTarget->WorldSpaceCenter(); + + // Generate enough force to make a 75kg guy move away at 700 in/sec + Vector vecForce = attackDir * ImpulseScale( 75, 700 ); + + // Deal the damage + CTakeDamageInfo info( pHunter, pHunter, vecForce, offset, flDamage, DMG_CLUB ); + pTarget->TakeDamage( info ); +} + + +//----------------------------------------------------------------------------- +// A simple trace filter class to skip small moveable physics objects +//----------------------------------------------------------------------------- +class CHunterTraceFilterSkipPhysics : public CTraceFilter +{ +public: + // It does have a base, but we'll never network anything below here.. + DECLARE_CLASS_NOBASE( CHunterTraceFilterSkipPhysics ); + + CHunterTraceFilterSkipPhysics( const IHandleEntity *passentity, int collisionGroup, float minMass ) + : m_pPassEnt(passentity), m_collisionGroup(collisionGroup), m_minMass(minMass) + { + } + virtual bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) + { + if ( !StandardFilterRules( pHandleEntity, contentsMask ) ) + return false; + + if ( !PassServerEntityFilter( pHandleEntity, m_pPassEnt ) ) + return false; + + // Don't test if the game code tells us we should ignore this collision... + CBaseEntity *pEntity = EntityFromEntityHandle( pHandleEntity ); + if ( pEntity ) + { + if ( !pEntity->ShouldCollide( m_collisionGroup, contentsMask ) ) + return false; + + if ( !g_pGameRules->ShouldCollide( m_collisionGroup, pEntity->GetCollisionGroup() ) ) + return false; + + // don't test small moveable physics objects (unless it's an NPC) + if ( !pEntity->IsNPC() && pEntity->GetMoveType() == MOVETYPE_VPHYSICS ) + { + float entMass = PhysGetEntityMass( pEntity ) ; + if ( entMass < m_minMass ) + { + if ( entMass < m_minMass * 0.666f || pEntity->CollisionProp()->BoundingRadius() < (assert_cast<const CAI_BaseNPC *>(EntityFromEntityHandle( m_pPassEnt )))->GetHullHeight() ) + { + return false; + } + } + } + + // If we hit an antlion, don't stop, but kill it + if ( pEntity->Classify() == CLASS_ANTLION ) + { + CBaseEntity *pHunter = (CBaseEntity *)EntityFromEntityHandle( m_pPassEnt ); + Hunter_ApplyChargeDamage( pHunter, pEntity, pEntity->GetHealth() ); + return false; + } + } + + return true; + } + +private: + const IHandleEntity *m_pPassEnt; + int m_collisionGroup; + float m_minMass; +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +inline void HunterTraceHull_SkipPhysics( const Vector &vecAbsStart, const Vector &vecAbsEnd, const Vector &hullMin, + const Vector &hullMax, unsigned int mask, const CBaseEntity *ignore, + int collisionGroup, trace_t *ptr, float minMass ) +{ + Ray_t ray; + ray.Init( vecAbsStart, vecAbsEnd, hullMin, hullMax ); + CHunterTraceFilterSkipPhysics traceFilter( ignore, collisionGroup, minMass ); + enginetrace->TraceRay( ray, mask, &traceFilter, ptr ); +} + + +//----------------------------------------------------------------------------- +// Hunter follow behavior +//----------------------------------------------------------------------------- +class CAI_HunterEscortBehavior : public CAI_FollowBehavior +{ +public: + DECLARE_CLASS( CAI_HunterEscortBehavior, CAI_FollowBehavior ); + + CAI_HunterEscortBehavior() : + BaseClass( AI_FollowParams_t( AIF_HUNTER, true ) ), + m_flTimeEscortReturn( 0 ), + m_bEnabled( false ) + { + } + + CNPC_Hunter *GetOuter() { return (CNPC_Hunter *)( BaseClass::GetOuter() ); } + + void SetEscortTarget( CNPC_Strider *pLeader, bool fFinishCurSchedule = false ); + CNPC_Strider * GetEscortTarget() { return (CNPC_Strider *)GetFollowTarget(); } + + bool FarFromFollowTarget() + { + return ( GetFollowTarget() && (GetAbsOrigin() - GetFollowTarget()->GetAbsOrigin()).LengthSqr() > HUNTER_FOLLOW_DISTANCE_SQR ); + } + + void DrawDebugGeometryOverlays(); + bool ShouldFollow(); + void BuildScheduleTestBits(); + + void BeginScheduleSelection(); + + void GatherConditions(); + void GatherConditionsNotActive(); + int SelectSchedule(); + int FollowCallBaseSelectSchedule(); + void StartTask( const Task_t *pTask ); + void RunTask( const Task_t *pTask ); + + void CheckBreakEscort(); + + void OnDamage( const CTakeDamageInfo &info ); + static void DistributeFreeHunters(); + static void FindFreeHunters( CUtlVector<CNPC_Hunter *> *pFreeHunters ); + + float m_flTimeEscortReturn; + CSimpleSimTimer m_FollowAttackTimer; + bool m_bEnabled; + + static float gm_flLastDefendSound; // not saved and loaded, it's okay to yell again after a load + + //--------------------------------- + + DECLARE_DATADESC(); +}; + + +BEGIN_DATADESC( CAI_HunterEscortBehavior ) + DEFINE_FIELD( m_flTimeEscortReturn, FIELD_TIME ), + DEFINE_EMBEDDED( m_FollowAttackTimer ), + DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), +END_DATADESC(); + +float CAI_HunterEscortBehavior::gm_flLastDefendSound; + +//----------------------------------------------------------------------------- +// Hunter PHYSICS DAMAGE TABLE +//----------------------------------------------------------------------------- +#define HUNTER_MIN_PHYSICS_DAMAGE 10 + +static impactentry_t s_HunterLinearTable[] = +{ + { 150*150, 75 }, + { 350*350, 105 }, + { 1000*1000, 300 }, +}; + +static impactentry_t s_HunterAngularTable[] = +{ + { 100*100, 75 }, + { 200*200, 105 }, + { 300*300, 300 }, +}; + +impactdamagetable_t s_HunterImpactDamageTable = +{ + s_HunterLinearTable, + s_HunterAngularTable, + + ARRAYSIZE(s_HunterLinearTable), + ARRAYSIZE(s_HunterAngularTable), + + 24*24, // minimum linear speed squared + 360*360, // minimum angular speed squared (360 deg/s to cause spin/slice damage) + 5, // can't take damage from anything under 5kg + + 10, // anything less than 10kg is "small" + HUNTER_MIN_PHYSICS_DAMAGE, // never take more than 10 pts of damage from anything under 10kg + 36*36, // <10kg objects must go faster than 36 in/s to do damage + + VPHYSICS_LARGE_OBJECT_MASS, // large mass in kg + 4, // large mass scale (anything over 500kg does 4X as much energy to read from damage table) + 5, // large mass falling scale (emphasize falling/crushing damage over sideways impacts since the stress will kill you anyway) + 0.0f, // min vel +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +class CNPC_Hunter : public CAI_BaseActor +{ + DECLARE_CLASS( CNPC_Hunter, CAI_BaseActor ); + +public: + CNPC_Hunter(); + ~CNPC_Hunter(); + + //--------------------------------- + + void Precache(); + void Spawn(); + void PostNPCInit(); + void Activate(); + void UpdateOnRemove(); + void OnRestore(); + bool CreateBehaviors(); + void IdleSound(); + bool ShouldPlayIdleSound(); + bool CanBecomeRagdoll(); + Activity GetDeathActivity(); + void StopLoopingSounds(); + + const impactdamagetable_t &GetPhysicsImpactDamageTable(); + + Class_T Classify(); + Vector BodyTarget( const Vector &posSrc, bool bNoisy /*= true*/ ); + + int DrawDebugTextOverlays(); + void DrawDebugGeometryOverlays(); + + void UpdateEfficiency( bool bInPVS ); + + //--------------------------------- + + virtual Vector GetNodeViewOffset() { return BaseClass::GetDefaultEyeOffset(); } + + int GetSoundInterests(); + + bool IsInLargeOutdoorMap(); + + //--------------------------------- + // CAI_BaseActor + //--------------------------------- + const char *SelectRandomExpressionForState( NPC_STATE state ); + void PlayExpressionForState( NPC_STATE state ); + + //--------------------------------- + // CBaseAnimating + //--------------------------------- + float GetIdealAccel() const { return GetIdealSpeed(); } + + //--------------------------------- + // Behavior + //--------------------------------- + void NPCThink(); + void PrescheduleThink(); + void GatherConditions(); + void CollectSiegeTargets(); + void ManageSiegeTargets(); + void KillCurrentSiegeTarget(); + bool QueryHearSound( CSound *pSound ); + void OnSeeEntity( CBaseEntity *pEntity ); + void CheckFlinches() {} // Hunter handles on own + void BuildScheduleTestBits(); + NPC_STATE SelectIdealState(); + int SelectSchedule(); + int SelectCombatSchedule(); + int SelectSiegeSchedule(); + int TranslateSchedule( int scheduleType ); + void StartTask( const Task_t *pTask ); + void RunTask( const Task_t *pTask ); + Activity NPC_TranslateActivity( Activity baseAct ); + void OnChangeActivity( Activity eNewActivity ); + + void HandleAnimEvent( animevent_t *pEvent ); + bool HandleInteraction(int interactionType, void *data, CBaseCombatCharacter *pSourceEnt); + + void PlayerHasIlluminatedNPC( CBasePlayer *pPlayer, float flDot ); + + void AddEntityRelationship( CBaseEntity *pEntity, Disposition_t nDisposition, int nPriority ); + float EnemyDistTolerance() { return 100.0f; } + + bool ScheduledMoveToGoalEntity( int scheduleType, CBaseEntity *pGoalEntity, Activity movementActivity ); + + void OnChangeHintGroup( string_t oldGroup, string_t newGroup ); + + bool IsUsingSiegeTargets() { return m_iszSiegeTargetName != NULL_STRING; } + + //--------------------------------- + // Inputs + //--------------------------------- + void InputDodge( inputdata_t &inputdata ); + void InputFlankEnemy( inputdata_t &inputdata ); + void InputDisableShooting( inputdata_t &inputdata ); + void InputEnableShooting( inputdata_t &inputdata ); + void InputFollowStrider( inputdata_t &inputdata ); + void InputUseSiegeTargets( inputdata_t &inputdata ); + void InputEnableSquadShootDelay( inputdata_t &inputdata ); + void InputDisableSquadShootDelay( inputdata_t &inputdata ); + void InputEnableUnplantedShooting( inputdata_t &inputdata ); + void InputDisableUnplantedShooting( inputdata_t &inputdata ); + + //--------------------------------- + // Combat + //--------------------------------- + bool FVisible( CBaseEntity *pEntity, int traceMask = MASK_BLOCKLOS, CBaseEntity **ppBlocker = NULL ); + bool IsValidEnemy( CBaseEntity *pEnemy ); + + Disposition_t IRelationType( CBaseEntity *pTarget ); + int IRelationPriority( CBaseEntity *pTarget ); + + void SetSquad( CAI_Squad *pSquad ); + + bool UpdateEnemyMemory( CBaseEntity *pEnemy, const Vector &position, CBaseEntity *pInformer = NULL ); + + int RangeAttack1Conditions( float flDot, float flDist ); + int RangeAttack2Conditions( float flDot, float flDist ); + + int MeleeAttack1Conditions ( float flDot, float flDist ); + int MeleeAttack1ConditionsVsEnemyInVehicle( CBaseCombatCharacter *pEnemy, float flDot ); + + int MeleeAttack2Conditions( float flDot, float flDist ); + + bool WeaponLOSCondition(const Vector &ownerPos, const Vector &targetPos, bool bSetConditions); + bool TestShootPosition(const Vector &vecShootPos, const Vector &targetPos ); + + Vector Weapon_ShootPosition(); + + CBaseEntity * MeleeAttack( float flDist, int iDamage, QAngle &qaViewPunch, Vector &vecVelocityPunch, int BloodOrigin ); + + void MakeTracer( const Vector &vecTracerSrc, const trace_t &tr, int iTracerType ); + void DoMuzzleFlash( int nAttachment ); + + bool CanShootThrough( const trace_t &tr, const Vector &vecTarget ); + + int CountRangedAttackers(); + void DelayRangedAttackers( float minDelay, float maxDelay, bool bForced = false ); + + //--------------------------------- + // Sounds & speech + //--------------------------------- + void AlertSound(); + void PainSound( const CTakeDamageInfo &info ); + void DeathSound( const CTakeDamageInfo &info ); + + //--------------------------------- + // Damage handling + //--------------------------------- + void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ); + bool IsHeavyDamage( const CTakeDamageInfo &info ); + int OnTakeDamage( const CTakeDamageInfo &info ); + int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + void Event_Killed( const CTakeDamageInfo &info ); + + void StartBleeding(); + inline bool IsBleeding() { return m_bIsBleeding; } + void Explode(); + + void SetupGlobalModelData(); + + //--------------------------------- + // Navigation & Movement + //--------------------------------- + bool OverrideMoveFacing( const AILocalMoveGoal_t &move, float flInterval ); + float MaxYawSpeed(); + bool IsJumpLegal(const Vector &startPos, const Vector &apex, const Vector &endPos) const; + float GetJumpGravity() const { return 3.0f; } + bool ShouldProbeCollideAgainstEntity( CBaseEntity *pEntity ); + void TaskFail( AI_TaskFailureCode_t code ); + void TaskFail( const char *pszGeneralFailText ) { TaskFail( MakeFailCode( pszGeneralFailText ) ); } + + CAI_BaseNPC * GetEntity() { return this; } + + //--------------------------------- + // Magnade + //--------------------------------- + void StriderBusterAttached( CBaseEntity *pAttached ); + void StriderBusterDetached( CBaseEntity *pAttached ); + +private: + + void ConsiderFlinching( const CTakeDamageInfo &info ); + + void TaskFindDodgeActivity(); + + void GatherChargeConditions(); + void GatherIndoorOutdoorConditions(); + + // Charge attack. + bool ShouldCharge( const Vector &startPos, const Vector &endPos, bool useTime, bool bCheckForCancel ); + void ChargeLookAhead(); + float ChargeSteer(); + bool EnemyIsRightInFrontOfMe( CBaseEntity **pEntity ); + void ChargeDamage( CBaseEntity *pTarget ); + bool HandleChargeImpact( Vector vecImpact, CBaseEntity *pEntity ); + + void BeginVolley( int nNum, float flStartTime ); + bool ShootFlechette( CBaseEntity *pTargetEntity, bool bSingleShot ); + bool ShouldSeekTarget( CBaseEntity *pTargetEntity, bool bStriderBuster ); + void GetShootDir( Vector &vecDir, const Vector &vecSrc, CBaseEntity *pTargetEntity, bool bStriderbuster, int nShotNum, bool bSingleShot ); + bool ClampShootDir( Vector &vecDir ); + + void SetAim( const Vector &aimDir, float flInterval ); + void RelaxAim( float flInterval ); + void UpdateAim(); + void UpdateEyes(); + void LockBothEyes( float flDuration ); + void UnlockBothEyes( float flDuration ); + + void TeslaThink(); + void BleedThink(); + void JostleVehicleThink(); + + void FollowStrider( const char *szStrider ); + void FollowStrider( CNPC_Strider * pStrider ); + int NumHuntersInMySquad(); + + bool CanPlantHere( const Vector &vecPos ); + + //--------------------------------- + // Foot handling + //--------------------------------- + Vector LeftFootHit( float eventtime ); + Vector RightFootHit( float eventtime ); + Vector BackFootHit( float eventtime ); + + void FootFX( const Vector &origin ); + + CBaseEntity *GetEnemyVehicle(); + bool IsCorporealEnemy( CBaseEntity *pEnemy ); + + void PhysicsDamageEffect( const Vector &vecPos, const Vector &vecDir ); + bool PlayerFlashlightOnMyEyes( CBasePlayer *pPlayer ); + + //----------------------------------------------------- + // Conditions, Schedules, Tasks + //----------------------------------------------------- + enum + { + SCHED_HUNTER_RANGE_ATTACK1 = BaseClass::NEXT_SCHEDULE, + SCHED_HUNTER_RANGE_ATTACK2, + SCHED_HUNTER_MELEE_ATTACK1, + SCHED_HUNTER_DODGE, + SCHED_HUNTER_CHASE_ENEMY, + SCHED_HUNTER_CHASE_ENEMY_MELEE, + SCHED_HUNTER_COMBAT_FACE, + SCHED_HUNTER_FLANK_ENEMY, + SCHED_HUNTER_CHANGE_POSITION, + SCHED_HUNTER_CHANGE_POSITION_FINISH, + SCHED_HUNTER_SIDESTEP, + SCHED_HUNTER_PATROL, + SCHED_HUNTER_FLINCH_STICKYBOMB, + SCHED_HUNTER_STAGGER, + SCHED_HUNTER_PATROL_RUN, + SCHED_HUNTER_TAKE_COVER_FROM_ENEMY, + SCHED_HUNTER_HIDE_UNDER_COVER, + SCHED_HUNTER_FAIL_IMMEDIATE, // instant fail without waiting + SCHED_HUNTER_CHARGE_ENEMY, + SCHED_HUNTER_FAIL_CHARGE_ENEMY, + SCHED_HUNTER_FOUND_ENEMY, + SCHED_HUNTER_FOUND_ENEMY_ACK, + SCHED_HUNTER_RANGE_ATTACK2_VS_STRIDERBUSTER, + SCHED_HUNTER_RANGE_ATTACK2_VS_STRIDERBUSTER_LATENT, + SCHED_HUNTER_GOTO_HINT, + SCHED_HUNTER_CLEAR_HINTNODE, + SCHED_HUNTER_FAIL_DODGE, + SCHED_HUNTER_SIEGE_STAND, + SCHED_HUNTER_CHANGE_POSITION_SIEGE, + + TASK_HUNTER_AIM = BaseClass::NEXT_TASK, + TASK_HUNTER_FIND_DODGE_POSITION, + TASK_HUNTER_DODGE, + TASK_HUNTER_PRE_RANGE_ATTACK2, + TASK_HUNTER_SHOOT_COMMIT, + TASK_HUNTER_BEGIN_FLANK, + TASK_HUNTER_ANNOUNCE_FLANK, + TASK_HUNTER_STAGGER, + TASK_HUNTER_CORNERED_TIMER, + TASK_HUNTER_FIND_SIDESTEP_POSITION, + TASK_HUNTER_CHARGE, + TASK_HUNTER_CHARGE_DELAY, + TASK_HUNTER_FINISH_RANGE_ATTACK, + TASK_HUNTER_WAIT_FOR_MOVEMENT_FACING_ENEMY, + + COND_HUNTER_SHOULD_PATROL = BaseClass::NEXT_CONDITION, + COND_HUNTER_FORCED_FLANK_ENEMY, + COND_HUNTER_FORCED_DODGE, + COND_HUNTER_CAN_CHARGE_ENEMY, + COND_HUNTER_HIT_BY_STICKYBOMB, + COND_HUNTER_STAGGERED, + COND_HUNTER_IS_INDOORS, + COND_HUNTER_SEE_STRIDERBUSTER, + COND_HUNTER_INCOMING_VEHICLE, + COND_HUNTER_NEW_HINTGROUP, + COND_HUNTER_CANT_PLANT, + COND_HUNTER_SQUADMATE_FOUND_ENEMY, + }; + + enum HunterEyeStates_t + { + HUNTER_EYE_STATE_TOP_LOCKED = 0, + HUNTER_EYE_STATE_BOTTOM_LOCKED, + HUNTER_EYE_STATE_BOTH_LOCKED, + HUNTER_EYE_STATE_BOTH_UNLOCKED, + }; + + string_t m_iszFollowTarget; // Name of the strider we should follow. + CSimpleStopwatch m_BeginFollowDelay; + + int m_nKillingDamageType; + HunterEyeStates_t m_eEyeState; + + float m_aimYaw; + float m_aimPitch; + + float m_flShootAllowInterruptTime; + float m_flNextChargeTime; // Prevents us from doing our threat display too often. + float m_flNextDamageTime; + float m_flNextSideStepTime; + + CSimpleSimTimer m_HeavyDamageDelay; + CSimpleSimTimer m_FlinchTimer; + CSimpleSimTimer m_EyeSwitchTimer; // Controls how often we switch which eye is focusing on our enemy. + + bool m_bTopMuzzle; // Used to alternate between top muzzle FX and bottom muzzle FX. + bool m_bEnableSquadShootDelay; + bool m_bIsBleeding; + + Activity m_eDodgeActivity; + CSimpleSimTimer m_RundownDelay; + CSimpleSimTimer m_IgnoreVehicleTimer; + + bool m_bDisableShooting; // Range attack disabled via an input. Used for scripting melee attacks. + + bool m_bFlashlightInEyes; // The player is shining the flashlight on our eyes. + float m_flPupilDilateTime; // When to dilate our pupils if the flashlight is no longer on our eyes. + + Vector m_vecEnemyLastSeen; + Vector m_vecLastCanPlantHerePos; + Vector m_vecStaggerDir; + + bool m_bPlanted; + bool m_bLastCanPlantHere; + bool m_bMissLeft; + bool m_bEnableUnplantedShooting; + + static float gm_flMinigunDistZ; + static Vector gm_vecLocalRelativePositionMinigun; + + static int gm_nTopGunAttachment; + static int gm_nBottomGunAttachment; + static int gm_nAimYawPoseParam; + static int gm_nAimPitchPoseParam; + static int gm_nBodyYawPoseParam; + static int gm_nBodyPitchPoseParam; + static int gm_nStaggerYawPoseParam; + static int gm_nHeadCenterAttachment; + static int gm_nHeadBottomAttachment; + static float gm_flHeadRadius; + + static int gm_nUnplantedNode; + static int gm_nPlantedNode; + + CAI_HunterEscortBehavior m_EscortBehavior; + + int m_nFlechettesQueued; + int m_nClampedShots; // The number of consecutive shots fired at an out-of-max yaw target. + + float m_flNextRangeAttack2Time; // Time when we can fire another volley of flechettes. + float m_flNextFlechetteTime; // Time to fire the next flechette in this volley. + + float m_flNextMeleeTime; + float m_flTeslaStopTime; + + string_t m_iszCurrentExpression; + + // buster fu + CUtlVector< EHANDLE > m_hAttachedBusters; // List of busters attached to us + float m_fCorneredTimer; ///< hunter was cornered when fleeing player; it won't flee again until this time + + CSimpleSimTimer m_CheckHintGroupTimer; + + DEFINE_CUSTOM_AI; + + DECLARE_DATADESC(); + + friend class CAI_HunterEscortBehavior; + friend class CHunterMaker; + + bool m_bInLargeOutdoorMap; + float m_flTimeSawEnemyAgain; + + // Sounds + //CSoundPatch *m_pGunFiringSound; + + CUtlVector<EHANDLE> m_pSiegeTargets; + string_t m_iszSiegeTargetName; + float m_flTimeNextSiegeTargetAttack; + EHANDLE m_hCurrentSiegeTarget; + + EHANDLE m_hHitByVehicle; +}; + + +LINK_ENTITY_TO_CLASS( npc_hunter, CNPC_Hunter ); + + +BEGIN_DATADESC( CNPC_Hunter ) + + DEFINE_KEYFIELD( m_iszFollowTarget, FIELD_STRING, "FollowTarget" ), + + DEFINE_FIELD( m_aimYaw, FIELD_FLOAT ), + DEFINE_FIELD( m_aimPitch, FIELD_FLOAT ), + + DEFINE_FIELD( m_flShootAllowInterruptTime, FIELD_TIME ), + DEFINE_FIELD( m_flNextChargeTime, FIELD_TIME ), + //DEFINE_FIELD( m_flNextDamageTime, FIELD_TIME ), + DEFINE_FIELD( m_flNextSideStepTime, FIELD_TIME ), + + DEFINE_EMBEDDED( m_HeavyDamageDelay ), + DEFINE_EMBEDDED( m_FlinchTimer ), + + DEFINE_FIELD( m_eEyeState, FIELD_INTEGER ), + + DEFINE_FIELD( m_bTopMuzzle, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bEnableSquadShootDelay, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bIsBleeding, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_bDisableShooting, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bFlashlightInEyes, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flPupilDilateTime, FIELD_TIME ), + + DEFINE_FIELD( m_vecEnemyLastSeen, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecLastCanPlantHerePos, FIELD_POSITION_VECTOR ), + DEFINE_FIELD( m_vecStaggerDir, FIELD_VECTOR ), + + DEFINE_FIELD( m_bPlanted, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bLastCanPlantHere, FIELD_BOOLEAN ), + //DEFINE_FIELD( m_bMissLeft, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bEnableUnplantedShooting, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_nKillingDamageType, FIELD_INTEGER ), + DEFINE_FIELD( m_eDodgeActivity, FIELD_INTEGER ), + DEFINE_EMBEDDED( m_RundownDelay ), + DEFINE_EMBEDDED( m_IgnoreVehicleTimer ), + + DEFINE_FIELD( m_flNextMeleeTime, FIELD_TIME ), + DEFINE_FIELD( m_flTeslaStopTime, FIELD_TIME ), + + // (auto saved by AI) + //DEFINE_FIELD( m_EscortBehavior, FIELD_EMBEDDED ), + + DEFINE_FIELD( m_iszCurrentExpression, FIELD_STRING ), + + DEFINE_FIELD( m_fCorneredTimer, FIELD_TIME), + + DEFINE_EMBEDDED( m_CheckHintGroupTimer ), + + // (Recomputed in Precache()) + //DEFINE_FIELD( m_bInLargeOutdoorMap, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flTimeSawEnemyAgain, FIELD_TIME ), + + //DEFINE_SOUNDPATCH( m_pGunFiringSound ), + + //DEFINE_UTLVECTOR( m_pSiegeTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_iszSiegeTargetName, FIELD_STRING ), + DEFINE_FIELD( m_flTimeNextSiegeTargetAttack, FIELD_TIME ), + DEFINE_FIELD( m_hCurrentSiegeTarget, FIELD_EHANDLE ), + DEFINE_FIELD( m_hHitByVehicle, FIELD_EHANDLE ), + + DEFINE_EMBEDDED( m_BeginFollowDelay ), + DEFINE_EMBEDDED( m_EyeSwitchTimer ), + + DEFINE_FIELD( m_nFlechettesQueued, FIELD_INTEGER ), + DEFINE_FIELD( m_nClampedShots, FIELD_INTEGER ), + DEFINE_FIELD( m_flNextRangeAttack2Time, FIELD_TIME ), + DEFINE_FIELD( m_flNextFlechetteTime, FIELD_TIME ), + DEFINE_UTLVECTOR( m_hAttachedBusters, FIELD_EHANDLE ), + DEFINE_UTLVECTOR( m_pSiegeTargets, FIELD_EHANDLE ), + + // inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Dodge", InputDodge ), + DEFINE_INPUTFUNC( FIELD_VOID, "FlankEnemy", InputFlankEnemy ), + DEFINE_INPUTFUNC( FIELD_STRING, "DisableShooting", InputDisableShooting ), + DEFINE_INPUTFUNC( FIELD_STRING, "EnableShooting", InputEnableShooting ), + DEFINE_INPUTFUNC( FIELD_STRING, "FollowStrider", InputFollowStrider ), + DEFINE_INPUTFUNC( FIELD_STRING, "UseSiegeTargets", InputUseSiegeTargets ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableSquadShootDelay", InputEnableSquadShootDelay ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableSquadShootDelay", InputDisableSquadShootDelay ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableUnplantedShooting", InputEnableUnplantedShooting ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableUnplantedShooting", InputDisableUnplantedShooting ), + + // Function Pointers + DEFINE_THINKFUNC( TeslaThink ), + DEFINE_THINKFUNC( BleedThink ), + DEFINE_THINKFUNC( JostleVehicleThink ), + +END_DATADESC() + +//----------------------------------------------------------------------------- + +int CNPC_Hunter::gm_nUnplantedNode = 0; +int CNPC_Hunter::gm_nPlantedNode = 0; + +int CNPC_Hunter::gm_nAimYawPoseParam = -1; +int CNPC_Hunter::gm_nAimPitchPoseParam = -1; +int CNPC_Hunter::gm_nBodyYawPoseParam = -1; +int CNPC_Hunter::gm_nBodyPitchPoseParam = -1; +int CNPC_Hunter::gm_nStaggerYawPoseParam = -1; +int CNPC_Hunter::gm_nHeadCenterAttachment = -1; +int CNPC_Hunter::gm_nHeadBottomAttachment = -1; +float CNPC_Hunter::gm_flHeadRadius = 0; + +int CNPC_Hunter::gm_nTopGunAttachment = -1; +int CNPC_Hunter::gm_nBottomGunAttachment = -1; + +float CNPC_Hunter::gm_flMinigunDistZ; +Vector CNPC_Hunter::gm_vecLocalRelativePositionMinigun; + +//----------------------------------------------------------------------------- + +static CUtlVector<CNPC_Hunter *> g_Hunters; +float g_TimeLastDistributeFreeHunters = -1; +const float FREE_HUNTER_DISTRIBUTE_INTERVAL = 2; + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +CNPC_Hunter::CNPC_Hunter() +{ + g_Hunters.AddToTail( this ); + g_TimeLastDistributeFreeHunters = -1; + m_flTimeSawEnemyAgain = HUNTER_SEE_ENEMY_TIME_INVALID; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +CNPC_Hunter::~CNPC_Hunter() +{ + g_Hunters.FindAndRemove( this ); + g_TimeLastDistributeFreeHunters = -1; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::Precache() +{ + PrecacheModel( "models/hunter.mdl" ); + PropBreakablePrecacheAll( MAKE_STRING("models/hunter.mdl") ); + + PrecacheScriptSound( "NPC_Hunter.Idle" ); + PrecacheScriptSound( "NPC_Hunter.Scan" ); + PrecacheScriptSound( "NPC_Hunter.Alert" ); + PrecacheScriptSound( "NPC_Hunter.Pain" ); + PrecacheScriptSound( "NPC_Hunter.PreCharge" ); + PrecacheScriptSound( "NPC_Hunter.Angry" ); + PrecacheScriptSound( "NPC_Hunter.Death" ); + PrecacheScriptSound( "NPC_Hunter.FireMinigun" ); + PrecacheScriptSound( "NPC_Hunter.Footstep" ); + PrecacheScriptSound( "NPC_Hunter.BackFootstep" ); + PrecacheScriptSound( "NPC_Hunter.FlechetteVolleyWarn" ); + PrecacheScriptSound( "NPC_Hunter.FlechetteShoot" ); + PrecacheScriptSound( "NPC_Hunter.FlechetteShootLoop" ); + PrecacheScriptSound( "NPC_Hunter.FlankAnnounce" ); + PrecacheScriptSound( "NPC_Hunter.MeleeAnnounce" ); + PrecacheScriptSound( "NPC_Hunter.MeleeHit" ); + PrecacheScriptSound( "NPC_Hunter.TackleAnnounce" ); + PrecacheScriptSound( "NPC_Hunter.TackleHit" ); + PrecacheScriptSound( "NPC_Hunter.ChargeHitEnemy" ); + PrecacheScriptSound( "NPC_Hunter.ChargeHitWorld" ); + PrecacheScriptSound( "NPC_Hunter.FoundEnemy" ); + PrecacheScriptSound( "NPC_Hunter.FoundEnemyAck" ); + PrecacheScriptSound( "NPC_Hunter.DefendStrider" ); + PrecacheScriptSound( "NPC_Hunter.HitByVehicle" ); + + PrecacheParticleSystem( "hunter_muzzle_flash" ); + PrecacheParticleSystem( "blood_impact_synth_01" ); + PrecacheParticleSystem( "blood_impact_synth_01_arc_parent" ); + PrecacheParticleSystem( "blood_spurt_synth_01" ); + PrecacheParticleSystem( "blood_drip_synth_01" ); + + PrecacheInstancedScene( "scenes/npc/hunter/hunter_scan.vcd" ); + PrecacheInstancedScene( "scenes/npc/hunter/hunter_eyeclose.vcd" ); + PrecacheInstancedScene( "scenes/npc/hunter/hunter_roar.vcd" ); + PrecacheInstancedScene( "scenes/npc/hunter/hunter_pain.vcd" ); + PrecacheInstancedScene( "scenes/npc/hunter/hunter_eyedarts_top.vcd" ); + PrecacheInstancedScene( "scenes/npc/hunter/hunter_eyedarts_bottom.vcd" ); + + PrecacheMaterial( "effects/water_highlight" ); + + UTIL_PrecacheOther( "hunter_flechette" ); + UTIL_PrecacheOther( "sparktrail" ); + + m_bInLargeOutdoorMap = false; + if( !Q_strnicmp( STRING(gpGlobals->mapname), "ep2_outland_12", 14) ) + { + m_bInLargeOutdoorMap = true; + } + + BaseClass::Precache(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::Spawn() +{ + Precache(); + + SetModel( "models/hunter.mdl" ); + BaseClass::Spawn(); + + //m_debugOverlays |= OVERLAY_NPC_ROUTE_BIT | OVERLAY_BBOX_BIT | OVERLAY_PIVOT_BIT; + + SetHullType( HULL_MEDIUM_TALL ); + SetHullSizeNormal(); + SetDefaultEyeOffset(); + + SetNavType( NAV_GROUND ); + m_flGroundSpeed = 500; + m_NPCState = NPC_STATE_NONE; + + SetBloodColor( DONT_BLEED ); + + m_iHealth = m_iMaxHealth = sk_hunter_health.GetInt(); + + m_flFieldOfView = HUNTER_FOV_DOT; + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + + SetupGlobalModelData(); + + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_SQUAD | bits_CAP_ANIMATEDFACE ); + CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_INNATE_RANGE_ATTACK2 | bits_CAP_INNATE_MELEE_ATTACK1 ); + CapabilitiesAdd( bits_CAP_SKIP_NAV_GROUND_CHECK ); + + if ( !hunter_allow_dissolve.GetBool() ) + { + AddEFlags( EFL_NO_DISSOLVE ); + } + + if( hunter_allow_nav_jump.GetBool() ) + { + CapabilitiesAdd( bits_CAP_MOVE_JUMP ); + } + + NPCInit(); + + m_bEnableSquadShootDelay = true; + + m_flDistTooFar = hunter_flechette_max_range.GetFloat(); + + // Discard time must be greater than free knowledge duration. Make it double. + float freeKnowledge = hunter_free_knowledge.GetFloat(); + if ( freeKnowledge < GetEnemies()->GetEnemyDiscardTime() ) + { + GetEnemies()->SetEnemyDiscardTime( MAX( freeKnowledge + 0.1, AI_DEF_ENEMY_DISCARD_TIME ) ); + } + GetEnemies()->SetFreeKnowledgeDuration( freeKnowledge ); + + // Find out what strider we should follow, if any. + if ( m_iszFollowTarget != NULL_STRING ) + { + m_BeginFollowDelay.Set( .1 ); // Allow time for strider to spawn + } + + //if ( !m_pGunFiringSound ) + //{ + // CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + // CPASAttenuationFilter filter( this ); + // + // m_pGunFiringSound = controller.SoundCreate( filter, entindex(), "NPC_Hunter.FlechetteShootLoop" ); + // controller.Play( m_pGunFiringSound, 0.0, 100 ); + //} +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::UpdateEfficiency( bool bInPVS ) +{ + SetEfficiency( ( GetSleepState() != AISS_AWAKE ) ? AIE_DORMANT : AIE_NORMAL ); + SetMoveEfficiency( AIME_NORMAL ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::CreateBehaviors() +{ + AddBehavior( &m_EscortBehavior ); + + return BaseClass::CreateBehaviors(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::SetupGlobalModelData() +{ + if ( gm_nBodyYawPoseParam != -1 ) + return; + + gm_nAimYawPoseParam = LookupPoseParameter( "aim_yaw" ); + gm_nAimPitchPoseParam = LookupPoseParameter( "aim_pitch" ); + + gm_nBodyYawPoseParam = LookupPoseParameter( "body_yaw" ); + gm_nBodyPitchPoseParam = LookupPoseParameter( "body_pitch" ); + + gm_nTopGunAttachment = LookupAttachment( "top_eye" ); + gm_nBottomGunAttachment = LookupAttachment( "bottom_eye" ); + gm_nStaggerYawPoseParam = LookupAttachment( "stagger_yaw" ); + + gm_nHeadCenterAttachment = LookupAttachment( "head_center" ); + gm_nHeadBottomAttachment = LookupAttachment( "head_radius_measure" ); + + // Measure the radius of the head. + Vector vecHeadCenter; + Vector vecHeadBottom; + GetAttachment( gm_nHeadCenterAttachment, vecHeadCenter ); + GetAttachment( gm_nHeadBottomAttachment, vecHeadBottom ); + gm_flHeadRadius = ( vecHeadCenter - vecHeadBottom ).Length(); + + int nSequence = SelectWeightedSequence( ACT_HUNTER_RANGE_ATTACK2_UNPLANTED ); + gm_nUnplantedNode = GetEntryNode( nSequence ); + + nSequence = SelectWeightedSequence( ACT_RANGE_ATTACK2 ); + gm_nPlantedNode = GetEntryNode( nSequence ); + + CollisionProp()->SetSurroundingBoundsType( USE_HITBOXES ); +} + + +//----------------------------------------------------------------------------- +// Shuts down looping sounds when we are killed in combat or deleted. +//----------------------------------------------------------------------------- +void CNPC_Hunter::StopLoopingSounds() +{ + BaseClass::StopLoopingSounds(); + + //if ( m_pGunFiringSound ) + //{ + // CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + // controller.SoundDestroy( m_pGunFiringSound ); + // m_pGunFiringSound = NULL; + //} +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::OnRestore() +{ + BaseClass::OnRestore(); + SetupGlobalModelData(); + CreateVPhysics(); + + if ( IsBleeding() ) + { + StartBleeding(); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::IdleSound() +{ + if ( HasCondition( COND_LOST_ENEMY ) ) + { + EmitSound( "NPC_Hunter.Scan" ); + } + else + { + EmitSound( "NPC_Hunter.Idle" ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::ShouldPlayIdleSound() +{ + if ( random->RandomInt(0, 99) == 0 && !HasSpawnFlags( SF_NPC_GAG ) ) + return true; + + return false; +} + + +//----------------------------------------------------------------------------- +// Stay facing our enemy when close enough. +//----------------------------------------------------------------------------- +bool CNPC_Hunter::OverrideMoveFacing( const AILocalMoveGoal_t &move, float flInterval ) +{ + if ( GetActivity() == ACT_TRANSITION ) + { + // No turning while in transitions. + return true; + } + + bool bSideStepping = IsCurSchedule( SCHED_HUNTER_SIDESTEP, false ); + + // FIXME: this will break scripted sequences that walk when they have an enemy + if ( GetEnemy() && + ( bSideStepping || + ( ( ( GetNavigator()->GetMovementActivity() == ACT_RUN ) || ( GetNavigator()->GetMovementActivity() == ACT_WALK ) ) && + !IsCurSchedule( SCHED_HUNTER_TAKE_COVER_FROM_ENEMY, false ) ) ) ) + { + Vector vecEnemyLKP = GetEnemyLKP(); + + // Face my enemy if we're close enough + if ( bSideStepping || UTIL_DistApprox( vecEnemyLKP, GetAbsOrigin() ) < HUNTER_FACE_ENEMY_DIST ) + { + AddFacingTarget( GetEnemy(), vecEnemyLKP, 1.0, 0.2 ); + } + } + + return BaseClass::OverrideMoveFacing( move, flInterval ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::PostNPCInit() +{ + BaseClass::PostNPCInit(); + + IPhysicsObject *pPhysObject = VPhysicsGetObject(); + Assert( pPhysObject ); + if ( pPhysObject ) + { + pPhysObject->SetMass( 600.0 ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::Activate() +{ + BaseClass::Activate(); + + s_iszStriderBusterClassname = AllocPooledString( "weapon_striderbuster" ); + s_iszStriderClassname = AllocPooledString( "npc_strider" ); + s_iszMagnadeClassname = AllocPooledString( "npc_grenade_magna" ); + s_iszPhysPropClassname = AllocPooledString( "prop_physics" ); + s_iszHuntersToRunOver = AllocPooledString( "hunters_to_run_over" ); + + // If no one has initialized the hunters to run over counter, just zero it out. + if ( !GlobalEntity_IsInTable( s_iszHuntersToRunOver ) ) + { + GlobalEntity_Add( s_iszHuntersToRunOver, gpGlobals->mapname, GLOBAL_ON ); + GlobalEntity_SetCounter( s_iszHuntersToRunOver, 0 ); + } + + CMissile::AddCustomDetonator( this, ( GetHullMaxs().AsVector2D() - GetHullMins().AsVector2D() ).Length() * 0.5, GetHullHeight() ); + + SetupGlobalModelData(); + + if ( gm_flMinigunDistZ == 0 ) + { + // Have to create a virgin hunter to ensure proper pose + CNPC_Hunter *pHunter = (CNPC_Hunter *)CreateEntityByName( "npc_hunter" ); + Assert(pHunter); + pHunter->Spawn(); + + pHunter->SetActivity( ACT_WALK ); + pHunter->InvalidateBoneCache(); + + // Currently just using the gun for the vertical component! + Vector defEyePos; + pHunter->GetAttachment( "minigunbase", defEyePos ); + gm_flMinigunDistZ = defEyePos.z - pHunter->GetAbsOrigin().z; + + Vector position; + pHunter->GetAttachment( gm_nTopGunAttachment, position ); + VectorITransform( position, pHunter->EntityToWorldTransform(), gm_vecLocalRelativePositionMinigun ); + UTIL_Remove( pHunter ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::UpdateOnRemove() +{ + CMissile::RemoveCustomDetonator( this ); + BaseClass::UpdateOnRemove(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +Class_T CNPC_Hunter::Classify() +{ + return CLASS_COMBINE_HUNTER; +} + +//----------------------------------------------------------------------------- +// Compensate for the hunter's long legs by moving the bodytarget up to his head. +//----------------------------------------------------------------------------- +Vector CNPC_Hunter::BodyTarget( const Vector &posSrc, bool bNoisy /*= true*/ ) +{ + Vector vecResult; + QAngle vecAngle; + GetAttachment( gm_nHeadCenterAttachment, vecResult, vecAngle ); + + if ( bNoisy ) + { + float rand1 = random->RandomFloat( 0, gm_flHeadRadius ) + random->RandomFloat( 0, gm_flHeadRadius ); + return vecResult + RandomVector( -rand1, rand1 ); + } + + return vecResult; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::DrawDebugTextOverlays() +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + EntityText( text_offset, CFmtStr("%s", m_bPlanted ? "Planted" : "Unplanted" ), 0 ); + text_offset++; + + EntityText( text_offset, CFmtStr("Eye state: %d", m_eEyeState ), 0 ); + text_offset++; + + if( IsUsingSiegeTargets() ) + { + EntityText( text_offset, CFmtStr("Next Siege Attempt:%f", m_flTimeNextSiegeTargetAttack - gpGlobals->curtime ), 0 ); + text_offset++; + } + } + + return text_offset; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::LockBothEyes( float flDuration ) +{ + m_eEyeState = HUNTER_EYE_STATE_BOTH_LOCKED; + m_EyeSwitchTimer.Set( flDuration ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::UnlockBothEyes( float flDuration ) +{ + m_eEyeState = HUNTER_EYE_STATE_BOTH_UNLOCKED; + m_EyeSwitchTimer.Set( flDuration ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::OnChangeActivity( Activity eNewActivity ) +{ + m_EyeSwitchTimer.Force(); + + BaseClass::OnChangeActivity( eNewActivity ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::UpdateEyes() +{ + // If the eyes are controlled by a script, do nothing. + if ( GetState() == NPC_STATE_SCRIPT ) + return; + + if ( m_EyeSwitchTimer.Expired() ) + { + RemoveActorFromScriptedScenes( this, false ); + + if ( GetActivity() == ACT_IDLE ) + { + // Idles have eye motion baked in. + m_eEyeState = HUNTER_EYE_STATE_BOTH_LOCKED; + } + else if ( GetEnemy() == NULL ) + { + m_eEyeState = HUNTER_EYE_STATE_BOTH_UNLOCKED; + } + else if ( m_eEyeState == HUNTER_EYE_STATE_BOTH_LOCKED ) + { + if ( random->RandomInt( 0, 1 ) == 0 ) + { + m_eEyeState = HUNTER_EYE_STATE_TOP_LOCKED; + } + else + { + m_eEyeState = HUNTER_EYE_STATE_BOTTOM_LOCKED; + } + } + else if ( m_eEyeState == HUNTER_EYE_STATE_TOP_LOCKED ) + { + m_eEyeState = HUNTER_EYE_STATE_BOTTOM_LOCKED; + } + else if ( m_eEyeState == HUNTER_EYE_STATE_BOTTOM_LOCKED ) + { + m_eEyeState = HUNTER_EYE_STATE_TOP_LOCKED; + } + + if ( ( m_eEyeState == HUNTER_EYE_STATE_BOTTOM_LOCKED ) || ( m_eEyeState == HUNTER_EYE_STATE_BOTH_UNLOCKED ) ) + { + SetExpression( "scenes/npc/hunter/hunter_eyedarts_top.vcd" ); + } + + if ( ( m_eEyeState == HUNTER_EYE_STATE_TOP_LOCKED ) || ( m_eEyeState == HUNTER_EYE_STATE_BOTH_UNLOCKED ) ) + { + SetExpression( "scenes/npc/hunter/hunter_eyedarts_bottom.vcd" ); + } + + m_EyeSwitchTimer.Set( random->RandomFloat( 1.0f, 3.0f ) ); + } + + /*Vector vecEyePos; + Vector vecEyeDir; + + GetAttachment( gm_nTopGunAttachment, vecEyePos, &vecEyeDir ); + NDebugOverlay::Line( vecEyePos, vecEyePos + vecEyeDir * 36, 255, 0, 0, 0, 0.1 ); + + GetAttachment( gm_nBottomGunAttachment, vecEyePos, &vecEyeDir ); + NDebugOverlay::Line( vecEyePos, vecEyePos + vecEyeDir * 36, 255, 0, 0, 0, 0.1 );*/ +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::NPCThink() +{ + BaseClass::NPCThink(); + + // Update our planted/unplanted state. + m_bPlanted = ( GetEntryNode( GetSequence() ) == gm_nPlantedNode ); + + UpdateAim(); + UpdateEyes(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::PrescheduleThink() +{ + BaseClass::PrescheduleThink(); + + if ( m_BeginFollowDelay.Expired() ) + { + FollowStrider( STRING( m_iszFollowTarget ) ); + m_BeginFollowDelay.Stop(); + } + + m_EscortBehavior.CheckBreakEscort(); + + // If we're being blinded by the flashlight, see if we should stop + if ( m_bFlashlightInEyes ) + { + if ( m_flPupilDilateTime < gpGlobals->curtime ) + { + CBasePlayer *pPlayer = UTIL_PlayerByIndex( 1 ); + if ( ( pPlayer && !pPlayer->IsIlluminatedByFlashlight( this, NULL ) ) || !PlayerFlashlightOnMyEyes( pPlayer ) ) + { + //Msg( "NOT SHINING FLASHLIGHT ON ME\n" ); + + // Remove the actor from the flashlight scene + RemoveActorFromScriptedScenes( this, true, false, "scenes/npc/hunter/hunter_eyeclose.vcd" ); + m_bFlashlightInEyes = false; + } + } + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::GatherChargeConditions() +{ + ClearCondition( COND_HUNTER_CAN_CHARGE_ENEMY ); + + if ( !hunter_charge.GetBool() ) + return; + + if ( !GetEnemy() ) + return; + + if ( GetHintGroup() != NULL_STRING ) + return; + + if ( !HasCondition( COND_SEE_ENEMY ) ) + return; + + if ( !hunter_charge_test.GetBool() && gpGlobals->curtime < m_flNextChargeTime ) + return; + + // No charging Alyx or Barney + if( GetEnemy()->Classify() == CLASS_PLAYER_ALLY_VITAL ) + return; + + if ( m_EscortBehavior.GetEscortTarget() && GetEnemy()->MyCombatCharacterPointer() && !GetEnemy()->MyCombatCharacterPointer()->FInViewCone( this ) ) + return; + + if ( ShouldCharge( GetAbsOrigin(), GetEnemy()->GetAbsOrigin(), true, false ) ) + { + SetCondition( COND_HUNTER_CAN_CHARGE_ENEMY ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::GatherConditions() +{ + GatherIndoorOutdoorConditions(); + GatherChargeConditions(); + + BaseClass::GatherConditions(); + + // Enemy LKP that doesn't get updated by the free knowledge code. + // Used for shooting at where our enemy was when we can't see them. + ClearCondition( COND_HUNTER_INCOMING_VEHICLE ); + if ( m_IgnoreVehicleTimer.Expired() && GetEnemy() && HasCondition( COND_SEE_ENEMY ) ) + { + CBaseEntity *pVehicle = GetEnemyVehicle(); + if ( ( pVehicle ) && ( GlobalEntity_GetCounter( s_iszHuntersToRunOver ) <= 0 ) ) + { + static float timeDrawnArrow; + + // Extrapolate the position of the vehicle and see if it's heading toward us. + float predictTime = hunter_dodge_warning.GetFloat(); + Vector2D vecFuturePos = pVehicle->GetAbsOrigin().AsVector2D() + pVehicle->GetSmoothedVelocity().AsVector2D() * predictTime; + if ( pVehicle->GetSmoothedVelocity().LengthSqr() > Square( 200 ) ) + { + float t = 0; + Vector2D vDirMovement = pVehicle->GetSmoothedVelocity().AsVector2D(); + if ( hunter_dodge_debug.GetBool() ) + { + NDebugOverlay::Line( pVehicle->GetAbsOrigin(), pVehicle->GetAbsOrigin() + pVehicle->GetSmoothedVelocity(), 255, 255, 255, true, .1 ); + } + vDirMovement.NormalizeInPlace(); + Vector2D vDirToHunter = GetAbsOrigin().AsVector2D() - pVehicle->GetAbsOrigin().AsVector2D(); + vDirToHunter.NormalizeInPlace(); + if ( DotProduct2D( vDirMovement, vDirToHunter ) > hunter_dodge_warning_cone.GetFloat() && + CalcDistanceSqrToLine2D( GetAbsOrigin().AsVector2D(), pVehicle->GetAbsOrigin().AsVector2D(), vecFuturePos, &t ) < Square( hunter_dodge_warning_width.GetFloat() * .5 ) && + t > 0.0 && t < 1.0 ) + { + if ( fabs( predictTime - hunter_dodge_warning.GetFloat() ) < .05 || random->RandomInt( 0, 3 ) ) + { + SetCondition( COND_HUNTER_INCOMING_VEHICLE ); + } + else + { + if ( hunter_dodge_debug. GetBool() ) + { + Msg( "Hunter %d failing dodge (ignore)\n", entindex() ); + } + } + + if ( hunter_dodge_debug. GetBool() ) + { + NDebugOverlay::Cross3D( EyePosition(), 100, 255, 255, 255, true, .1 ); + if ( timeDrawnArrow != gpGlobals->curtime ) + { + timeDrawnArrow = gpGlobals->curtime; + Vector vEndpoint( vecFuturePos.x, vecFuturePos.y, UTIL_GetLocalPlayer()->WorldSpaceCenter().z - 24 ); + NDebugOverlay::HorzArrow( UTIL_GetLocalPlayer()->WorldSpaceCenter() - Vector(0, 0, 24), vEndpoint, hunter_dodge_warning_width.GetFloat(), 255, 0, 0, 64, true, .1 ); + } + } + } + else if ( hunter_dodge_debug.GetBool() ) + { + if ( t <= 0 ) + { + NDebugOverlay::Cross3D( EyePosition(), 100, 0, 0, 255, true, .1 ); + } + else + { + NDebugOverlay::Cross3D( EyePosition(), 100, 0, 255, 255, true, .1 ); + } + } + } + else if ( hunter_dodge_debug.GetBool() ) + { + NDebugOverlay::Cross3D( EyePosition(), 100, 0, 255, 0, true, .1 ); + } + if ( hunter_dodge_debug. GetBool() ) + { + if ( timeDrawnArrow != gpGlobals->curtime ) + { + timeDrawnArrow = gpGlobals->curtime; + Vector vEndpoint( vecFuturePos.x, vecFuturePos.y, UTIL_GetLocalPlayer()->WorldSpaceCenter().z - 24 ); + NDebugOverlay::HorzArrow( UTIL_GetLocalPlayer()->WorldSpaceCenter() - Vector(0, 0, 24), vEndpoint, hunter_dodge_warning_width.GetFloat(), 127, 127, 127, 64, true, .1 ); + } + } + + } + + m_vecEnemyLastSeen = GetEnemy()->GetAbsOrigin(); + } + + if( !HasCondition(COND_ENEMY_OCCLUDED) ) + { + // m_flTimeSawEnemyAgain always tells us what time I first saw this + // enemy again after some period of not seeing them. This is used to + // compute how long the enemy has been visible to me THIS TIME. + // Every time I lose sight of the enemy this time is set invalid until + // I see the enemy again and record that time. + if( m_flTimeSawEnemyAgain == HUNTER_SEE_ENEMY_TIME_INVALID ) + { + m_flTimeSawEnemyAgain = gpGlobals->curtime; + } + } + else + { + m_flTimeSawEnemyAgain = HUNTER_SEE_ENEMY_TIME_INVALID; + } + + ManageSiegeTargets(); +} + +//----------------------------------------------------------------------------- +// Search all entities in the map +//----------------------------------------------------------------------------- +void CNPC_Hunter::CollectSiegeTargets() +{ + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, m_iszSiegeTargetName ); + + while( pTarget != NULL ) + { + if( pTarget->Classify() == CLASS_BULLSEYE ) + { + m_pSiegeTargets.AddToTail( pTarget ); + } + + pTarget = gEntList.FindEntityByName( pTarget, m_iszSiegeTargetName ); + }; + + if( m_pSiegeTargets.Count() < 1 ) + { + m_iszSiegeTargetName = NULL_STRING; // And stop trying! + } +} + +//----------------------------------------------------------------------------- +// For use when Hunters are outside and the player is inside a structure +// Create a temporary bullseye in a location that makes it seem like +// I am aware of the location of a player I cannot see. (Then fire at +// at this bullseye, thus laying 'siege' to the part of the building he +// is in.) The locations are copied from suitable info_target entities. +// (these should be placed in exterior windows and doorways so that +// the Hunter fires into the building through these apertures) +//----------------------------------------------------------------------------- +void CNPC_Hunter::ManageSiegeTargets() +{ + if( gpGlobals->curtime < m_flTimeNextSiegeTargetAttack ) + return; + + if( m_pSiegeTargets.Count() == 0 ) + { + // If my list of siege targets is empty, go and cache all of them now + // so that I don't have to search the world every time. + CollectSiegeTargets(); + + if( m_pSiegeTargets.Count() == 0 ) + return; + } + + m_flTimeNextSiegeTargetAttack = gpGlobals->curtime + (hunter_siege_frequency.GetFloat() * RandomFloat( 0.8f, 1.2f) ); + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + + // Start by assuming we are not going to create a siege target + bool bCreateSiegeTarget = false; + if( GetEnemy() == NULL ) + { + // If I have no enemy at all, give it a try. + bCreateSiegeTarget = true; + } + + if( bCreateSiegeTarget ) + { + // We've decided that the situation calls for a siege target. So, we dig through all of my siege targets and + // take the closest one to the player that the player can see! (Obey they bullseye's FOV) + float flClosestDistSqr = Square( 1200.0f ); // Only use siege targets within 100 feet of player + CBaseEntity *pSiegeTargetLocation = NULL; + int iTraces = 0; + for( int i = 0 ; i < m_pSiegeTargets.Count() ; i++ ) + { + CBaseEntity *pCandidate = m_pSiegeTargets[i]; + if ( pCandidate == NULL ) + continue; + + float flDistSqr = pCandidate->GetAbsOrigin().DistToSqr(pPlayer->GetAbsOrigin()); + + if( flDistSqr < flClosestDistSqr ) + { + // CollectSiegeTargets() guarantees my list is populated only with bullseye entities. + CNPC_Bullseye *pBullseye = dynamic_cast<CNPC_Bullseye*>(pCandidate); + if( !pBullseye->FInViewCone(this) ) + continue; + + if( pPlayer->FVisible(pCandidate) ) + { + iTraces++;// Only counting these as a loose perf measurement + flClosestDistSqr = flDistSqr; + pSiegeTargetLocation = pCandidate; + } + } + } + + if( pSiegeTargetLocation != NULL ) + { + // Ditch any leftover siege target. + KillCurrentSiegeTarget(); + + // Create a bullseye that will live for 20 seconds. If we can't attack it within 20 seconds, it's probably + // out of reach anyone, so have it clean itself up after that long. + CBaseEntity *pSiegeTarget = CreateCustomTarget( pSiegeTargetLocation->GetAbsOrigin(), 20.0f ); + pSiegeTarget->SetName( MAKE_STRING("siegetarget") ); + + m_hCurrentSiegeTarget.Set( pSiegeTarget ); + + AddEntityRelationship( pSiegeTarget, D_HT, 1 ); + GetEnemies()->UpdateMemory( GetNavigator()->GetNetwork(), pSiegeTarget, pSiegeTarget->GetAbsOrigin(), 0.0f, true ); + AI_EnemyInfo_t *pMemory = GetEnemies()->Find( pSiegeTarget ); + + if( pMemory ) + { + // Pretend we've known about this target longer than we really have so that our AI doesn't waste time running ALERT schedules. + pMemory->timeFirstSeen = gpGlobals->curtime - 5.0f; + pMemory->timeLastSeen = gpGlobals->curtime - 1.0f; + } + } + } +} + +//----------------------------------------------------------------------------- +// Destroy the bullseye that we're using as a temporary target +//----------------------------------------------------------------------------- +void CNPC_Hunter::KillCurrentSiegeTarget() +{ + if ( m_hCurrentSiegeTarget ) + { + GetEnemies()->ClearMemory( m_hCurrentSiegeTarget ); + + UTIL_Remove( m_hCurrentSiegeTarget ); + m_hCurrentSiegeTarget.Set( NULL ); + } +} + +//----------------------------------------------------------------------------- +// Return true if this NPC can hear the specified sound +//----------------------------------------------------------------------------- +bool CNPC_Hunter::QueryHearSound( CSound *pSound ) +{ + if ( pSound->SoundContext() & SOUND_CONTEXT_EXCLUDE_COMBINE ) + return false; + + if ( pSound->SoundContext() & SOUND_CONTEXT_PLAYER_VEHICLE ) + return false; + + return BaseClass::QueryHearSound( pSound ); +} + + +//----------------------------------------------------------------------------- +// This is a fairly bogus heuristic right now, but it works on 06a and 12 (sjb) +// +// Better options: Trace infinitely and check the material we hit for sky +// Put some leaf info in the BSP +// Use volumes in the levels? (yucky for designers) +//----------------------------------------------------------------------------- +// TODO: use this or nuke it! +void CNPC_Hunter::GatherIndoorOutdoorConditions() +{ + // Check indoor/outdoor before calling base class, since base class calls our + // RangeAttackConditions() functions, and we want those functions to know + // whether we're indoors or out. + trace_t tr; + + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, 40.0f * 12.0f ), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + if( tr.fraction < 1.0f ) + { + SetCondition( COND_HUNTER_IS_INDOORS ); + } + else + { + ClearCondition( COND_HUNTER_IS_INDOORS ); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::BuildScheduleTestBits() +{ + BaseClass::BuildScheduleTestBits(); + + if ( m_lifeState != LIFE_ALIVE ) + { + return; + } + + // Our range attack is uninterruptable for the first few seconds. + if ( IsCurSchedule( SCHED_HUNTER_RANGE_ATTACK2, false ) && ( gpGlobals->curtime < m_flShootAllowInterruptTime ) ) + { + ClearCustomInterruptConditions(); + SetCustomInterruptCondition( COND_HEAVY_DAMAGE ); + } + else if ( IsCurSchedule( SCHED_HUNTER_RANGE_ATTACK2, false ) && ( GetActivity() == ACT_TRANSITION ) ) + { + // Don't stop unplanting just because we can range attack again. + ClearCustomInterruptCondition( COND_CAN_RANGE_ATTACK1 ); + ClearCustomInterruptCondition( COND_CAN_RANGE_ATTACK2 ); + } + else if ( !IsInLargeOutdoorMap() && IsCurSchedule( SCHED_HUNTER_FLANK_ENEMY, false ) && GetEnemy() != NULL ) + { + if( HasCondition(COND_CAN_RANGE_ATTACK2) && m_flTimeSawEnemyAgain != HUNTER_SEE_ENEMY_TIME_INVALID ) + { + if( (gpGlobals->curtime - m_flTimeSawEnemyAgain) >= 2.0f ) + { + // When we're running flank behavior, wait a moment AFTER being able to see the enemy before + // breaking my schedule to range attack. This helps assure that the hunter gets well inside + // the room before stopping to attack. Otherwise the Hunter may stop immediately in the doorway + // and stop the progress of any hunters behind it. + SetCustomInterruptCondition( COND_CAN_RANGE_ATTACK2 ); + } + } + } + + // If our enemy is anything but a striderbuster, drop everything if we see one. + if ( !IsStriderBuster( GetEnemy() ) ) + { + SetCustomInterruptCondition( COND_HUNTER_SEE_STRIDERBUSTER ); + } + + // If we're not too busy, allow ourselves to ACK found enemy signals. + if ( !GetEnemy() ) + { + SetCustomInterruptCondition( COND_HUNTER_SQUADMATE_FOUND_ENEMY ); + } + + // Interrupt everything if we need to dodge. + if ( !IsCurSchedule( SCHED_HUNTER_DODGE, false ) && + !IsCurSchedule( SCHED_HUNTER_STAGGER, false ) && + !IsCurSchedule( SCHED_ALERT_FACE_BESTSOUND, false ) ) + { + SetCustomInterruptCondition( COND_HUNTER_INCOMING_VEHICLE ); + SetCustomInterruptCondition( COND_HEAR_PHYSICS_DANGER ); + SetCustomInterruptCondition( COND_HUNTER_FORCED_DODGE ); + } + + // Always interrupt on a flank command. + SetCustomInterruptCondition( COND_HUNTER_FORCED_FLANK_ENEMY ); + + // Always interrupt if staggered. + SetCustomInterruptCondition( COND_HUNTER_STAGGERED ); + + // Always interrupt if hit by a sticky bomb. + SetCustomInterruptCondition( COND_HUNTER_HIT_BY_STICKYBOMB ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +static bool IsMovablePhysicsObject( CBaseEntity *pEntity ) +{ + return pEntity && pEntity->GetMoveType() == MOVETYPE_VPHYSICS && pEntity->VPhysicsGetObject() && pEntity->VPhysicsGetObject()->IsMoveable(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +NPC_STATE CNPC_Hunter::SelectIdealState() +{ + switch ( m_NPCState ) + { + case NPC_STATE_COMBAT: + { + if ( GetEnemy() == NULL ) + { + if ( !HasCondition( COND_ENEMY_DEAD ) && !hunter_disable_patrol.GetBool() ) + { + // Lost track of my enemy. Patrol. + SetCondition( COND_HUNTER_SHOULD_PATROL ); + } + + return NPC_STATE_ALERT; + } + else if ( HasCondition( COND_ENEMY_DEAD ) ) + { + // dvs: TODO: announce enemy kills? + //AnnounceEnemyKill(GetEnemy()); + } + } + + default: + { + return BaseClass::SelectIdealState(); + } + } + + return GetIdealState(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::ShouldCharge( const Vector &startPos, const Vector &endPos, bool useTime, bool bCheckForCancel ) +{ + // Must have a target + if ( !GetEnemy() ) + return false; + + // Don't check the distance once we start charging + if ( !bCheckForCancel && !hunter_charge_test.GetBool() ) + { + float distance = ( startPos.AsVector2D() - endPos.AsVector2D() ).LengthSqr(); + + // Must be within our tolerance range + if ( ( distance < Square(HUNTER_CHARGE_MIN) ) || ( distance > Square(HUNTER_CHARGE_MAX) ) ) + return false; + } + + // FIXME: We'd like to exclude small physics objects from this check! + + // We only need to hit the endpos with the edge of our bounding box + Vector vecDir = endPos - startPos; + VectorNormalize( vecDir ); + float flWidth = WorldAlignSize().x * 0.5; + Vector vecTargetPos = endPos - (vecDir * flWidth); + + // See if we can directly move there + AIMoveTrace_t moveTrace; + GetMoveProbe()->MoveLimit( NAV_GROUND, startPos, vecTargetPos, MASK_NPCSOLID_BRUSHONLY, GetEnemy(), &moveTrace ); + + // Draw the probe + if ( g_debug_hunter_charge.GetInt() == 1 ) + { + Vector enemyDir = (vecTargetPos - startPos); + float enemyDist = VectorNormalize( enemyDir ); + + NDebugOverlay::BoxDirection( startPos, GetHullMins(), GetHullMaxs() + Vector(enemyDist,0,0), enemyDir, 0, 255, 0, 8, 1.0f ); + } + + // If we're not blocked, charge + if ( IsMoveBlocked( moveTrace ) ) + { + // Don't allow it if it's too close to us + if ( UTIL_DistApprox( WorldSpaceCenter(), moveTrace.vEndPosition ) < HUNTER_CHARGE_MIN ) + return false; + + // Allow some special cases to not block us + if ( moveTrace.pObstruction != NULL ) + { + // If we've hit the world, see if it's a cliff + if ( moveTrace.pObstruction == GetContainingEntity( INDEXENT(0) ) ) + { + // Can't be too far above/below the target + if ( fabs( moveTrace.vEndPosition.z - vecTargetPos.z ) > StepHeight() ) + return false; + + // Allow it if we got pretty close + if ( UTIL_DistApprox( moveTrace.vEndPosition, vecTargetPos ) < 64 ) + return true; + } + + // Hit things that will take damage + if ( moveTrace.pObstruction->m_takedamage != DAMAGE_NO ) + return true; + + // Hit things that will move + if ( moveTrace.pObstruction->GetMoveType() == MOVETYPE_VPHYSICS ) + return true; + } + + return false; + } + + float zDelta = endPos.z - moveTrace.vEndPosition.z; + if ( fabsf(zDelta) > GetHullHeight() * 0.7) + { + return false; + } + + return true; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter *pSourceEnt) +{ + if ( ( pSourceEnt != this ) && ( interactionType == g_interactionHunterFoundEnemy ) ) + { + SetCondition( COND_HUNTER_SQUADMATE_FOUND_ENEMY ); + return true; + } + + return BaseClass::HandleInteraction( interactionType, data, pSourceEnt ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::SelectCombatSchedule() +{ + // If we're here with no enemy, patrol and hope we find one. + CBaseEntity *pEnemy = GetEnemy(); + if ( pEnemy == NULL ) + { + if ( !hunter_disable_patrol.GetBool() ) + return SCHED_HUNTER_PATROL_RUN; + else + return SCHED_ALERT_STAND; + } + + if ( hunter_flechette_test.GetBool() ) + { + if ( HasCondition( COND_CAN_RANGE_ATTACK2 ) ) + { + return SCHED_HUNTER_RANGE_ATTACK2; + } + return SCHED_COMBAT_FACE; + } + + bool bStriderBuster = IsStriderBuster( pEnemy ); + if ( bStriderBuster ) + { + if ( gpGlobals->curtime - CAI_HunterEscortBehavior::gm_flLastDefendSound > 10.0 ) + { + EmitSound( "NPC_Hunter.DefendStrider" ); + CAI_HunterEscortBehavior::gm_flLastDefendSound = gpGlobals->curtime; + } + + if ( HasCondition( COND_CAN_RANGE_ATTACK2 ) || HasCondition( COND_NOT_FACING_ATTACK ) ) + { + return SCHED_HUNTER_RANGE_ATTACK2; + } + return SCHED_ESTABLISH_LINE_OF_FIRE; + } + + // Certain behaviors, like flanking and melee attacks, only make sense on visible, + // corporeal enemies (NOT bullseyes). + bool bIsCorporealEnemy = IsCorporealEnemy( pEnemy ); + + // Take a quick swipe at our enemy if able to do so. + if ( bIsCorporealEnemy && HasCondition( COND_CAN_MELEE_ATTACK1 ) ) + { + return SCHED_HUNTER_MELEE_ATTACK1; + } + + // React to newly acquired enemies. + if ( bIsCorporealEnemy && HasCondition( COND_NEW_ENEMY ) ) + { + AI_EnemyInfo_t *pEnemyInfo = GetEnemies()->Find( pEnemy ); + + if ( GetSquad() && pEnemyInfo && ( pEnemyInfo->timeFirstSeen == pEnemyInfo->timeAtFirstHand ) ) + { + GetSquad()->BroadcastInteraction( g_interactionHunterFoundEnemy, NULL, this ); + + // First contact for my squad. + return SCHED_HUNTER_FOUND_ENEMY; + } + } + + if ( HasCondition( COND_HUNTER_SQUADMATE_FOUND_ENEMY ) ) + { + // A squadmate found an enemy. Respond to their call. + return SCHED_HUNTER_FOUND_ENEMY_ACK; + } + + // Fire a flechette volley. Ignore squad slots if we're attacking a striderbuster. + // See if there is an opportunity to charge. + if ( !bStriderBuster && bIsCorporealEnemy && HasCondition( COND_HUNTER_CAN_CHARGE_ENEMY ) ) + { + if ( hunter_charge_test.GetBool() || random->RandomInt( 1, 100 ) < hunter_charge_pct.GetInt() ) + { + if ( hunter_charge_test.GetBool() || OccupyStrategySlot( SQUAD_SLOT_HUNTER_CHARGE ) ) + { + return SCHED_HUNTER_CHARGE_ENEMY; + } + } + } + + if ( HasCondition( COND_CAN_RANGE_ATTACK2 ) ) + { + if ( bStriderBuster || CountRangedAttackers() < hunter_flechette_max_concurrent_volleys.GetInt() ) + { + DelayRangedAttackers( hunter_flechette_volley_start_min_delay.GetFloat(), hunter_flechette_volley_start_max_delay.GetFloat(), true ); + return SCHED_HUNTER_RANGE_ATTACK2; + } + } + + if ( pEnemy->GetGroundEntity() == this ) + { + return SCHED_HUNTER_MELEE_ATTACK1; + } + + if ( HasCondition( COND_TOO_CLOSE_TO_ATTACK ) ) + { + return SCHED_MOVE_AWAY_FROM_ENEMY; + } + + // Sidestep every so often if my enemy is nearby and facing me. +/* + if ( gpGlobals->curtime > m_flNextSideStepTime ) + { + if ( HasCondition( COND_ENEMY_FACING_ME ) && ( UTIL_DistApprox( GetEnemy()->GetAbsOrigin(), GetAbsOrigin() ) < HUNTER_FACE_ENEMY_DIST ) ) + { + m_flNextSideStepTime = gpGlobals->curtime + random->RandomFloat( 1.0f, 3.0f ); + return SCHED_HUNTER_SIDESTEP; + } + } +*/ + if ( HasCondition( COND_HEAVY_DAMAGE ) && ( gpGlobals->curtime > m_flNextSideStepTime ) ) + { + m_flNextSideStepTime = gpGlobals->curtime + random->RandomFloat( 1.0f, 3.0f ); + return SCHED_HUNTER_SIDESTEP; + } + + if ( !bStriderBuster && bIsCorporealEnemy ) + { + if ( HasCondition( COND_HUNTER_CAN_CHARGE_ENEMY ) ) + { + if ( OccupyStrategySlot( SQUAD_SLOT_HUNTER_CHARGE ) ) + { + return SCHED_HUNTER_CHARGE_ENEMY; + } +/* + else + { + return SCHED_HUNTER_SIDESTEP; + } +*/ + } + + // Try to be a flanker. + if ( ( NumHuntersInMySquad() > 1 ) && OccupyStrategySlotRange( SQUAD_SLOT_HUNTER_FLANK_FIRST, SQUAD_SLOT_HUNTER_FLANK_LAST ) ) + { + return SCHED_HUNTER_FLANK_ENEMY; + } + } + + // Can't see my enemy. + if ( HasCondition( COND_ENEMY_OCCLUDED ) || HasCondition( COND_ENEMY_TOO_FAR ) || HasCondition( COND_TOO_FAR_TO_ATTACK ) || HasCondition( COND_NOT_FACING_ATTACK ) ) + { + return SCHED_HUNTER_CHASE_ENEMY; + } + + if ( HasCondition( COND_HUNTER_CANT_PLANT ) ) + { + return SCHED_ESTABLISH_LINE_OF_FIRE; + } + + //if ( HasCondition( COND_ENEMY_OCCLUDED ) && IsCurSchedule( SCHED_RANGE_ATTACK1, false ) ) + //{ + // return SCHED_HUNTER_COMBAT_FACE; + //} + + return SCHED_HUNTER_CHANGE_POSITION; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::SelectSiegeSchedule() +{ + bool bHasEnemy = (GetEnemy() != NULL); + + if( bHasEnemy ) + { + // We have an enemy, so we should be making every effort to attack it. + if( !HasCondition(COND_SEE_ENEMY) || !HasCondition(COND_CAN_RANGE_ATTACK2) ) + return SCHED_ESTABLISH_LINE_OF_FIRE; + + if( HasCondition(COND_CAN_RANGE_ATTACK2) ) + return SCHED_HUNTER_RANGE_ATTACK2; + + return SCHED_HUNTER_SIEGE_STAND; + } + else + { + // Otherwise we are loitering in siege mode. Break line of sight with the player + // if they expose our position. + if( HasCondition( COND_SEE_PLAYER ) ) + return SCHED_HUNTER_CHANGE_POSITION_SIEGE; + } + + return SCHED_HUNTER_SIEGE_STAND; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::SelectSchedule() +{ + if ( hunter_stand_still.GetBool() ) + { + m_bPlanted = false; + return SCHED_IDLE_STAND; + } + + if ( HasCondition( COND_HUNTER_FORCED_DODGE ) ) + return SCHED_HUNTER_DODGE; + + if ( HasCondition( COND_HUNTER_NEW_HINTGROUP ) || ( GetHintGroup() != NULL_STRING && m_CheckHintGroupTimer.Expired() ) ) + { + CAI_Hint *pHint; + CHintCriteria criteria; + criteria.SetGroup( GetHintGroup() ); + criteria.SetFlag( bits_HINT_NODE_NEAREST ); + + if ( HasCondition( COND_HUNTER_NEW_HINTGROUP ) ) + { + ClearCondition( COND_HUNTER_NEW_HINTGROUP ); + if ( GetEnemy() ) + { + pHint = CAI_HintManager::FindHint( NULL, GetEnemy()->GetAbsOrigin(), criteria ); + } + else + { + pHint = CAI_HintManager::FindHint( GetAbsOrigin(), criteria ); + } + + if ( pHint ) + { + pHint->Lock( this ); + } + } + else + { + pHint = CAI_HintManager::FindHint( GetAbsOrigin(), criteria ); + if ( pHint ) + { + if ( (pHint->GetAbsOrigin() - GetAbsOrigin()).Length2DSqr() < Square( 20*12 ) ) + { + m_CheckHintGroupTimer.Set( 5 ); + pHint = NULL; + } + else + { + m_CheckHintGroupTimer.Set( 15 ); + } + } + } + + if ( pHint ) + { + SetHintNode( pHint ); + return SCHED_HUNTER_GOTO_HINT; + } + } + + if ( HasCondition( COND_HUNTER_INCOMING_VEHICLE ) ) + { + if ( m_RundownDelay.Expired() ) + { + int iRundownCounter = 0; + if ( GetSquad() ) + { + GetSquad()->GetSquadData( HUNTER_RUNDOWN_SQUADDATA, &iRundownCounter ); + } + + if ( iRundownCounter % 2 == 0 ) + { + for ( int i = 0; i < g_Hunters.Count(); i++ ) + { + if ( g_Hunters[i] != this ) + { + g_Hunters[i]->m_RundownDelay.Set( 3 ); + g_Hunters[i]->m_IgnoreVehicleTimer.Force(); + } + } + m_IgnoreVehicleTimer.Set( hunter_dodge_warning.GetFloat() * 4 ); + if ( hunter_dodge_debug.GetBool() ) + { + Msg( "Hunter %d rundown\n", entindex() ); + } + + if ( HasCondition( COND_SEE_ENEMY ) ) + { + if ( m_bPlanted && HasCondition( COND_CAN_RANGE_ATTACK2 ) ) + { + return SCHED_HUNTER_RANGE_ATTACK2; + } + else if ( random->RandomInt( 0, 1 ) ) + { + return SCHED_HUNTER_CHARGE_ENEMY; + } + else + { + return SCHED_MOVE_AWAY; + } + } + else + { + SetTarget( UTIL_GetLocalPlayer() ); + return SCHED_TARGET_FACE; + } + } + else + { + if ( hunter_dodge_debug.GetBool() ) + { + Msg( "Hunter %d safe from rundown\n", entindex() ); + } + for ( int i = 0; i < g_Hunters.Count(); i++ ) + { + g_Hunters[i]->m_RundownDelay.Set( 4 ); + g_Hunters[i]->m_IgnoreVehicleTimer.Force(); + } + if ( GetSquad() ) + { + GetSquad()->SetSquadData( HUNTER_RUNDOWN_SQUADDATA, iRundownCounter + 1 ); + } + } + } + + if ( HasCondition( COND_SEE_ENEMY ) ) + { + if ( hunter_dodge_debug.GetBool() ) + { + Msg( "Hunter %d try dodge\n", entindex() ); + } + return SCHED_HUNTER_DODGE; + } + else + { + SetTarget( UTIL_GetLocalPlayer() ); + return SCHED_TARGET_FACE; + } + + CSound *pBestSound = GetBestSound( SOUND_PHYSICS_DANGER ); + if ( pBestSound && ( pBestSound->SoundContext() & SOUND_CONTEXT_PLAYER_VEHICLE ) ) + { + return SCHED_ALERT_FACE_BESTSOUND; + } + } + + if ( HasCondition( COND_HUNTER_FORCED_FLANK_ENEMY ) ) + { + return SCHED_HUNTER_FLANK_ENEMY; + } + + if ( HasCondition( COND_HUNTER_STAGGERED ) /*|| HasCondition( COND_HUNTER_HIT_BY_STICKYBOMB )*/ ) + { + return SCHED_HUNTER_STAGGER; + } + + // Now that we're past all of the forced reactions to things, if we're running the siege + // behavior, go pick an appropriate siege schedule UNLESS we have an enemy. If we have + // an enemy, we should focus on attacking that enemy. + if( IsUsingSiegeTargets() ) + { + return SelectSiegeSchedule(); + } + + // back away if there's a magnade glued to my head. + if ( hunter_retreat_striderbusters.GetBool() /*&& GetEnemy() && ( GetEnemy()->IsPlayer() )*/ + && (m_hAttachedBusters.Count() > 0) + && m_fCorneredTimer < gpGlobals->curtime) + { + return SCHED_HUNTER_TAKE_COVER_FROM_ENEMY; + } + + if ( !BehaviorSelectSchedule() ) + { + switch ( GetState() ) + { + case NPC_STATE_IDLE: + { + return SCHED_HUNTER_PATROL; + } + + case NPC_STATE_ALERT: + { + if ( HasCondition( COND_HUNTER_SHOULD_PATROL ) ) + return SCHED_HUNTER_PATROL; + + break; + } + + case NPC_STATE_COMBAT: + { + return SelectCombatSchedule(); + } + } + } + + return BaseClass::SelectSchedule(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::TranslateSchedule( int scheduleType ) +{ + switch ( scheduleType ) + { + case SCHED_RANGE_ATTACK1: + { + return SCHED_HUNTER_RANGE_ATTACK1; + } + + case SCHED_RANGE_ATTACK2: + case SCHED_HUNTER_RANGE_ATTACK2: + { + if ( scheduleType == SCHED_RANGE_ATTACK2 ) + { + Msg( "HUNTER IGNORING SQUAD SLOTS\n" ); + } + + if ( IsStriderBuster( GetEnemy() ) ) + { + // Attack as FAST as possible. The point is to shoot down the buster. + return SCHED_HUNTER_RANGE_ATTACK2_VS_STRIDERBUSTER; + } + + return SCHED_HUNTER_RANGE_ATTACK2; + } + + case SCHED_MELEE_ATTACK1: + { + return SCHED_HUNTER_MELEE_ATTACK1; + } + + case SCHED_ESTABLISH_LINE_OF_FIRE_FALLBACK: + { + return SCHED_HUNTER_CHANGE_POSITION; + } + + case SCHED_ALERT_STAND: + { + if ( !hunter_disable_patrol.GetBool() ) + return SCHED_HUNTER_PATROL_RUN; + break; + } + + case SCHED_COMBAT_FACE: + { + return SCHED_HUNTER_COMBAT_FACE; + } + + case SCHED_HUNTER_PATROL: + { + if ( hunter_disable_patrol.GetBool() ) + { + return SCHED_IDLE_STAND; + } + break; + } + } + + return BaseClass::TranslateSchedule( scheduleType ); +} + + +//----------------------------------------------------------------------------- +// catch blockage while escaping magnade +//----------------------------------------------------------------------------- +void CNPC_Hunter::TaskFail( AI_TaskFailureCode_t code ) +{ + if ( IsCurSchedule( SCHED_HUNTER_TAKE_COVER_FROM_ENEMY, false ) && ( code == FAIL_NO_ROUTE_BLOCKED ) ) + { + // cornered! + if ( m_fCorneredTimer < gpGlobals->curtime ) + { + m_fCorneredTimer = gpGlobals->curtime + 6.0f; + } + } + + BaseClass::TaskFail( code ); +} + + +//----------------------------------------------------------------------------- +// The player is speeding toward us in a vehicle! Find a good activity for dodging. +//----------------------------------------------------------------------------- +void CNPC_Hunter::TaskFindDodgeActivity() +{ + if ( GetEnemy() == NULL ) + { + TaskFail( "No enemy to dodge" ); + return; + } + + Vector vecUp; + Vector vecRight; + GetVectors( NULL, &vecRight, &vecUp ); + + // TODO: find most perpendicular 8-way dodge when we get the anims + Vector vecEnemyDir = GetEnemy()->GetAbsOrigin() - GetAbsOrigin(); + //Vector vecDir = CrossProduct( vecEnemyDir, vecUp ); + VectorNormalize( vecEnemyDir ); + if ( fabs( DotProduct( vecEnemyDir, vecRight ) ) > 0.7 ) + { + TaskFail( "Can't dodge, enemy approaching perpendicularly" ); + return; + } + + // Check left or right randomly first. + bool bDodgeLeft = false; + CBaseEntity *pVehicle = GetEnemyVehicle(); + if ( pVehicle ) + { + Ray_t enemyRay; + Ray_t perpendicularRay; + enemyRay.Init( pVehicle->GetAbsOrigin(), pVehicle->GetAbsOrigin() + pVehicle->GetSmoothedVelocity() ); + Vector vPerpendicularPt = vecEnemyDir; + vPerpendicularPt.y = -vPerpendicularPt.y; + perpendicularRay.Init( GetAbsOrigin(), GetAbsOrigin() + vPerpendicularPt ); + + enemyRay.m_Start.z = enemyRay.m_Delta.z = enemyRay.m_StartOffset.z; + perpendicularRay.m_Start.z = perpendicularRay.m_Delta.z = perpendicularRay.m_StartOffset.z; + + float t, s; + + IntersectRayWithRay( perpendicularRay, enemyRay, t, s ); + + if ( t > 0 ) + { + bDodgeLeft = true; + } + } + else if ( random->RandomInt( 0, 1 ) == 0 ) + { + bDodgeLeft = true; + } + + bool bFoundDir = false; + int nTries = 0; + + while ( !bFoundDir && ( nTries < 2 ) ) + { + // Pick a dodge activity to try. + if ( bDodgeLeft ) + { + m_eDodgeActivity = ACT_HUNTER_DODGEL; + } + else + { + m_eDodgeActivity = ACT_HUNTER_DODGER; + } + + // See where the dodge will put us. + Vector vecLocalDelta; + int nSeq = SelectWeightedSequence( m_eDodgeActivity ); + GetSequenceLinearMotion( nSeq, &vecLocalDelta ); + + // Transform the sequence delta into local space. + matrix3x4_t fRotateMatrix; + AngleMatrix( GetLocalAngles(), fRotateMatrix ); + Vector vecDelta; + VectorRotate( vecLocalDelta, fRotateMatrix, vecDelta ); + + // Trace a bit high so this works better on uneven terrain. + Vector testHullMins = GetHullMins(); + testHullMins.z += ( StepHeight() * 2 ); + + // See if all is clear in that direction. + trace_t tr; + HunterTraceHull_SkipPhysics( GetAbsOrigin(), GetAbsOrigin() + vecDelta, testHullMins, GetHullMaxs(), MASK_NPCSOLID, this, GetCollisionGroup(), &tr, VPhysicsGetObject()->GetMass() * 0.5f ); + + // TODO: dodge anyway if we'll make it a certain percentage of the way through the dodge? + if ( tr.fraction == 1.0f ) + { + //NDebugOverlay::SweptBox( GetAbsOrigin(), GetAbsOrigin() + vecDelta, testHullMins, GetHullMaxs(), QAngle( 0, 0, 0 ), 0, 255, 0, 128, 5 ); + bFoundDir = true; + TaskComplete(); + } + else + { + //NDebugOverlay::SweptBox( GetAbsOrigin(), GetAbsOrigin() + vecDelta, testHullMins, GetHullMaxs(), QAngle( 0, 0, 0 ), 255, 0, 0, 128, 5 ); + nTries++; + bDodgeLeft = !bDodgeLeft; + } + } + + if ( nTries < 2 ) + { + TaskComplete(); + } + else + { + TaskFail( "Couldn't find dodge position\n" ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::StartTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_HUNTER_FINISH_RANGE_ATTACK: + { + if( GetEnemy() != NULL && GetEnemy()->Classify() == CLASS_PLAYER_ALLY_VITAL ) + { + // Just finished shooting at Alyx! So forget her for a little while and get back on the player + // !!!LATER - make sure there's someone else in enemy memory to go bother. + GetEnemies()->SetTimeValidEnemy( GetEnemy(), gpGlobals->curtime + 10.0f ); + } + + if( m_hCurrentSiegeTarget ) + { + // We probably just fired at our siege target, so dump it. + KillCurrentSiegeTarget(); + } + + TaskComplete(); + } + + case TASK_HUNTER_WAIT_FOR_MOVEMENT_FACING_ENEMY: + { + ChainStartTask( TASK_WAIT_FOR_MOVEMENT, pTask->flTaskData ); + break; + } + + case TASK_HUNTER_BEGIN_FLANK: + { + if ( IsInSquad() && GetSquad()->NumMembers() > 1 ) + { + // Flank relative to the other shooter in our squad. + // If there's no other shooter, just flank relative to any squad member. + AISquadIter_t iter; + CAI_BaseNPC *pNPC = GetSquad()->GetFirstMember( &iter ); + while ( pNPC == this ) + { + pNPC = GetSquad()->GetNextMember( &iter ); + } + + m_vSavePosition = pNPC->GetAbsOrigin(); + } + else + { + // Flank relative to our current position. + m_vSavePosition = GetAbsOrigin(); + } + + TaskComplete(); + break; + } + + case TASK_HUNTER_ANNOUNCE_FLANK: + { + EmitSound( "NPC_Hunter.FlankAnnounce" ); + TaskComplete(); + break; + } + + case TASK_HUNTER_DODGE: + { + if ( hunter_dodge_debug. GetBool() ) + { + Msg( "Hunter %d dodging\n", entindex() ); + } + SetIdealActivity( m_eDodgeActivity ); + break; + } + + // Guarantee a certain delay between volleys. If we aren't already planted, + // the plant transition animation will take care of that. + case TASK_HUNTER_PRE_RANGE_ATTACK2: + { + if ( !m_bPlanted || ( GetEnemy() && IsStriderBuster( GetEnemy() ) ) ) + { + TaskComplete(); + } + else + { + SetIdealActivity( ACT_HUNTER_ANGRY ); + } + break; + } + + case TASK_HUNTER_SHOOT_COMMIT: + { + // We're committing to shooting. Don't allow interrupts until after we've shot a bit (see TASK_RANGE_ATTACK1). + m_flShootAllowInterruptTime = gpGlobals->curtime + 100.0f; + TaskComplete(); + break; + } + + case TASK_RANGE_ATTACK2: + { + if ( GetEnemy() ) + { + bool bIsBuster = IsStriderBuster( GetEnemy() ); + if ( bIsBuster ) + { + AddFacingTarget( GetEnemy(), GetEnemy()->GetAbsOrigin() + GetEnemy()->GetSmoothedVelocity() * .5, 1.0, 0.8 ); + } + + // Start the firing sound. + //CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + //controller.SoundChangeVolume( m_pGunFiringSound, 1.0, hunter_first_flechette_delay.GetFloat() ); + + SetIdealActivity( ACT_RANGE_ATTACK2 ); + + // Decide how many shots to fire. + int nShots = hunter_flechette_volley_size.GetInt(); + if ( g_pGameRules->IsSkillLevel( SKILL_EASY ) ) + { + nShots--; + } + + // Decide when to fire the first shot. + float initialDelay = hunter_first_flechette_delay.GetFloat(); + if ( bIsBuster ) + { + initialDelay = 0; //*= 0.5; + } + + BeginVolley( nShots, gpGlobals->curtime + initialDelay ); + + // In case we need to miss on purpose, pick a direction now. + m_bMissLeft = false; + if ( random->RandomInt( 0, 1 ) == 0 ) + { + m_bMissLeft = true; + } + + LockBothEyes( initialDelay + ( nShots * hunter_flechette_delay.GetFloat() ) ); + } + else + { + TaskFail( FAIL_NO_ENEMY ); + } + + break; + } + + case TASK_HUNTER_STAGGER: + { + // Stagger in the direction the impact force would push us. + VMatrix worldToLocalRotation = EntityToWorldTransform(); + Vector vecLocalStaggerDir = worldToLocalRotation.InverseTR().ApplyRotation( m_vecStaggerDir ); + + float flStaggerYaw = VecToYaw( vecLocalStaggerDir ); + SetPoseParameter( gm_nStaggerYawPoseParam, flStaggerYaw ); + + // Go straight there! + SetActivity( ACT_RESET ); + SetActivity( ( Activity )ACT_HUNTER_STAGGER ); + break; + } + + case TASK_MELEE_ATTACK1: + { + SetLastAttackTime( gpGlobals->curtime ); + + if ( GetEnemy() && GetEnemy()->IsPlayer() ) + { + ResetIdealActivity( ( Activity )ACT_HUNTER_MELEE_ATTACK1_VS_PLAYER ); + } + else + { + ResetIdealActivity( ACT_MELEE_ATTACK1 ); + } + + break; + } + + case TASK_HUNTER_CORNERED_TIMER: + { + m_fCorneredTimer = gpGlobals->curtime + pTask->flTaskData; + + break; + } + + case TASK_HUNTER_FIND_SIDESTEP_POSITION: + { + if ( GetEnemy() == NULL ) + { + TaskFail( "No enemy to sidestep" ); + } + else + { + Vector vecUp; + GetVectors( NULL, NULL, &vecUp ); + + Vector vecEnemyDir = GetEnemy()->GetAbsOrigin() - GetAbsOrigin(); + Vector vecDir = CrossProduct( vecEnemyDir, vecUp ); + VectorNormalize( vecDir ); + + // Sidestep left or right randomly. + if ( random->RandomInt( 0, 1 ) == 0 ) + { + vecDir *= -1; + } + + // Start high and then trace down so that it works on uneven terrain. + Vector vecPos = GetAbsOrigin() + Vector( 0, 0, 64 ) + random->RandomFloat( 120, 200 ) * vecDir; + + // Try to find the ground at the sidestep position. + trace_t tr; + UTIL_TraceLine( vecPos, vecPos + Vector( 0, 0, -128 ), MASK_NPCSOLID, NULL, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction < 1.0f ) + { + //NDebugOverlay::Line( vecPos, tr.endpos, 0, 255, 0, true, 10 ); + + m_vSavePosition = tr.endpos; + + TaskComplete(); + } + else + { + TaskFail( "Couldn't find sidestep position\n" ); + } + } + + break; + } + + case TASK_HUNTER_FIND_DODGE_POSITION: + { + TaskFindDodgeActivity(); + break; + } + + case TASK_HUNTER_CHARGE: + { + SetIdealActivity( ( Activity )ACT_HUNTER_CHARGE_START ); + break; + } + + case TASK_HUNTER_CHARGE_DELAY: + { + m_flNextChargeTime = gpGlobals->curtime + pTask->flTaskData; + TaskComplete(); + break; + } + + case TASK_DIE: + { + GetNavigator()->StopMoving(); + ResetActivity(); + SetIdealActivity( GetDeathActivity() ); + m_lifeState = LIFE_DYING; + + break; + } + + //case TASK_HUNTER_END_FLANK: + //{ + // + //} + + default: + { + BaseClass::StartTask( pTask ); + break; + } + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::RunTask( const Task_t *pTask ) +{ + switch ( pTask->iTask ) + { + case TASK_HUNTER_PRE_RANGE_ATTACK2: + { + if ( IsActivityFinished() ) + { + TaskComplete(); + } + break; + } + + case TASK_RANGE_ATTACK2: + { + if( !hunter_hate_thrown_striderbusters.GetBool() && GetEnemy() != NULL && IsStriderBuster( GetEnemy() ) ) + { + if( !IsValidEnemy(GetEnemy()) ) + { + TaskFail("No longer hate this StriderBuster"); + } + } + + bool bIsBuster = IsStriderBuster( GetEnemy() ); + if ( bIsBuster ) + { + Vector vFuturePosition = GetEnemy()->GetAbsOrigin() + GetEnemy()->GetSmoothedVelocity() * .3; + AddFacingTarget( GetEnemy(), vFuturePosition, 1.0, 0.8 ); + + Vector2D vToFuturePositon = ( vFuturePosition.AsVector2D() - GetAbsOrigin().AsVector2D() ); + vToFuturePositon.NormalizeInPlace(); + Vector2D facingDir = BodyDirection2D().AsVector2D(); + + float flDot = DotProduct2D( vToFuturePositon, facingDir ); + + if ( flDot < .4 ) + { + GetMotor()->SetIdealYawToTarget( vFuturePosition ); + GetMotor()->UpdateYaw(); + break; + } + } + + if ( gpGlobals->curtime >= m_flNextFlechetteTime ) + { + // Must have an enemy and a shot queued up. + bool bDone = false; + if ( GetEnemy() != NULL && m_nFlechettesQueued > 0 ) + { + if ( ShootFlechette( GetEnemy(), false ) ) + { + m_nClampedShots++; + } + else + { + m_nClampedShots = 0; + } + + m_nFlechettesQueued--; + + // If we fired three or more clamped shots in a row, call it quits so we don't look dumb. + if ( ( m_nClampedShots >= 3 ) || ( m_nFlechettesQueued == 0 ) ) + { + bDone = true; + } + else + { + // More shooting to do. Schedule our next flechette. + m_flNextFlechetteTime = gpGlobals->curtime + hunter_flechette_delay.GetFloat(); + } + } + else + { + bDone = true; + } + + if ( bDone ) + { + // Stop the firing sound. + //CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + //controller.SoundChangeVolume( m_pGunFiringSound, 0.0f, 0.1f ); + + DelayRangedAttackers( hunter_flechette_volley_end_min_delay.GetFloat(), hunter_flechette_volley_end_max_delay.GetFloat(), true ); + TaskComplete(); + } + } + + break; + } + + case TASK_GET_PATH_TO_ENEMY_LOS: + { + ChainRunTask( TASK_GET_PATH_TO_ENEMY_LKP_LOS, pTask->flTaskData ); + break; + } + + case TASK_HUNTER_DODGE: + { + AutoMovement(); + + if ( IsActivityFinished() ) + { + TaskComplete(); + } + break; + } + + case TASK_HUNTER_CORNERED_TIMER: + { + TaskComplete(); + break; + } + + case TASK_HUNTER_STAGGER: + { + if ( IsActivityFinished() ) + { + TaskComplete(); + } + break; + } + + case TASK_HUNTER_CHARGE: + { + Activity eActivity = GetActivity(); + + // See if we're trying to stop after hitting/missing our target + if ( eActivity == ACT_HUNTER_CHARGE_STOP || eActivity == ACT_HUNTER_CHARGE_CRASH ) + { + if ( IsActivityFinished() ) + { + m_flNextChargeTime = gpGlobals->curtime + hunter_charge_min_delay.GetFloat() + random->RandomFloat( 0, 2.5 ) + random->RandomFloat( 0, 2.5 ); + float delayMultiplier = ( g_pGameRules->IsSkillLevel( SKILL_EASY ) ) ? 1.5 : 1.0; + float groupDelay = gpGlobals->curtime + ( 2.0 + random->RandomFloat( 0, 2 ) ) * delayMultiplier; + for ( int i = 0; i < g_Hunters.Count(); i++ ) + { + if ( g_Hunters[i] != this && g_Hunters[i]->m_flNextChargeTime < groupDelay ) + { + g_Hunters[i]->m_flNextChargeTime = groupDelay; + } + } + TaskComplete(); + return; + } + + // Still in the process of slowing down. Run movement until it's done. + AutoMovement(); + return; + } + + // Check for manual transition + if ( ( eActivity == ACT_HUNTER_CHARGE_START ) && ( IsActivityFinished() ) ) + { + SetIdealActivity( ACT_HUNTER_CHARGE_RUN ); + } + + // See if we're still running + if ( eActivity == ACT_HUNTER_CHARGE_RUN || eActivity == ACT_HUNTER_CHARGE_START ) + { + if ( HasCondition( COND_NEW_ENEMY ) || HasCondition( COND_LOST_ENEMY ) || HasCondition( COND_ENEMY_DEAD ) ) + { + SetIdealActivity( ACT_HUNTER_CHARGE_STOP ); + return; + } + else + { + if ( GetEnemy() != NULL ) + { + Vector goalDir = ( GetEnemy()->GetAbsOrigin() - GetAbsOrigin() ); + VectorNormalize( goalDir ); + + if ( DotProduct( BodyDirection2D(), goalDir ) < 0.25f ) + { + SetIdealActivity( ACT_HUNTER_CHARGE_STOP ); + } + } + } + } + + // Steer towards our target + float idealYaw; + if ( GetEnemy() == NULL ) + { + idealYaw = GetMotor()->GetIdealYaw(); + } + else + { + idealYaw = CalcIdealYaw( GetEnemy()->GetAbsOrigin() ); + } + + // Add in our steering offset + idealYaw += ChargeSteer(); + + // Turn to face + GetMotor()->SetIdealYawAndUpdate( idealYaw ); + + // See if we're going to run into anything soon + ChargeLookAhead(); + + // Let our animations simply move us forward. Keep the result + // of the movement so we know whether we've hit our target. + AIMoveTrace_t moveTrace; + if ( AutoMovement( GetEnemy(), &moveTrace ) == false ) + { + // Only stop if we hit the world + if ( HandleChargeImpact( moveTrace.vEndPosition, moveTrace.pObstruction ) ) + { + // If we're starting up, this is an error + if ( eActivity == ACT_HUNTER_CHARGE_START ) + { + TaskFail( "Unable to make initial movement of charge\n" ); + return; + } + + // Crash unless we're trying to stop already + if ( eActivity != ACT_HUNTER_CHARGE_STOP ) + { + if ( moveTrace.fStatus == AIMR_BLOCKED_WORLD && moveTrace.vHitNormal == vec3_origin ) + { + SetIdealActivity( ACT_HUNTER_CHARGE_STOP ); + } + else + { + // Shake the screen + if ( moveTrace.fStatus != AIMR_BLOCKED_NPC ) + { + EmitSound( "NPC_Hunter.ChargeHitWorld" ); + UTIL_ScreenShake( GetAbsOrigin(), 16.0f, 4.0f, 1.0f, 400.0f, SHAKE_START ); + } + SetIdealActivity( ACT_HUNTER_CHARGE_CRASH ); + } + } + } + else if ( moveTrace.pObstruction ) + { + // If we hit another hunter, stop + if ( moveTrace.pObstruction->Classify() == CLASS_COMBINE_HUNTER ) + { + // Crash unless we're trying to stop already + if ( eActivity != ACT_HUNTER_CHARGE_STOP ) + { + SetIdealActivity( ACT_HUNTER_CHARGE_STOP ); + } + } + // If we hit an antlion, don't stop, but kill it + // We never have hunters and antlions together, but you never know. + else if (moveTrace.pObstruction->Classify() == CLASS_ANTLION ) + { + if ( FClassnameIs( moveTrace.pObstruction, "npc_antlionguard" ) ) + { + // Crash unless we're trying to stop already + if ( eActivity != ACT_HUNTER_CHARGE_STOP ) + { + SetIdealActivity( ACT_HUNTER_CHARGE_STOP ); + } + } + else + { + Hunter_ApplyChargeDamage( this, moveTrace.pObstruction, moveTrace.pObstruction->GetHealth() ); + } + } + } + } + + break; + } + + case TASK_HUNTER_WAIT_FOR_MOVEMENT_FACING_ENEMY: + { + if ( GetEnemy() ) + { + Vector vecEnemyLKP = GetEnemyLKP(); + AddFacingTarget( GetEnemy(), vecEnemyLKP, 1.0, 0.8 ); + } + ChainRunTask( TASK_WAIT_FOR_MOVEMENT, pTask->flTaskData ); + break; + } + + default: + { + BaseClass::RunTask( pTask ); + break; + } + } +} + + +//----------------------------------------------------------------------------- +// Return true if our charge target is right in front of the hunter. +//----------------------------------------------------------------------------- +bool CNPC_Hunter::EnemyIsRightInFrontOfMe( CBaseEntity **pEntity ) +{ + if ( !GetEnemy() ) + return false; + + if ( (GetEnemy()->WorldSpaceCenter() - WorldSpaceCenter()).LengthSqr() < (156*156) ) + { + Vector vecLOS = ( GetEnemy()->GetAbsOrigin() - GetAbsOrigin() ); + vecLOS.z = 0; + VectorNormalize( vecLOS ); + Vector vBodyDir = BodyDirection2D(); + if ( DotProduct( vecLOS, vBodyDir ) > 0.8 ) + { + // He's in front of me, and close. Make sure he's not behind a wall. + trace_t tr; + UTIL_TraceHull( WorldSpaceCenter(), GetEnemy()->WorldSpaceCenter(), GetHullMins() * 0.5, GetHullMaxs() * 0.5, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + if ( tr.m_pEnt == GetEnemy() ) + { + *pEntity = tr.m_pEnt; + return true; + } + } + } + + return false; +} + + +//----------------------------------------------------------------------------- +// While charging, look ahead and see if we're going to run into anything. +// If we are, start the gesture so it looks like we're anticipating the hit. +//----------------------------------------------------------------------------- +void CNPC_Hunter::ChargeLookAhead( void ) +{ +#if 0 + trace_t tr; + Vector vecForward; + GetVectors( &vecForward, NULL, NULL ); + Vector vecTestPos = GetAbsOrigin() + ( vecForward * m_flGroundSpeed * 0.75 ); + Vector testHullMins = GetHullMins(); + testHullMins.z += (StepHeight() * 2); + HunterTraceHull_SkipPhysics( GetAbsOrigin(), vecTestPos, testHullMins, GetHullMaxs(), MASK_SHOT_HULL, this, COLLISION_GROUP_NONE, &tr, VPhysicsGetObject()->GetMass() * 0.5 ); + + //NDebugOverlay::Box( tr.startpos, testHullMins, GetHullMaxs(), 0, 255, 0, true, 0.1f ); + //NDebugOverlay::Box( vecTestPos, testHullMins, GetHullMaxs(), 255, 0, 0, true, 0.1f ); + + if ( tr.fraction != 1.0 ) + { + // dvs: TODO: + // Start playing the hit animation + //AddGesture( ACT_HUNTER_CHARGE_ANTICIPATION ); + } +#endif +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +float CNPC_Hunter::ChargeSteer() +{ + trace_t tr; + Vector testPos, steer, forward, right; + QAngle angles; + const float testLength = m_flGroundSpeed * 0.15f; + + //Get our facing + GetVectors( &forward, &right, NULL ); + + steer = forward; + + const float faceYaw = UTIL_VecToYaw( forward ); + + //Offset right + VectorAngles( forward, angles ); + angles[YAW] += 45.0f; + AngleVectors( angles, &forward ); + + // Probe out + testPos = GetAbsOrigin() + ( forward * testLength ); + + // Offset by step height + Vector testHullMins = GetHullMins(); + testHullMins.z += (StepHeight() * 2); + + // Probe + HunterTraceHull_SkipPhysics( GetAbsOrigin(), testPos, testHullMins, GetHullMaxs(), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr, VPhysicsGetObject()->GetMass() * 0.5f ); + + // Debug info + if ( g_debug_hunter_charge.GetInt() == 1 ) + { + if ( tr.fraction == 1.0f ) + { + NDebugOverlay::BoxDirection( GetAbsOrigin(), testHullMins, GetHullMaxs() + Vector(testLength,0,0), forward, 0, 255, 0, 8, 0.1f ); + } + else + { + NDebugOverlay::BoxDirection( GetAbsOrigin(), testHullMins, GetHullMaxs() + Vector(testLength,0,0), forward, 255, 0, 0, 8, 0.1f ); + } + } + + // Add in this component + steer += ( right * 0.5f ) * ( 1.0f - tr.fraction ); + + // Offset left + angles[YAW] -= 90.0f; + AngleVectors( angles, &forward ); + + // Probe out + testPos = GetAbsOrigin() + ( forward * testLength ); + HunterTraceHull_SkipPhysics( GetAbsOrigin(), testPos, testHullMins, GetHullMaxs(), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr, VPhysicsGetObject()->GetMass() * 0.5f ); + + // Debug + if ( g_debug_hunter_charge.GetInt() == 1 ) + { + if ( tr.fraction == 1.0f ) + { + NDebugOverlay::BoxDirection( GetAbsOrigin(), testHullMins, GetHullMaxs() + Vector(testLength,0,0), forward, 0, 255, 0, 8, 0.1f ); + } + else + { + NDebugOverlay::BoxDirection( GetAbsOrigin(), testHullMins, GetHullMaxs() + Vector(testLength,0,0), forward, 255, 0, 0, 8, 0.1f ); + } + } + + // Add in this component + steer -= ( right * 0.5f ) * ( 1.0f - tr.fraction ); + + // Debug + if ( g_debug_hunter_charge.GetInt() == 1 ) + { + NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + ( steer * 512.0f ), 255, 255, 0, true, 0.1f ); + NDebugOverlay::Cross3D( GetAbsOrigin() + ( steer * 512.0f ), Vector(2,2,2), -Vector(2,2,2), 255, 255, 0, true, 0.1f ); + + NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + ( BodyDirection3D() * 256.0f ), 255, 0, 255, true, 0.1f ); + NDebugOverlay::Cross3D( GetAbsOrigin() + ( BodyDirection3D() * 256.0f ), Vector(2,2,2), -Vector(2,2,2), 255, 0, 255, true, 0.1f ); + } + + return UTIL_AngleDiff( UTIL_VecToYaw( steer ), faceYaw ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::ChargeDamage( CBaseEntity *pTarget ) +{ + if ( pTarget == NULL ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( pTarget ); + + if ( pPlayer != NULL ) + { + //Kick the player angles + pPlayer->ViewPunch( QAngle( 20, 20, -30 ) ); + + Vector dir = pPlayer->WorldSpaceCenter() - WorldSpaceCenter(); + VectorNormalize( dir ); + dir.z = 0.0f; + + Vector vecNewVelocity = dir * 250.0f; + vecNewVelocity[2] += 128.0f; + pPlayer->SetAbsVelocity( vecNewVelocity ); + + color32 red = {128,0,0,128}; + UTIL_ScreenFade( pPlayer, red, 1.0f, 0.1f, FFADE_IN ); + } + + // Player takes less damage + float flDamage = ( pPlayer == NULL ) ? 250 : sk_hunter_dmg_charge.GetFloat(); + + // If it's being held by the player, break that bond + Pickup_ForcePlayerToDropThisObject( pTarget ); + + // Calculate the physics force + Hunter_ApplyChargeDamage( this, pTarget, flDamage ); +} + + +//----------------------------------------------------------------------------- +// Handles the hunter charging into something. Returns true if it hit the world. +//----------------------------------------------------------------------------- +bool CNPC_Hunter::HandleChargeImpact( Vector vecImpact, CBaseEntity *pEntity ) +{ + // Cause a shock wave from this point which will disrupt nearby physics objects + //ImpactShock( vecImpact, 128, 350 ); + + // Did we hit anything interesting? + if ( !pEntity || pEntity->IsWorld() ) + { + // Robin: Due to some of the finicky details in the motor, the hunter will hit + // the world when it is blocked by our enemy when trying to step up + // during a moveprobe. To get around this, we see if the enemy's within + // a volume in front of the hunter when we hit the world, and if he is, + // we hit him anyway. + EnemyIsRightInFrontOfMe( &pEntity ); + + // Did we manage to find him? If not, increment our charge miss count and abort. + if ( pEntity->IsWorld() ) + { + return true; + } + } + + // Hit anything we don't like + if ( IRelationType( pEntity ) == D_HT && ( GetNextAttack() < gpGlobals->curtime ) ) + { + EmitSound( "NPC_Hunter.ChargeHitEnemy" ); + + // dvs: TODO: + //if ( !IsPlayingGesture( ACT_HUNTER_CHARGE_HIT ) ) + //{ + // RestartGesture( ACT_HUNTER_CHARGE_HIT ); + //} + + ChargeDamage( pEntity ); + + if ( !pEntity->IsNPC() ) + { + pEntity->ApplyAbsVelocityImpulse( ( BodyDirection2D() * 400 ) + Vector( 0, 0, 200 ) ); + } + + if ( !pEntity->IsAlive() && GetEnemy() == pEntity ) + { + SetEnemy( NULL ); + } + + SetNextAttack( gpGlobals->curtime + 2.0f ); + + if ( !pEntity->IsAlive() || !pEntity->IsNPC() ) + { + SetIdealActivity( ACT_HUNTER_CHARGE_STOP ); + return false; + } + else + return true; + + } + + // Hit something we don't hate. If it's not moveable, crash into it. + if ( pEntity->GetMoveType() == MOVETYPE_NONE || pEntity->GetMoveType() == MOVETYPE_PUSH ) + { + CBreakable *pBreakable = dynamic_cast<CBreakable *>(pEntity); + if ( pBreakable && pBreakable->IsBreakable() && pBreakable->m_takedamage == DAMAGE_YES && pBreakable->GetHealth() > 0 ) + { + ChargeDamage( pEntity ); + } + return true; + } + + // If it's a vphysics object that's too heavy, crash into it too. + if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS ) + { + IPhysicsObject *pPhysics = pEntity->VPhysicsGetObject(); + if ( pPhysics ) + { + // If the object is being held by the player, knock it out of his hands + if ( pPhysics->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) + { + Pickup_ForcePlayerToDropThisObject( pEntity ); + return false; + } + + if ( !pPhysics->IsMoveable() ) + return true; + + float entMass = PhysGetEntityMass( pEntity ) ; + float minMass = VPhysicsGetObject()->GetMass() * 0.5f; + if ( entMass < minMass ) + { + if ( entMass < minMass * 0.666f || pEntity->CollisionProp()->BoundingRadius() < GetHullHeight() ) + { + if ( pEntity->GetHealth() > 0 ) + { + CBreakableProp *pBreakable = dynamic_cast<CBreakableProp *>(pEntity); + if ( pBreakable && pBreakable->m_takedamage == DAMAGE_YES && pBreakable->GetHealth() > 0 && pBreakable->GetHealth() <= 50 ) + { + ChargeDamage( pEntity ); + } + } + pEntity->SetNavIgnore( 2.0 ); + return false; + } + } + return true; + + } + } + + return false; +} + + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +void CNPC_Hunter::Explode() +{ + Vector velocity = vec3_origin; + AngularImpulse angVelocity = RandomAngularImpulse( -150, 150 ); + + PropBreakableCreateAll( GetModelIndex(), NULL, EyePosition(), GetAbsAngles(), velocity, angVelocity, 1.0, 150, COLLISION_GROUP_NPC, this ); + + ExplosionCreate( EyePosition(), GetAbsAngles(), this, 500, 256, (SF_ENVEXPLOSION_NOPARTICLES|SF_ENVEXPLOSION_NOSPARKS|SF_ENVEXPLOSION_NODLIGHTS|SF_ENVEXPLOSION_NODAMAGE|SF_ENVEXPLOSION_NOSMOKE), false ); + + // Create liquid fountain gushtacular effect here! + CEffectData data; + + data.m_vOrigin = EyePosition(); + data.m_vNormal = Vector( 0, 0, 1 ); + data.m_flScale = 4.0f; + + DispatchEffect( "StriderBlood", data ); + + // Go away + m_lifeState = LIFE_DEAD; + + SetThink( &CNPC_Hunter::SUB_Remove ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + AddEffects( EF_NODRAW ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +Activity CNPC_Hunter::NPC_TranslateActivity( Activity baseAct ) +{ + if ( ( baseAct == ACT_WALK ) || ( baseAct == ACT_RUN ) ) + { + if ( GetEnemy() ) + { + Vector vecEnemyLKP = GetEnemyLKP(); + + // Only start facing when we're close enough + if ( UTIL_DistApprox( vecEnemyLKP, GetAbsOrigin() ) < HUNTER_FACE_ENEMY_DIST ) + { + return (Activity)ACT_HUNTER_WALK_ANGRY; + } + } + } + else if ( ( baseAct == ACT_IDLE ) && m_bPlanted ) + { + return ( Activity )ACT_HUNTER_IDLE_PLANTED; + } + else if ( baseAct == ACT_RANGE_ATTACK2 ) + { + if ( !m_bPlanted && ( m_bEnableUnplantedShooting || IsStriderBuster( GetEnemy() ) ) ) + { + return (Activity)ACT_HUNTER_RANGE_ATTACK2_UNPLANTED; + } + } + + return BaseClass::NPC_TranslateActivity( baseAct ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::HandleAnimEvent( animevent_t *pEvent ) +{ + Vector footPosition; + QAngle angles; + + if ( pEvent->event == AE_HUNTER_FOOTSTEP_LEFT ) + { + LeftFootHit( pEvent->eventtime ); + return; + } + + if ( pEvent->event == AE_HUNTER_FOOTSTEP_RIGHT ) + { + RightFootHit( pEvent->eventtime ); + return; + } + + if ( pEvent->event == AE_HUNTER_FOOTSTEP_BACK ) + { + BackFootHit( pEvent->eventtime ); + return; + } + + if ( pEvent->event == AE_HUNTER_START_EXPRESSION ) + { + if ( pEvent->options && Q_strlen( pEvent->options ) ) + { + //m_iszCurrentExpression = AllocPooledString( pEvent->options ); + //SetExpression( pEvent->options ); + } + return; + } + + if ( pEvent->event == AE_HUNTER_END_EXPRESSION ) + { + if ( pEvent->options && Q_strlen( pEvent->options ) ) + { + //m_iszCurrentExpression = NULL_STRING; + //RemoveActorFromScriptedScenes( this, true, false, pEvent->options ); + } + return; + } + + if ( pEvent->event == AE_HUNTER_MELEE_ANNOUNCE ) + { + EmitSound( "NPC_Hunter.MeleeAnnounce" ); + return; + } + + if ( pEvent->event == AE_HUNTER_MELEE_ATTACK_LEFT ) + { + Vector right, forward, dir; + AngleVectors( GetLocalAngles(), &forward, &right, NULL ); + + right = right * -100; + forward = forward * 600; + dir = right + forward; + QAngle angle( 25, 30, -20 ); + + MeleeAttack( HUNTER_MELEE_REACH, sk_hunter_dmg_one_slash.GetFloat(), angle, dir, HUNTER_BLOOD_LEFT_FOOT ); + return; + } + + if ( pEvent->event == AE_HUNTER_MELEE_ATTACK_RIGHT ) + { + Vector right, forward,dir; + AngleVectors( GetLocalAngles(), &forward, &right, NULL ); + + right = right * 100; + forward = forward * 600; + dir = right + forward; + + QAngle angle( 25, -30, 20 ); + + MeleeAttack( HUNTER_MELEE_REACH, sk_hunter_dmg_one_slash.GetFloat(), angle, dir, HUNTER_BLOOD_LEFT_FOOT ); + return; + } + + if ( pEvent->event == AE_HUNTER_SPRAY_BLOOD ) + { + Vector vecOrigin; + Vector vecDir; + + // spray blood from the attachment point + bool bGotAttachment = false; + if ( pEvent->options ) + { + QAngle angDir; + if ( GetAttachment( pEvent->options, vecOrigin, angDir ) ) + { + bGotAttachment = true; + AngleVectors( angDir, &vecDir, NULL, NULL ); + } + } + + // fall back to our center, tracing forward + if ( !bGotAttachment ) + { + vecOrigin = WorldSpaceCenter(); + GetVectors( &vecDir, NULL, NULL ); + } + + UTIL_BloodSpray( vecOrigin, vecDir, BLOOD_COLOR_RED, 4, FX_BLOODSPRAY_ALL ); + + for ( int i = 0 ; i < 3 ; i++ ) + { + Vector vecTraceDir = vecDir; + vecTraceDir.x += random->RandomFloat( -0.1, 0.1 ); + vecTraceDir.y += random->RandomFloat( -0.1, 0.1 ); + vecTraceDir.z += random->RandomFloat( -0.1, 0.1 ); + + trace_t tr; + AI_TraceLine( vecOrigin, vecOrigin + ( vecTraceDir * 192.0f ), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction != 1.0 ) + { + UTIL_BloodDecalTrace( &tr, BLOOD_COLOR_RED ); + } + } + + return; + } + + BaseClass::HandleAnimEvent( pEvent ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::AddEntityRelationship( CBaseEntity *pEntity, Disposition_t nDisposition, int nPriority ) +{ + if ( nDisposition == D_HT && pEntity->ClassMatches("npc_bullseye") ) + UpdateEnemyMemory( pEntity, pEntity->GetAbsOrigin() ); + BaseClass::AddEntityRelationship( pEntity, nDisposition, nPriority ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::ScheduledMoveToGoalEntity( int scheduleType, CBaseEntity *pGoalEntity, Activity movementActivity ) +{ + if ( IsCurSchedule( SCHED_HUNTER_RANGE_ATTACK1, false ) ) + { + SetGoalEnt( pGoalEntity ); + return true; + } + return BaseClass::ScheduledMoveToGoalEntity( scheduleType, pGoalEntity, movementActivity ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::OnChangeHintGroup( string_t oldGroup, string_t newGroup ) +{ + SetCondition( COND_HUNTER_NEW_HINTGROUP ); + m_CheckHintGroupTimer.Set( 10 ); +} + + +//----------------------------------------------------------------------------- +// Tells whether any given hunter is in a squad that contains other hunters. +// This is useful for preventing timid behavior for Hunters that are not +// supported by other hunters. +// +// NOTE: This counts the self! So a hunter that is alone in his squad +// receives a result of 1. +//----------------------------------------------------------------------------- +int CNPC_Hunter::NumHuntersInMySquad() +{ + AISquadIter_t iter; + CAI_BaseNPC *pSquadmate = m_pSquad ? m_pSquad->GetFirstMember( &iter ) : NULL; + + if( !pSquadmate ) + { + // Not in a squad at all, but the caller is not concerned with that. Just + // tell them that we're in a squad of one (ourself) + return 1; + } + + int count = 0; + + while ( pSquadmate ) + { + if( pSquadmate->m_iClassname == m_iClassname ) + count++; + + pSquadmate = m_pSquad->GetNextMember( &iter ); + } + + return count; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::FollowStrider( const char *szStrider ) +{ + if ( !szStrider ) + return; + + CBaseEntity *pEnt = gEntList.FindEntityByName( NULL, szStrider, this ); + CNPC_Strider *pStrider = dynamic_cast <CNPC_Strider *>( pEnt ); + FollowStrider(pStrider); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::FollowStrider( CNPC_Strider * pStrider ) +{ + if ( !IsAlive() ) + { + return; + } + + if ( pStrider ) + { + if ( m_EscortBehavior.GetFollowTarget() != pStrider ) + { + m_iszFollowTarget = pStrider->GetEntityName(); + if ( m_iszFollowTarget == NULL_STRING ) + { + m_iszFollowTarget = AllocPooledString( "unnamed_strider" ); + } + m_EscortBehavior.SetEscortTarget( pStrider ); + } + } + else + { + DevWarning("Hunter set to follow entity %s that is not a strider\n", STRING( m_iszFollowTarget ) ); + m_iszFollowTarget = AllocPooledString( "unknown_strider" ); + } +} + +void CAI_HunterEscortBehavior::SetEscortTarget( CNPC_Strider *pStrider, bool fFinishCurSchedule ) +{ + m_bEnabled = true; + + if ( GetOuter()->GetSquad() ) + { + GetOuter()->GetSquad()->RemoveFromSquad( GetOuter() ); + } + + for ( int i = 0; i < g_Hunters.Count(); i++ ) + { + if ( g_Hunters[i]->m_EscortBehavior.GetFollowTarget() == pStrider ) + { + Assert( g_Hunters[i]->GetSquad() ); + g_Hunters[i]->GetSquad()->AddToSquad( GetOuter() ); + break; + } + } + + if ( !GetOuter()->GetSquad() ) + { + GetOuter()->AddToSquad( AllocPooledString( CFmtStr( "%s_hunter_squad", STRING( pStrider->GetEntityName() ) ) ) ); + } + + BaseClass::SetFollowTarget( pStrider ); + m_flTimeEscortReturn = gpGlobals->curtime; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::InputEnableUnplantedShooting( inputdata_t &inputdata ) +{ + m_bEnableUnplantedShooting = true; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::InputDisableUnplantedShooting( inputdata_t &inputdata ) +{ + m_bEnableUnplantedShooting = false; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::InputFollowStrider( inputdata_t &inputdata ) +{ + m_iszFollowTarget = inputdata.value.StringID(); + if ( m_iszFollowTarget == s_iszStriderClassname ) + { + m_EscortBehavior.m_bEnabled = true; + m_iszFollowTarget = NULL_STRING; + } + m_BeginFollowDelay.Start( .1 ); // Allow time for strider to spawn +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::InputUseSiegeTargets( inputdata_t &inputdata ) +{ + m_iszSiegeTargetName = inputdata.value.StringID(); + m_flTimeNextSiegeTargetAttack = gpGlobals->curtime + random->RandomFloat( 1, hunter_siege_frequency.GetFloat() ); + + if( m_iszSiegeTargetName == NULL_STRING ) + { + // Turning the feature off. Restore m_flDistTooFar to default. + m_flDistTooFar = hunter_flechette_max_range.GetFloat(); + m_pSiegeTargets.RemoveAll(); + } + else + { + // We're going into siege mode. Adjust range accordingly. + m_flDistTooFar = hunter_flechette_max_range.GetFloat() * HUNTER_SIEGE_MAX_DIST_MODIFIER; + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::InputDodge( inputdata_t &inputdata ) +{ + SetCondition( COND_HUNTER_FORCED_DODGE ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::InputFlankEnemy( inputdata_t &inputdata ) +{ + SetCondition( COND_HUNTER_FORCED_FLANK_ENEMY ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::InputDisableShooting( inputdata_t &inputdata ) +{ + m_bDisableShooting = true; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::InputEnableShooting( inputdata_t &inputdata ) +{ + m_bDisableShooting = false; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::InputEnableSquadShootDelay( inputdata_t &inputdata ) +{ + m_bEnableSquadShootDelay = true; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::InputDisableSquadShootDelay( inputdata_t &inputdata ) +{ + m_bEnableSquadShootDelay = false; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::FVisible( CBaseEntity *pEntity, int traceMask, CBaseEntity **ppBlocker ) +{ + return BaseClass::FVisible( pEntity, traceMask, ppBlocker ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::IsValidEnemy( CBaseEntity *pTarget ) +{ + if ( IsStriderBuster( pTarget) ) + { + if ( !m_EscortBehavior.m_bEnabled || !m_EscortBehavior.GetEscortTarget() ) + { + // We only hate striderbusters when we are actively protecting a strider. + return false; + } + + if ( pTarget->VPhysicsGetObject() ) + { + if ( ( pTarget->VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) && + hunter_hate_held_striderbusters.GetBool() ) + { + if ( gpGlobals->curtime - StriderBuster_GetPickupTime( pTarget ) > hunter_hate_held_striderbusters_delay.GetFloat()) + { + if ( StriderBuster_NumFlechettesAttached( pTarget ) <= 2 ) + { + if ( m_EscortBehavior.GetEscortTarget() && + ( m_EscortBehavior.GetEscortTarget()->GetAbsOrigin().AsVector2D() - pTarget->GetAbsOrigin().AsVector2D() ).LengthSqr() < Square( hunter_hate_held_striderbusters_tolerance.GetFloat() ) ) + { + return true; + } + } + } + return false; + } + + bool bThrown = ( pTarget->VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_WAS_THROWN ) != 0; + bool bAttached = StriderBuster_IsAttachedStriderBuster( pTarget ); + + if ( ( bThrown && !bAttached ) && hunter_hate_thrown_striderbusters.GetBool() ) + { + float t; + float dist = CalcDistanceSqrToLineSegment2D( m_EscortBehavior.GetEscortTarget()->GetAbsOrigin().AsVector2D(), + pTarget->GetAbsOrigin().AsVector2D(), + pTarget->GetAbsOrigin().AsVector2D() + pTarget->GetSmoothedVelocity().AsVector2D(), &t ); + + if ( t > 0 && dist < Square( hunter_hate_thrown_striderbusters_tolerance.GetFloat() )) + { + return true; + } + return false; + } + + if ( bAttached && StriderBuster_IsAttachedStriderBuster( pTarget, m_EscortBehavior.GetEscortTarget() ) && hunter_hate_attached_striderbusters.GetBool() ) + { + return true; + } + } + return false; + } + + return BaseClass::IsValidEnemy( pTarget ); +} + + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +Disposition_t CNPC_Hunter::IRelationType( CBaseEntity *pTarget ) +{ + if ( !pTarget ) + return D_NU; + + if ( IsStriderBuster( pTarget ) ) + { + if ( HateThisStriderBuster( pTarget ) ) + return D_HT; + + return D_NU; + } + + if ( hunter_retreat_striderbusters.GetBool() ) + { + if ( pTarget->IsPlayer() && (m_hAttachedBusters.Count() > 0) ) + { + return D_FR; + } + } + + return BaseClass::IRelationType( pTarget ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::IRelationPriority( CBaseEntity *pTarget ) +{ + if ( IsStriderBuster( pTarget ) ) + { + // If we're here, we already know that we hate striderbusters. + return 1000.0f; + } + + return BaseClass::IRelationPriority( pTarget ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::SetSquad( CAI_Squad *pSquad ) +{ + BaseClass::SetSquad( pSquad ); + if ( pSquad && pSquad->NumMembers() == 1 ) + { + pSquad->SetSquadData( HUNTER_RUNDOWN_SQUADDATA, 0 ); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::OnSeeEntity( CBaseEntity *pEntity ) +{ + BaseClass::OnSeeEntity(pEntity); + + if ( IsStriderBuster( pEntity ) && IsValidEnemy( pEntity ) ) + { + SetCondition( COND_HUNTER_SEE_STRIDERBUSTER ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::UpdateEnemyMemory( CBaseEntity *pEnemy, const Vector &position, CBaseEntity *pInformer ) +{ + //EmitSound( "NPC_Hunter.Alert" ); + return BaseClass::UpdateEnemyMemory( pEnemy, position, pInformer ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::CanPlantHere( const Vector &vecPos ) +{ + // TODO: cache results? + //if ( vecPos == m_vecLastCanPlantHerePos ) + //{ + // return m_bLastCanPlantHere; + //} + + Vector vecMins = GetHullMins(); + Vector vecMaxs = GetHullMaxs(); + + vecMins.x -= 16; + vecMins.y -= 16; + + vecMaxs.x += 16; + vecMaxs.y += 16; + vecMaxs.z -= hunter_plant_adjust_z.GetInt(); + + bool bResult = false; + + trace_t tr; + UTIL_TraceHull( vecPos, vecPos, vecMins, vecMaxs, MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr ); + if ( tr.startsolid ) + { + // Try again, tracing down from above. + Vector vecStart = vecPos; + vecStart.z += hunter_plant_adjust_z.GetInt(); + + UTIL_TraceHull( vecStart, vecPos, vecMins, vecMaxs, MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr ); + } + + if ( tr.startsolid ) + { + //NDebugOverlay::Box( vecPos, vecMins, vecMaxs, 255, 0, 0, 0, 0 ); + } + else + { + //NDebugOverlay::Box( vecPos, vecMins, vecMaxs, 0, 255, 0, 0, 0 ); + bResult = true; + } + + // Cache the results in case we ask again for the same spot. + //m_vecLastCanPlantHerePos = vecPos; + //m_bLastCanPlantHere = bResult; + + return bResult; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::MeleeAttack1ConditionsVsEnemyInVehicle( CBaseCombatCharacter *pEnemy, float flDot ) +{ + if( !IsCorporealEnemy( GetEnemy() ) ) + return COND_NONE; + + // Try and trace a box to the player, and if I hit the vehicle, attack it + Vector vecDelta = (pEnemy->WorldSpaceCenter() - WorldSpaceCenter()); + VectorNormalize( vecDelta ); + trace_t tr; + AI_TraceHull( WorldSpaceCenter(), WorldSpaceCenter() + (vecDelta * 64), -Vector(8,8,8), Vector(8,8,8), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction != 1.0 && tr.m_pEnt == pEnemy->GetVehicleEntity() ) + { + // We're near the vehicle. Are we facing it? + if (flDot < 0.7) + return COND_NOT_FACING_ATTACK; + + return COND_CAN_MELEE_ATTACK1; + } + + return COND_TOO_FAR_TO_ATTACK; +} + + +//----------------------------------------------------------------------------- +// For innate melee attack +//----------------------------------------------------------------------------- +int CNPC_Hunter::MeleeAttack1Conditions ( float flDot, float flDist ) +{ + if ( !IsCorporealEnemy( GetEnemy() ) ) + return COND_NONE; + + if ( ( gpGlobals->curtime < m_flNextMeleeTime ) && // allow berzerk bashing if cornered + !( m_hAttachedBusters.Count() > 0 && gpGlobals->curtime < m_fCorneredTimer ) ) + { + return COND_NONE; + } + + if ( GetEnemy()->Classify() == CLASS_PLAYER_ALLY_VITAL ) + { + return COND_NONE; + } + + if ( flDist > HUNTER_MELEE_REACH ) + { + // Translate a hit vehicle into its passenger if found + if ( GetEnemy() != NULL ) + { + CBaseCombatCharacter *pCCEnemy = GetEnemy()->MyCombatCharacterPointer(); + if ( pCCEnemy != NULL && pCCEnemy->IsInAVehicle() ) + { + return MeleeAttack1ConditionsVsEnemyInVehicle( pCCEnemy, flDot ); + } + +#if defined(HL2_DLL) && !defined(HL2MP) + // If the player is holding an object, knock it down. + if ( GetEnemy()->IsPlayer() ) + { + CBasePlayer *pPlayer = ToBasePlayer( GetEnemy() ); + + Assert( pPlayer != NULL ); + + // Is the player carrying something? + CBaseEntity *pObject = GetPlayerHeldEntity(pPlayer); + + if ( !pObject ) + { + pObject = PhysCannonGetHeldEntity( pPlayer->GetActiveWeapon() ); + } + + if ( pObject ) + { + float flDist = pObject->WorldSpaceCenter().DistTo( WorldSpaceCenter() ); + + if ( flDist <= HUNTER_MELEE_REACH ) + { + return COND_CAN_MELEE_ATTACK1; + } + } + } +#endif + } + + return COND_TOO_FAR_TO_ATTACK; + } + + if (flDot < 0.7) + { + return COND_NOT_FACING_ATTACK; + } + + // Build a cube-shaped hull, the same hull that MeleeAttack is going to use. + Vector vecMins = GetHullMins(); + Vector vecMaxs = GetHullMaxs(); + vecMins.z = vecMins.x; + vecMaxs.z = vecMaxs.x; + + Vector forward; + GetVectors( &forward, NULL, NULL ); + + trace_t tr; + AI_TraceHull( WorldSpaceCenter(), WorldSpaceCenter() + forward * HUNTER_MELEE_REACH, vecMins, vecMaxs, MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction == 1.0 || !tr.m_pEnt ) + { + // This attack would miss completely. Trick the hunter into moving around some more. + return COND_TOO_FAR_TO_ATTACK; + } + + if ( tr.m_pEnt == GetEnemy() || tr.m_pEnt->IsNPC() || (tr.m_pEnt->m_takedamage == DAMAGE_YES && (dynamic_cast<CBreakableProp*>(tr.m_pEnt))) ) + { + // Let the hunter swipe at his enemy if he's going to hit them. + // Also let him swipe at NPC's that happen to be between the hunter and the enemy. + // This makes mobs of hunters seem more rowdy since it doesn't leave guys in the back row standing around. + // Also let him swipe at things that takedamage, under the assumptions that they can be broken. + return COND_CAN_MELEE_ATTACK1; + } + + // dvs TODO: incorporate this + /*if ( tr.m_pEnt->IsBSPModel() ) + { + // The trace hit something solid, but it's not the enemy. If this item is closer to the hunter than + // the enemy is, treat this as an obstruction. + Vector vecToEnemy = GetEnemy()->WorldSpaceCenter() - WorldSpaceCenter(); + Vector vecTrace = tr.endpos - tr.startpos; + + if ( vecTrace.Length2DSqr() < vecToEnemy.Length2DSqr() ) + { + return COND_HUNTER_LOCAL_MELEE_OBSTRUCTION; + } + }*/ + + if ( !tr.m_pEnt->IsWorld() && GetEnemy() && GetEnemy()->GetGroundEntity() == tr.m_pEnt ) + { + // Try to swat whatever the player is standing on instead of acting like a dill. + return COND_CAN_MELEE_ATTACK1; + } + + // Move around some more + return COND_TOO_FAR_TO_ATTACK; +} + + +//----------------------------------------------------------------------------- +// For innate melee attack +//----------------------------------------------------------------------------- +int CNPC_Hunter::MeleeAttack2Conditions ( float flDot, float flDist ) +{ + return COND_NONE; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::IsCorporealEnemy( CBaseEntity *pEnemy ) +{ + if( !pEnemy ) + return false; + + // Generally speaking, don't melee attack anything the player can't see. + if( pEnemy->IsEffectActive( EF_NODRAW ) ) + return false; + + // Don't flank, melee attack striderbusters. + if ( IsStriderBuster( pEnemy ) ) + return false; + + return true; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::RangeAttack1Conditions( float flDot, float flDist ) +{ + return COND_NONE; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::RangeAttack2Conditions( float flDot, float flDist ) +{ + bool bIsBuster = IsStriderBuster( GetEnemy() ); + bool bIsPerfectBullseye = ( GetEnemy() && dynamic_cast<CNPC_Bullseye *>(GetEnemy()) && ((CNPC_Bullseye *)GetEnemy())->UsePerfectAccuracy() ); + + if ( !bIsPerfectBullseye && !bIsBuster && !hunter_flechette_test.GetBool() && ( gpGlobals->curtime < m_flNextRangeAttack2Time ) ) + { + return COND_NONE; + } + + if ( m_bDisableShooting ) + { + return COND_NONE; + } + + if ( !HasCondition( COND_SEE_ENEMY ) ) + { + return COND_NONE; + } + + float flMaxFlechetteRange = hunter_flechette_max_range.GetFloat(); + + if ( IsUsingSiegeTargets() ) + { + flMaxFlechetteRange *= HUNTER_SIEGE_MAX_DIST_MODIFIER; + } + + if ( !bIsBuster && ( flDist > flMaxFlechetteRange ) ) + { + return COND_TOO_FAR_TO_ATTACK; + } + else if ( !bIsBuster && ( !GetEnemy() || !GetEnemy()->ClassMatches( "npc_bullseye" ) ) && flDist < hunter_flechette_min_range.GetFloat() ) + { + return COND_TOO_CLOSE_TO_ATTACK; + } + else if ( flDot < HUNTER_FACING_DOT ) + { + return COND_NOT_FACING_ATTACK; + } + + if ( !bIsBuster && !m_bEnableUnplantedShooting && !hunter_flechette_test.GetBool() && !CanPlantHere( GetAbsOrigin() ) ) + { + return COND_HUNTER_CANT_PLANT; + } + + return COND_CAN_RANGE_ATTACK2; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::WeaponLOSCondition(const Vector &ownerPos, const Vector &targetPos, bool bSetConditions) +{ + CBaseEntity *pTargetEnt; + + pTargetEnt = GetEnemy(); + + trace_t tr; + Vector vFrom = ownerPos + GetViewOffset(); + AI_TraceLine( vFrom, targetPos, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + + if ( ( pTargetEnt && tr.m_pEnt == pTargetEnt) || tr.fraction == 1.0 || CanShootThrough( tr, targetPos ) ) + { + static Vector vMins( -2.0, -2.0, -2.0 ); + static Vector vMaxs( -vMins); + // Hit the enemy, or hit nothing (traced all the way to a nonsolid enemy like a bullseye) + AI_TraceHull( vFrom - Vector( 0, 0, 18 ), targetPos, vMins, vMaxs, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr); + + if ( ( pTargetEnt && tr.m_pEnt == pTargetEnt) || tr.fraction == 1.0 || CanShootThrough( tr, targetPos ) ) + { + if ( hunter_show_weapon_los_condition.GetBool() ) + { + NDebugOverlay::Line( vFrom, targetPos, 255, 0, 255, false, 0.1 ); + NDebugOverlay::Line( vFrom - Vector( 0, 0, 18 ), targetPos, 0, 0, 255, false, 0.1 ); + } + return true; + } + } + else if ( bSetConditions ) + { + SetCondition( COND_WEAPON_SIGHT_OCCLUDED ); + SetEnemyOccluder( tr.m_pEnt ); + } + + return false; +} + +//----------------------------------------------------------------------------- +// Look in front and see if the claw hit anything. +// +// Input : flDist distance to trace +// iDamage damage to do if attack hits +// vecViewPunch camera punch (if attack hits player) +// vecVelocityPunch velocity punch (if attack hits player) +// +// Output : The entity hit by claws. NULL if nothing. +//----------------------------------------------------------------------------- +CBaseEntity *CNPC_Hunter::MeleeAttack( float flDist, int iDamage, QAngle &qaViewPunch, Vector &vecVelocityPunch, int BloodOrigin ) +{ + // Added test because claw attack anim sometimes used when for cases other than melee + if ( GetEnemy() ) + { + trace_t tr; + AI_TraceHull( WorldSpaceCenter(), GetEnemy()->WorldSpaceCenter(), -Vector(8,8,8), Vector(8,8,8), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + + if ( tr.fraction < 1.0f ) + return NULL; + } + + // + // Trace out a cubic section of our hull and see what we hit. + // + Vector vecMins = GetHullMins(); + Vector vecMaxs = GetHullMaxs(); + vecMins.z = vecMins.x; + vecMaxs.z = vecMaxs.x; + + CBaseEntity *pHurt = CheckTraceHullAttack( flDist, vecMins, vecMaxs, iDamage, DMG_SLASH ); + + if ( pHurt ) + { + EmitSound( "NPC_Hunter.MeleeHit" ); + EmitSound( "NPC_Hunter.TackleHit" ); + + CBasePlayer *pPlayer = ToBasePlayer( pHurt ); + + if ( pPlayer != NULL && !(pPlayer->GetFlags() & FL_GODMODE ) ) + { + pPlayer->ViewPunch( qaViewPunch ); + pPlayer->VelocityPunch( vecVelocityPunch ); + + // Shake the screen + UTIL_ScreenShake( pPlayer->GetAbsOrigin(), 100.0, 1.5, 1.0, 2, SHAKE_START ); + + // Red damage indicator + color32 red = { 128, 0, 0, 128 }; + UTIL_ScreenFade( pPlayer, red, 1.0f, 0.1f, FFADE_IN ); + + /*if ( UTIL_ShouldShowBlood( pPlayer->BloodColor() ) ) + { + // Spray some of the player's blood on the hunter. + trace_t tr; + + Vector vecHunterEyePos; // = EyePosition(); + QAngle angDiscard; + GetBonePosition( LookupBone( "MiniStrider.top_eye_bone" ), vecHunterEyePos, angDiscard ); + + Vector vecPlayerEyePos = pPlayer->EyePosition(); + + Vector vecDir = vecHunterEyePos - vecPlayerEyePos; + float flLen = VectorNormalize( vecDir ); + + Vector vecStart = vecPlayerEyePos - ( vecDir * 64 ); + Vector vecEnd = vecPlayerEyePos + ( vecDir * ( flLen + 64 ) ); + + NDebugOverlay::HorzArrow( vecStart, vecEnd, 16, 255, 255, 0, 255, false, 10 ); + + UTIL_TraceLine( vecStart, vecEnd, MASK_SHOT, pPlayer, COLLISION_GROUP_NONE, &tr ); + + if ( tr.m_pEnt ) + { + Msg( "Hit %s!!!\n", tr.m_pEnt->GetDebugName() ); + UTIL_DecalTrace( &tr, "Blood" ); + } + }*/ + } + else if ( !pPlayer ) + { + if ( IsMovablePhysicsObject( pHurt ) ) + { + // If it's a vphysics object that's too heavy, crash into it too. + IPhysicsObject *pPhysics = pHurt->VPhysicsGetObject(); + if ( pPhysics ) + { + // If the object is being held by the player, break it or make them drop it. + if ( pPhysics->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) + { + // If it's breakable, break it. + if ( pHurt->m_takedamage == DAMAGE_YES ) + { + CBreakableProp *pBreak = dynamic_cast<CBreakableProp*>(pHurt); + if ( pBreak ) + { + CTakeDamageInfo info( this, this, 20, DMG_SLASH ); + pBreak->Break( this, info ); + } + } + } + } + } + + if ( UTIL_ShouldShowBlood(pHurt->BloodColor()) ) + { + // Hit an NPC. Bleed them! + Vector vecBloodPos; + + switch ( BloodOrigin ) + { + case HUNTER_BLOOD_LEFT_FOOT: + { + if ( GetAttachment( "blood_left", vecBloodPos ) ) + { + SpawnBlood( vecBloodPos, g_vecAttackDir, pHurt->BloodColor(), MIN( iDamage, 30 ) ); + } + + break; + } + } + } + } + } + else + { + // TODO: + //AttackMissSound(); + } + + m_flNextMeleeTime = gpGlobals->curtime + hunter_melee_delay.GetFloat(); + + return pHurt; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::TestShootPosition(const Vector &vecShootPos, const Vector &targetPos ) +{ + if ( !CanPlantHere(vecShootPos ) ) + { + return false; + } + + return BaseClass::TestShootPosition( vecShootPos, targetPos ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +Vector CNPC_Hunter::Weapon_ShootPosition( ) +{ + matrix3x4_t gunMatrix; + GetAttachment( gm_nTopGunAttachment, gunMatrix ); + + Vector vecShootPos; + MatrixGetColumn( gunMatrix, 3, vecShootPos ); + + return vecShootPos; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::MakeTracer( const Vector &vecTracerSrc, const trace_t &tr, int iTracerType ) +{ + float flTracerDist; + Vector vecDir; + Vector vecEndPos; + + vecDir = tr.endpos - vecTracerSrc; + + flTracerDist = VectorNormalize( vecDir ); + + int nAttachment = LookupAttachment( "MiniGun" ); + + UTIL_Tracer( vecTracerSrc, tr.endpos, nAttachment, TRACER_FLAG_USEATTACHMENT, 5000, true, "HunterTracer" ); +} + + +//----------------------------------------------------------------------------- +// Trace didn't hit the intended target, but should the hunter +// shoot anyway? We use this to get the hunter to destroy +// breakables that are between him and his target. +//----------------------------------------------------------------------------- +bool CNPC_Hunter::CanShootThrough( const trace_t &tr, const Vector &vecTarget ) +{ + if ( !tr.m_pEnt ) + { + return false; + } + + if ( !tr.m_pEnt->GetHealth() ) + { + return false; + } + + // Don't try to shoot through allies. + CAI_BaseNPC *pNPC = tr.m_pEnt->MyNPCPointer(); + if ( pNPC && ( IRelationType( pNPC ) == D_LI ) ) + { + return false; + } + + // Would a trace ignoring this entity continue to the target? + trace_t continuedTrace; + AI_TraceLine( tr.endpos, vecTarget, MASK_SHOT, tr.m_pEnt, COLLISION_GROUP_NONE, &continuedTrace ); + + if ( continuedTrace.fraction != 1.0 ) + { + if ( continuedTrace.m_pEnt != GetEnemy() ) + { + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::GetSoundInterests() +{ + return SOUND_WORLD | SOUND_COMBAT | SOUND_PLAYER | SOUND_DANGER | SOUND_PHYSICS_DANGER | SOUND_PLAYER_VEHICLE | SOUND_BULLET_IMPACT | SOUND_MOVE_AWAY; +} + +//----------------------------------------------------------------------------- +// Tells us whether the Hunter is acting in a large, outdoor map, +// currently only ep2_outland_12. This allows us to create logic +// branches here in the AI code so that we can make choices that +// tailor behavior to larger and smaller maps. +//----------------------------------------------------------------------------- +bool CNPC_Hunter::IsInLargeOutdoorMap() +{ + return m_bInLargeOutdoorMap; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::AlertSound() +{ + EmitSound( "NPC_Hunter.Alert" ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::PainSound( const CTakeDamageInfo &info ) +{ + if ( gpGlobals->curtime > m_flNextDamageTime ) + { + EmitSound( "NPC_Hunter.Pain" ); + m_flNextDamageTime = gpGlobals->curtime + random->RandomFloat( 0.5, 1.2 ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::DeathSound( const CTakeDamageInfo &info ) +{ + EmitSound( "NPC_Hunter.Death" ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::TraceAttack( const CTakeDamageInfo &inputInfo, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) +{ + CTakeDamageInfo info = inputInfo; + + // Even though the damage might not hurt us, we want to react to it + // if it's from the player. + if ( info.GetAttacker()->IsPlayer() ) + { + if ( !HasMemory( bits_MEMORY_PROVOKED ) ) + { + GetEnemies()->ClearMemory( info.GetAttacker() ); + Remember( bits_MEMORY_PROVOKED ); + SetCondition( COND_LIGHT_DAMAGE ); + } + } + + // HUnters have special resisitance to some types of damage. + if ( ( info.GetDamageType() & DMG_BULLET ) || + ( info.GetDamageType() & DMG_BUCKSHOT ) || + ( info.GetDamageType() & DMG_CLUB ) || + ( info.GetDamageType() & DMG_NEVERGIB ) ) + { + float flScale = 1.0; + + if ( info.GetDamageType() & DMG_BUCKSHOT ) + { + flScale = sk_hunter_buckshot_damage_scale.GetFloat(); + } + else if ( ( info.GetDamageType() & DMG_BULLET ) || ( info.GetDamageType() & DMG_NEVERGIB ) ) + { + // Hunters resist most bullet damage, but they are actually vulnerable to .357 rounds, + // since players regard that weapon as one of the game's truly powerful weapons. + if( info.GetAmmoType() == GetAmmoDef()->Index("357") ) + { + flScale = 1.16f; + } + else + { + flScale = sk_hunter_bullet_damage_scale.GetFloat(); + } + } + + if ( GetActivity() == ACT_HUNTER_CHARGE_RUN ) + { + flScale *= sk_hunter_charge_damage_scale.GetFloat(); + } + + if ( flScale != 0 ) + { + float flDamage = info.GetDamage() * flScale; + info.SetDamage( flDamage ); + } + + QAngle vecAngles; + VectorAngles( ptr->plane.normal, vecAngles ); + DispatchParticleEffect( "blood_impact_synth_01", ptr->endpos, vecAngles ); + DispatchParticleEffect( "blood_impact_synth_01_arc_parent", PATTACH_POINT_FOLLOW, this, gm_nHeadCenterAttachment ); + } + + BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +const impactdamagetable_t &CNPC_Hunter::GetPhysicsImpactDamageTable() +{ + return s_HunterImpactDamageTable; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::PhysicsDamageEffect( const Vector &vecPos, const Vector &vecDir ) +{ + CEffectData data; + data.m_vOrigin = vecPos; + data.m_vNormal = vecDir; + DispatchEffect( "HunterDamage", data ); + + if ( random->RandomInt( 0, 1 ) == 0 ) + { + CBaseEntity *pTrail = CreateEntityByName( "sparktrail" ); + pTrail->SetOwnerEntity( this ); + pTrail->Spawn(); + } +} + + +//----------------------------------------------------------------------------- +// We were hit by a strider buster. Do the tesla effect on our hitboxes. +//----------------------------------------------------------------------------- +void CNPC_Hunter::TeslaThink() +{ + CEffectData data; + data.m_nEntIndex = entindex(); + data.m_flMagnitude = 3; + data.m_flScale = 0.5f; + DispatchEffect( "TeslaHitboxes", data ); + EmitSound( "RagdollBoogie.Zap" ); + + if ( gpGlobals->curtime < m_flTeslaStopTime ) + { + SetContextThink( &CNPC_Hunter::TeslaThink, gpGlobals->curtime + random->RandomFloat( 0.1f, 0.3f ), HUNTER_ZAP_THINK ); + } +} + + +//----------------------------------------------------------------------------- +// Our health is low. Show damage effects. +//----------------------------------------------------------------------------- +void CNPC_Hunter::BleedThink() +{ + // Spurt blood from random points on the hunter's head. + Vector vecOrigin; + QAngle angDir; + GetAttachment( gm_nHeadCenterAttachment, vecOrigin, angDir ); + + Vector vecDir = RandomVector( -1, 1 ); + VectorNormalize( vecDir ); + VectorAngles( vecDir, Vector( 0, 0, 1 ), angDir ); + + vecDir *= gm_flHeadRadius; + DispatchParticleEffect( "blood_spurt_synth_01", vecOrigin + vecDir, angDir ); + + SetNextThink( gpGlobals->curtime + random->RandomFloat( 0.6, 1.5 ), HUNTER_BLEED_THINK ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::IsHeavyDamage( const CTakeDamageInfo &info ) +{ + if ( info.GetDamage() < 45 ) + { + return false; + } + + if ( info.GetDamage() < 180 ) + { + if ( !m_HeavyDamageDelay.Expired() || !BaseClass::IsHeavyDamage( info ) ) + { + return false; + } + } + + m_HeavyDamageDelay.Set( 15, 25 ); + return true; + +} + + +//----------------------------------------------------------------------------- +// We've taken some damage. Maybe we should flinch because of it. +//----------------------------------------------------------------------------- +void CNPC_Hunter::ConsiderFlinching( const CTakeDamageInfo &info ) +{ + if ( !m_FlinchTimer.Expired() ) + { + // Someone is whaling on us. Push out the timer so we don't keep flinching. + m_FlinchTimer.Set( random->RandomFloat( 0.3 ) ); + return; + } + + if ( GetState() == NPC_STATE_SCRIPT ) + { + return; + } + + Activity eGesture = ACT_HUNTER_FLINCH_N; + + Vector forward; + GetVectors( &forward, NULL, NULL ); + + Vector vecForceDir = info.GetDamageForce(); + VectorNormalize( vecForceDir ); + + float flDot = DotProduct( forward, vecForceDir ); + + if ( flDot > 0.707 ) + { + // flinch forward + eGesture = ACT_HUNTER_FLINCH_N; + } + else if ( flDot < -0.707 ) + { + // flinch back + eGesture = ACT_HUNTER_FLINCH_S; + } + else + { + // flinch left or right + Vector cross = CrossProduct( forward, vecForceDir ); + + if ( cross.z > 0 ) + { + eGesture = ACT_HUNTER_FLINCH_W; + } + else + { + eGesture = ACT_HUNTER_FLINCH_E; + } + } + + if ( !IsPlayingGesture( eGesture ) ) + { + RestartGesture( eGesture ); + m_FlinchTimer.Set( random->RandomFloat( 0.3, 1.0 ) ); + } +} + + +//----------------------------------------------------------------------------- +// This is done from a think function because when the hunter is killed, +// the physics code puts the vehicle's pre-collision velocity back so the jostle +// is basically discared. +//----------------------------------------------------------------------------- +void CNPC_Hunter::JostleVehicleThink() +{ + CBaseEntity *pInflictor = m_hHitByVehicle; + if ( !pInflictor ) + return; + + Vector vecVelDir = pInflictor->GetSmoothedVelocity(); + float flSpeed = VectorNormalize( vecVelDir ); + Vector vecForce = CrossProduct( vecVelDir, Vector( 0, 0, 1 ) ); + if ( DotProduct( vecForce, GetAbsOrigin() ) < DotProduct( vecForce, pInflictor->GetAbsOrigin() ) ) + { + // We're to the left of the vehicle that's hitting us. + vecForce *= -1; + } + + VectorNormalize( vecForce ); + vecForce.z = 1.0; + + float flForceScale = RemapValClamped( flSpeed, hunter_jostle_car_min_speed.GetFloat(), hunter_jostle_car_max_speed.GetFloat(), 50.0f, 150.0f ); + + vecForce *= ( flForceScale * pInflictor->VPhysicsGetObject()->GetMass() ); + + pInflictor->VPhysicsGetObject()->ApplyForceOffset( vecForce, WorldSpaceCenter() ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::OnTakeDamage( const CTakeDamageInfo &info ) +{ + CTakeDamageInfo myInfo = info; + + if ( ( info.GetDamageType() & DMG_CRUSH ) && !( info.GetDamageType() & DMG_VEHICLE ) ) + { + // Don't take damage from physics objects that weren't thrown by the player. + CBaseEntity *pInflictor = info.GetInflictor(); + + IPhysicsObject *pObj = pInflictor->VPhysicsGetObject(); + //Assert( pObj ); + + if ( !pObj || !pInflictor->HasPhysicsAttacker( 4.0 ) ) + { + myInfo.SetDamage( 0 ); + } + else + { + // Physics objects that have flechettes stuck in them spoof + // a flechette hitting us so we dissolve when killed and award + // the achievement of killing a hunter with its flechettes. + CUtlVector<CBaseEntity *> children; + GetAllChildren( pInflictor, children ); + for (int i = 0; i < children.Count(); i++ ) + { + CBaseEntity *pent = children.Element( i ); + if ( dynamic_cast<CHunterFlechette *>( pent ) ) + { + myInfo.SetInflictor( pent ); + myInfo.SetDamageType( myInfo.GetDamageType() | DMG_DISSOLVE ); + } + } + } + } + + return BaseClass::OnTakeDamage( myInfo ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::OnTakeDamage_Alive( const CTakeDamageInfo &info ) +{ + CTakeDamageInfo myInfo = info; + + // don't take damage from my own weapons!!! + // Exception: I "own" a magnade if it's glued to me. + CBaseEntity *pInflictor = info.GetInflictor(); + CBaseEntity *pAttacker = info.GetAttacker(); + if ( pInflictor ) + { + if ( IsStriderBuster( pInflictor ) ) + { + // Get a tesla effect on our hitboxes for a little while. + SetContextThink( &CNPC_Hunter::TeslaThink, gpGlobals->curtime, HUNTER_ZAP_THINK ); + m_flTeslaStopTime = gpGlobals->curtime + 2.0f; + + myInfo.SetDamage( sk_hunter_dmg_from_striderbuster.GetFloat() ) ; + + SetCondition( COND_HUNTER_STAGGERED ); + } + else if ( pInflictor->ClassMatches( GetClassname() ) && !( info.GetDamageType() == DMG_GENERIC ) ) + { + return 0; + } + else if ( pInflictor->ClassMatches( "hunter_flechette" ) ) + { + if ( !( ( CHunterFlechette *)pInflictor )->WasThrownBack() ) + { + // Flechettes only hurt us if they were thrown back at us by the player. This prevents + // hunters from hurting themselves when they walk into their own flechette clusters. + return 0; + } + } + } + + if ( m_EscortBehavior.GetFollowTarget() && m_EscortBehavior.GetFollowTarget() == pAttacker ) + { + return 0; + } + + bool bHitByUnoccupiedCar = false; + if ( ( ( info.GetDamageType() & DMG_CRUSH ) && ( pAttacker && pAttacker->IsPlayer() ) ) || + ( info.GetDamageType() & DMG_VEHICLE ) ) + { + // myInfo, not info! it may have been modified above. + float flDamage = myInfo.GetDamage(); + if ( flDamage < HUNTER_MIN_PHYSICS_DAMAGE ) + { + //DevMsg( "hunter: <<<< ZERO PHYSICS DAMAGE: %f\n", flDamage ); + myInfo.SetDamage( 0 ); + } + else + { + CBaseEntity *pInflictor = info.GetInflictor(); + if ( ( info.GetDamageType() & DMG_VEHICLE ) || + ( pInflictor && pInflictor->GetServerVehicle() && + ( ( bHitByUnoccupiedCar = ( dynamic_cast<CPropVehicleDriveable *>(pInflictor) && static_cast<CPropVehicleDriveable *>(pInflictor)->GetDriver() == NULL ) ) == false ) ) ) + { + // Adjust the damage from vehicles. + flDamage *= sk_hunter_vehicle_damage_scale.GetFloat(); + myInfo.SetDamage( flDamage ); + + // Apply a force to jostle the vehicle that hit us. + // Pick a force direction based on which side we're on relative to the vehicle's motion. + Vector vecVelDir = pInflictor->GetSmoothedVelocity(); + if ( vecVelDir.Length() >= hunter_jostle_car_min_speed.GetFloat() ) + { + EmitSound( "NPC_Hunter.HitByVehicle" ); + m_hHitByVehicle = pInflictor; + SetContextThink( &CNPC_Hunter::JostleVehicleThink, gpGlobals->curtime, HUNTER_JOSTLE_VEHICLE_THINK ); + } + } + + if ( !bHitByUnoccupiedCar ) + { + SetCondition( COND_HUNTER_STAGGERED ); + } + } + + //DevMsg( "hunter: >>>> PHYSICS DAMAGE: %f (was %f)\n", flDamage, info.GetDamage() ); + } + + // Show damage effects if we actually took damage. + if ( ( myInfo.GetDamageType() & ( DMG_CRUSH | DMG_BLAST ) ) && ( myInfo.GetDamage() > 0 ) ) + { + if ( !bHitByUnoccupiedCar ) + SetCondition( COND_HUNTER_STAGGERED ); + } + + if ( HasCondition( COND_HUNTER_STAGGERED ) ) + { + // Throw a bunch of gibs out + Vector vecForceDir = -myInfo.GetDamageForce(); + VectorNormalize( vecForceDir ); + PhysicsDamageEffect( myInfo.GetDamagePosition(), vecForceDir ); + + // Stagger away from the direction the damage came from. + m_vecStaggerDir = myInfo.GetDamageForce(); + VectorNormalize( m_vecStaggerDir ); + } + + // Take less damage from citizens and Alyx, otherwise hunters go down too easily. + float flScale = 1.0; + + if ( pAttacker && + ( ( pAttacker->Classify() == CLASS_CITIZEN_REBEL ) || + ( pAttacker->Classify() == CLASS_PLAYER_ALLY ) || + ( pAttacker->Classify() == CLASS_PLAYER_ALLY_VITAL ) ) ) + { + flScale *= sk_hunter_citizen_damage_scale.GetFloat(); + } + + if ( flScale != 0 ) + { + // We're taking a nonzero amount of damage. + + // If we're not staggering, consider flinching! + if ( !HasCondition( COND_HUNTER_STAGGERED ) ) + { + ConsiderFlinching( info ); + } + + if( pAttacker && pAttacker->IsPlayer() ) + { + // This block of code will distract the Hunter back to the player if the + // player does harm to the Hunter but is not the Hunter's current enemy. + // This is achieved by updating the Hunter's enemy memory of the player and + // making the Hunter's current enemy invalid for a short time. + if( !GetEnemy() || !GetEnemy()->IsPlayer() ) + { + UpdateEnemyMemory( pAttacker, pAttacker->GetAbsOrigin(), this ); + + if( GetEnemy() ) + { + // Gotta forget about this person for a little bit. + GetEnemies()->SetTimeValidEnemy( GetEnemy(), gpGlobals->curtime + HUNTER_IGNORE_ENEMY_TIME ); + } + } + } + + float flDamage = myInfo.GetDamage() * flScale; + myInfo.SetDamage( flDamage ); + } + + int nRet = BaseClass::OnTakeDamage_Alive( myInfo ); + + m_EscortBehavior.OnDamage( myInfo ); + + // Spark at 30% health. + if ( !IsBleeding() && ( GetHealth() <= sk_hunter_health.GetInt() * 0.3 ) ) + { + StartBleeding(); + } + + if ( IsUsingSiegeTargets() && info.GetAttacker() != NULL && info.GetAttacker()->IsPlayer() ) + { + // Defend myself. Try to siege attack immediately. + m_flTimeNextSiegeTargetAttack = gpGlobals->curtime; + } + + return nRet; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::Event_Killed( const CTakeDamageInfo &info ) +{ + // Remember the killing blow to make decisions about ragdolling. + m_nKillingDamageType = info.GetDamageType(); + + if ( m_EscortBehavior.GetFollowTarget() ) + { + if ( AIGetNumFollowers( m_EscortBehavior.GetFollowTarget(), m_iClassname ) == 1 ) + { + m_EscortBehavior.GetEscortTarget()->AlertSound(); + if ( info.GetAttacker() && info.GetAttacker()->IsPlayer() ) + { + m_EscortBehavior.GetEscortTarget()->UpdateEnemyMemory( UTIL_GetLocalPlayer(), UTIL_GetLocalPlayer()->GetAbsOrigin(), this ); + } + } + } + + if ( info.GetDamageType() & DMG_VEHICLE ) + { + bool bWasRunDown = false; + int iRundownCounter = 0; + if ( GetSquad() ) + { + if ( !m_IgnoreVehicleTimer.Expired() ) + { + GetSquad()->GetSquadData( HUNTER_RUNDOWN_SQUADDATA, &iRundownCounter ); + GetSquad()->SetSquadData( HUNTER_RUNDOWN_SQUADDATA, iRundownCounter + 1 ); + bWasRunDown = true; + } + } + + if ( hunter_dodge_debug.GetBool() ) + Msg( "Hunter %d was%s run down\n", entindex(), ( bWasRunDown ) ? "" : " not" ); + + // Death by vehicle! Decrement the hunters to run over counter. + // When the counter reaches zero hunters will start dodging. + if ( GlobalEntity_GetCounter( s_iszHuntersToRunOver ) > 0 ) + { + GlobalEntity_AddToCounter( s_iszHuntersToRunOver, -1 ); + } + } + + // Stop all our thinks + SetContextThink( NULL, 0, HUNTER_BLEED_THINK ); + + StopParticleEffects( this ); + + BaseClass::Event_Killed( info ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::StartBleeding() +{ + // Do this even if we're already bleeding (see OnRestore). + m_bIsBleeding = true; + + // Start gushing blood from our... anus or something. + DispatchParticleEffect( "blood_drip_synth_01", PATTACH_POINT_FOLLOW, this, gm_nHeadBottomAttachment ); + + // Emit spurts of our blood + SetContextThink( &CNPC_Hunter::BleedThink, gpGlobals->curtime + 0.1, HUNTER_BLEED_THINK ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +float CNPC_Hunter::MaxYawSpeed() +{ + if ( IsStriderBuster( GetEnemy() ) ) + { + return 60; + } + + if ( GetActivity() == ACT_HUNTER_ANGRY ) + return 0; + + if ( GetActivity() == ACT_HUNTER_CHARGE_RUN ) + return 5; + + if ( GetActivity() == ACT_HUNTER_IDLE_PLANTED ) + return 0; + + if ( GetActivity() == ACT_HUNTER_RANGE_ATTACK2_UNPLANTED ) + return 180; + + switch ( GetActivity() ) + { + case ACT_RANGE_ATTACK2: + { + return 0; + } + + case ACT_90_LEFT: + case ACT_90_RIGHT: + { + return 45; + } + + case ACT_TURN_LEFT: + case ACT_TURN_RIGHT: + { + return 45; + } + + case ACT_WALK: + { + return 25; + } + + default: + { + return 35; + } + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::IsJumpLegal(const Vector &startPos, const Vector &apex, const Vector &endPos) const +{ + float MAX_JUMP_RISE = 220.0f; + float MAX_JUMP_DISTANCE = 512.0f; + float MAX_JUMP_DROP = 384.0f; + + trace_t tr; + UTIL_TraceHull( startPos, startPos, GetHullMins(), GetHullMaxs(), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr ); + if ( tr.startsolid ) + { + // Trying to start a jump in solid! Consider checking for this in CAI_MoveProbe::JumpMoveLimit. + Assert( 0 ); + return false; + } + + if ( BaseClass::IsJumpLegal( startPos, apex, endPos, MAX_JUMP_RISE, MAX_JUMP_DROP, MAX_JUMP_DISTANCE ) ) + { + return true; + } + return false; +} + + +//----------------------------------------------------------------------------- +// Let the probe know I can run through small debris +// Stolen shamelessly from the Antlion Guard +//----------------------------------------------------------------------------- +bool CNPC_Hunter::ShouldProbeCollideAgainstEntity( CBaseEntity *pEntity ) +{ + if ( s_iszPhysPropClassname != pEntity->m_iClassname ) + return BaseClass::ShouldProbeCollideAgainstEntity( pEntity ); + + if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS ) + { + IPhysicsObject *pPhysObj = pEntity->VPhysicsGetObject(); + + if( pPhysObj && pPhysObj->GetMass() <= 500.0f ) + { + return false; + } + } + + return BaseClass::ShouldProbeCollideAgainstEntity( pEntity ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::DoMuzzleFlash( int nAttachment ) +{ + BaseClass::DoMuzzleFlash(); + + DispatchParticleEffect( "hunter_muzzle_flash", PATTACH_POINT_FOLLOW, this, nAttachment ); + + // Dispatch the elight + CEffectData data; + data.m_nAttachmentIndex = nAttachment; + data.m_nEntIndex = entindex(); + DispatchEffect( "HunterMuzzleFlash", data ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CNPC_Hunter::CountRangedAttackers() +{ + CBaseEntity *pEnemy = GetEnemy(); + if ( !pEnemy ) + { + return 0; + } + + int nAttackers = 0; + for ( int i = 0; i < g_Hunters.Count(); i++ ) + { + CNPC_Hunter *pOtherHunter = g_Hunters[i]; + if ( pOtherHunter->GetEnemy() == pEnemy ) + { + if ( pOtherHunter->IsCurSchedule( SCHED_HUNTER_RANGE_ATTACK2 ) ) + { + nAttackers++; + } + } + } + return nAttackers; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::DelayRangedAttackers( float minDelay, float maxDelay, bool bForced ) +{ + float delayMultiplier = ( g_pGameRules->IsSkillLevel( SKILL_EASY ) ) ? 1.25 : 1.0; + if ( !m_bEnableSquadShootDelay && !bForced ) + { + m_flNextRangeAttack2Time = gpGlobals->curtime + random->RandomFloat( minDelay, maxDelay ) * delayMultiplier; + return; + } + + CBaseEntity *pEnemy = GetEnemy(); + for ( int i = 0; i < g_Hunters.Count(); i++ ) + { + CNPC_Hunter *pOtherHunter = g_Hunters[i]; + if ( pOtherHunter->GetEnemy() == pEnemy ) + { + float nextTime = gpGlobals->curtime + random->RandomFloat( minDelay, maxDelay ) * delayMultiplier; + if ( nextTime > pOtherHunter->m_flNextRangeAttack2Time ) + pOtherHunter->m_flNextRangeAttack2Time = nextTime; + } + } +} + + +//----------------------------------------------------------------------------- +// Given a target to shoot at, decide where to aim. +//----------------------------------------------------------------------------- +void CNPC_Hunter::GetShootDir( Vector &vecDir, const Vector &vecSrc, CBaseEntity *pTargetEntity, bool bStriderBuster, int nShotNum, bool bSingleShot ) +{ + //RestartGesture( ACT_HUNTER_GESTURE_SHOOT ); + + EmitSound( "NPC_Hunter.FlechetteShoot" ); + + Vector vecBodyTarget; + + if( pTargetEntity->Classify() == CLASS_PLAYER_ALLY_VITAL ) + { + // Shooting at Alyx, most likely (in EP2). The attack is designed to displace + // her, not necessarily actually harm her. So shoot at the area around her feet. + vecBodyTarget = pTargetEntity->GetAbsOrigin(); + } + else + { + vecBodyTarget = pTargetEntity->BodyTarget( vecSrc ); + } + + Vector vecTarget = vecBodyTarget; + + Vector vecDelta = pTargetEntity->GetAbsOrigin() - GetAbsOrigin(); + float flDist = vecDelta.Length(); + + if ( !bStriderBuster ) + { + // If we're not firing at a strider buster, miss in an entertaining way for the + // first three shots of each volley. + if ( ( nShotNum < 3 ) && ( flDist > 200 ) ) + { + Vector vecTargetForward; + Vector vecTargetRight; + pTargetEntity->GetVectors( &vecTargetForward, &vecTargetRight, NULL ); + + Vector vecForward; + GetVectors( &vecForward, NULL, NULL ); + + float flDot = DotProduct( vecTargetForward, vecForward ); + + if ( flDot < -0.8f ) + { + // Our target is facing us, shoot the ground between us. + float flPerc = 0.7 + ( 0.1 * nShotNum ); + vecTarget = GetAbsOrigin() + ( flPerc * ( pTargetEntity->GetAbsOrigin() - GetAbsOrigin() ) ); + } + else if ( flDot > 0.8f ) + { + // Our target is facing away from us, shoot to the left or right. + Vector vecMissDir = vecTargetRight; + if ( m_bMissLeft ) + { + vecMissDir *= -1.0f; + } + + vecTarget = pTargetEntity->EyePosition() + ( 36.0f * ( 3 - nShotNum ) ) * vecMissDir; + } + else + { + // Our target is facing vaguely perpendicular to us, shoot across their view. + vecTarget = pTargetEntity->EyePosition() + ( 36.0f * ( 3 - nShotNum ) ) * vecTargetForward; + } + } + // If we can't see them, shoot where we last saw them. + else if ( !HasCondition( COND_SEE_ENEMY ) ) + { + Vector vecDelta = vecTarget - pTargetEntity->GetAbsOrigin(); + vecTarget = m_vecEnemyLastSeen + vecDelta; + } + } + else + { + // If we're firing at a striderbuster, lead it. + float flSpeed = hunter_flechette_speed.GetFloat(); + if ( !flSpeed ) + { + flSpeed = 2500.0f; + } + + flSpeed *= 1.5; + + float flDeltaTime = flDist / flSpeed; + vecTarget = vecTarget + flDeltaTime * pTargetEntity->GetSmoothedVelocity(); + } + + vecDir = vecTarget - vecSrc; + VectorNormalize( vecDir ); +} + + +//----------------------------------------------------------------------------- +// Ensures that we don't exceed our pitch/yaw limits when shooting flechettes. +// Returns true if we had to clamp, false if not. +//----------------------------------------------------------------------------- +bool CNPC_Hunter::ClampShootDir( Vector &vecDir ) +{ + Vector vecDir2D = vecDir; + vecDir2D.z = 0; + + Vector vecForward; + GetVectors( &vecForward, NULL, NULL ); + + Vector vecForward2D = vecForward; + vecForward2D.z = 0; + + float flDot = DotProduct( vecForward2D, vecDir2D ); + if ( flDot >= HUNTER_SHOOT_MAX_YAW_COS ) + { + // No need to clamp. + return false; + } + + Vector vecAxis; + CrossProduct( vecDir, vecForward, vecAxis ); + VectorNormalize( vecAxis ); + + Quaternion q; + AxisAngleQuaternion( vecAxis, -HUNTER_SHOOT_MAX_YAW_DEG, q ); + + matrix3x4_t rot; + QuaternionMatrix( q, rot ); + VectorRotate( vecForward, rot, vecDir ); + VectorNormalize( vecDir ); + + return true; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::ShouldSeekTarget( CBaseEntity *pTargetEntity, bool bStriderBuster ) +{ + bool bSeek = false; + + if ( bStriderBuster ) + { + bool bSeek = false; + + if ( pTargetEntity->VPhysicsGetObject() && ( pTargetEntity->VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) ) + { + bSeek = true; + } + else if ( StriderBuster_NumFlechettesAttached( pTargetEntity ) == 0 ) + { + if ( StriderBuster_IsAttachedStriderBuster(pTargetEntity) ) + { + bSeek = true; + } + else if ( hunter_seek_thrown_striderbusters_tolerance.GetFloat() > 0.0 ) + { + CNPC_Strider *pEscortTarget = m_EscortBehavior.GetEscortTarget(); + if ( pEscortTarget && ( pEscortTarget->GetAbsOrigin() - pTargetEntity->GetAbsOrigin() ).LengthSqr() < Square( hunter_seek_thrown_striderbusters_tolerance.GetFloat() ) ) + { + bSeek = true; + } + } + } + } + + return bSeek; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::BeginVolley( int nNum, float flStartTime ) +{ + m_nFlechettesQueued = nNum; + m_nClampedShots = 0; + m_flNextFlechetteTime = flStartTime; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::ShootFlechette( CBaseEntity *pTargetEntity, bool bSingleShot ) +{ + if ( !pTargetEntity ) + { + Assert( false ); + return false; + } + + int nShotNum = hunter_flechette_volley_size.GetInt() - m_nFlechettesQueued; + + bool bStriderBuster = IsStriderBuster( pTargetEntity ); + + // Choose the next muzzle to shoot from. + Vector vecSrc; + QAngle angMuzzle; + + if ( m_bTopMuzzle ) + { + GetAttachment( gm_nTopGunAttachment, vecSrc, angMuzzle ); + DoMuzzleFlash( gm_nTopGunAttachment ); + } + else + { + GetAttachment( gm_nBottomGunAttachment, vecSrc, angMuzzle ); + DoMuzzleFlash( gm_nBottomGunAttachment ); + } + + m_bTopMuzzle = !m_bTopMuzzle; + + Vector vecDir; + GetShootDir( vecDir, vecSrc, pTargetEntity, bStriderBuster, nShotNum, bSingleShot ); + + bool bClamped = false; + if ( hunter_clamp_shots.GetBool() ) + { + bClamped = ClampShootDir( vecDir ); + } + + CShotManipulator manipulator( vecDir ); + Vector vecShoot; + + if( IsUsingSiegeTargets() && nShotNum >= 2 && (nShotNum % 2) == 0 ) + { + // Near perfect accuracy for these three shots, so they are likely to fly right into the windows. + // NOTE! In siege behavior in the map that this behavior was designed for (ep2_outland_10), the + // Hunters will only ever shoot at siege targets in siege mode. If you allow Hunters in siege mode + // to attack players or other NPCs, this accuracy bonus will apply unless we apply a bit more logic to it. + vecShoot = manipulator.ApplySpread( VECTOR_CONE_1DEGREES * 0.5, 1.0f ); + } + else + { + vecShoot = manipulator.ApplySpread( VECTOR_CONE_4DEGREES, 1.0f ); + } + + QAngle angShoot; + VectorAngles( vecShoot, angShoot ); + + CHunterFlechette *pFlechette = CHunterFlechette::FlechetteCreate( vecSrc, angShoot, this ); + + pFlechette->AddEffects( EF_NOSHADOW ); + + vecShoot *= hunter_flechette_speed.GetFloat(); + + pFlechette->Shoot( vecShoot, bStriderBuster ); + + if ( ShouldSeekTarget( pTargetEntity, bStriderBuster ) ) + { + pFlechette->SetSeekTarget( pTargetEntity ); + } + + if( nShotNum == 1 && pTargetEntity->Classify() == CLASS_PLAYER_ALLY_VITAL ) + { + // Make this person afraid and react to ME, not to the flechettes. + // Otherwise they could be scared into running towards the hunter. + CSoundEnt::InsertSound( SOUND_DANGER|SOUND_CONTEXT_REACT_TO_SOURCE|SOUND_CONTEXT_EXCLUDE_COMBINE, pTargetEntity->EyePosition(), 180.0f, 2.0f, this ); + } + + return bClamped; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +Vector CNPC_Hunter::LeftFootHit( float eventtime ) +{ + Vector footPosition; + + GetAttachment( "left foot", footPosition ); + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "NPC_Hunter.Footstep", &footPosition, eventtime ); + + FootFX( footPosition ); + + return footPosition; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +Vector CNPC_Hunter::RightFootHit( float eventtime ) +{ + Vector footPosition; + + GetAttachment( "right foot", footPosition ); + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "NPC_Hunter.Footstep", &footPosition, eventtime ); + FootFX( footPosition ); + + return footPosition; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +Vector CNPC_Hunter::BackFootHit( float eventtime ) +{ + Vector footPosition; + + GetAttachment( "back foot", footPosition ); + CPASAttenuationFilter filter( this ); + EmitSound( filter, entindex(), "NPC_Hunter.BackFootstep", &footPosition, eventtime ); + FootFX( footPosition ); + + return footPosition; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::FootFX( const Vector &origin ) +{ + return; + + // dvs TODO: foot dust? probably too expensive for these guys + /*trace_t tr; + AI_TraceLine( origin, origin - Vector(0,0,100), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + float yaw = random->RandomInt(0,120); + for ( int i = 0; i < 3; i++ ) + { + Vector dir = UTIL_YawToVector( yaw + i*120 ) * 10; + VectorNormalize( dir ); + dir.z = 0.25; + VectorNormalize( dir ); + g_pEffects->Dust( tr.endpos, dir, 12, 50 ); + }*/ +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +CBaseEntity *CNPC_Hunter::GetEnemyVehicle() +{ + if ( GetEnemy() == NULL ) + return NULL; + + CBaseCombatCharacter *pCCEnemy = GetEnemy()->MyCombatCharacterPointer(); + if ( pCCEnemy != NULL ) + return pCCEnemy->GetVehicleEntity(); + + return NULL; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::DrawDebugGeometryOverlays() +{ + if (m_debugOverlays & OVERLAY_BBOX_BIT) + { + float flViewRange = acos(0.8); + Vector vEyeDir = EyeDirection2D( ); + Vector vLeftDir, vRightDir; + float fSin, fCos; + SinCos( flViewRange, &fSin, &fCos ); + + vLeftDir.x = vEyeDir.x * fCos - vEyeDir.y * fSin; + vLeftDir.y = vEyeDir.x * fSin + vEyeDir.y * fCos; + vLeftDir.z = vEyeDir.z; + fSin = sin(-flViewRange); + fCos = cos(-flViewRange); + vRightDir.x = vEyeDir.x * fCos - vEyeDir.y * fSin; + vRightDir.y = vEyeDir.x * fSin + vEyeDir.y * fCos; + vRightDir.z = vEyeDir.z; + + int nSeq = GetSequence(); + if ( ( GetEntryNode( nSeq ) == gm_nPlantedNode ) && ( GetExitNode( nSeq ) == gm_nPlantedNode ) ) + { + // planted - green + NDebugOverlay::Box( GetAbsOrigin(), GetHullMins(), GetHullMaxs(), 0, 255, 0, 128, 0 ); + } + else if ( ( GetEntryNode( nSeq ) == gm_nUnplantedNode ) && ( GetExitNode( nSeq ) == gm_nUnplantedNode ) ) + { + // unplanted - blue + NDebugOverlay::Box( GetAbsOrigin(), GetHullMins(), GetHullMaxs(), 0, 0, 255, 128, 0 ); + } + else if ( ( GetEntryNode( nSeq ) == gm_nUnplantedNode ) && ( GetExitNode( nSeq ) == gm_nPlantedNode ) ) + { + // planting transition - cyan + NDebugOverlay::Box( GetAbsOrigin(), GetHullMins(), GetHullMaxs(), 0, 255, 255, 128, 0 ); + } + else if ( ( GetEntryNode( nSeq ) == gm_nPlantedNode ) && ( GetExitNode( nSeq ) == gm_nUnplantedNode ) ) + { + // unplanting transition - purple + NDebugOverlay::Box( GetAbsOrigin(), GetHullMins(), GetHullMaxs(), 255, 0, 255, 128, 0 ); + } + else + { + // unknown / other node - red + Msg( "UNKNOWN: %s\n", GetSequenceName( GetSequence() ) ); + NDebugOverlay::Box( GetAbsOrigin(), GetHullMins(), GetHullMaxs(), 255, 0, 0, 128, 0 ); + } + + NDebugOverlay::BoxDirection(EyePosition(), Vector(0,0,-1), Vector(200,0,1), vLeftDir, 255, 0, 0, 50, 0 ); + NDebugOverlay::BoxDirection(EyePosition(), Vector(0,0,-1), Vector(200,0,1), vRightDir, 255, 0, 0, 50, 0 ); + NDebugOverlay::BoxDirection(EyePosition(), Vector(0,0,-1), Vector(200,0,1), vEyeDir, 0, 255, 0, 50, 0 ); + NDebugOverlay::Box(EyePosition(), -Vector(2,2,2), Vector(2,2,2), 0, 255, 0, 128, 0 ); + } + + m_EscortBehavior.DrawDebugGeometryOverlays(); + + BaseClass::DrawDebugGeometryOverlays(); +} + + +//----------------------------------------------------------------------------- +// Player has illuminated this NPC with the flashlight +//----------------------------------------------------------------------------- +void CNPC_Hunter::PlayerHasIlluminatedNPC( CBasePlayer *pPlayer, float flDot ) +{ + if ( m_bFlashlightInEyes ) + return; + + // Ignore the flashlight if it's not shining at my eyes + if ( PlayerFlashlightOnMyEyes( pPlayer ) ) + { + //Msg( ">>>> SHINING FLASHLIGHT ON ME\n" ); + m_bFlashlightInEyes = true; + SetExpression( "scenes/npc/hunter/hunter_eyeclose.vcd" ); + m_flPupilDilateTime = gpGlobals->curtime + 0.2f; + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CNPC_Hunter::PlayerFlashlightOnMyEyes( CBasePlayer *pPlayer ) +{ + Vector vecEyes, vecEyeForward, vecPlayerForward; + GetAttachment( gm_nTopGunAttachment, vecEyes, &vecEyeForward ); + pPlayer->EyeVectors( &vecPlayerForward ); + + Vector vecToEyes = (vecEyes - pPlayer->EyePosition()); + //float flDist = VectorNormalize( vecToEyes ); + + float flDot = DotProduct( vecPlayerForward, vecToEyes ); + if ( flDot < 0.98 ) + return false; + + // Check facing to ensure we're in front of her + Vector los = ( pPlayer->EyePosition() - EyePosition() ); + los.z = 0; + VectorNormalize( los ); + Vector facingDir = EyeDirection2D(); + flDot = DotProduct( los, facingDir ); + return ( flDot > 0.3 ); +} + + +//----------------------------------------------------------------------------- +// Return a random expression for the specified state to play over +// the state's expression loop. +//----------------------------------------------------------------------------- +const char *CNPC_Hunter::SelectRandomExpressionForState( NPC_STATE state ) +{ + if ( m_bFlashlightInEyes ) + return NULL; + + if ( !hunter_random_expressions.GetBool() ) + return NULL; + + char *szExpressions[4] = + { + "scenes/npc/hunter/hunter_scan.vcd", + "scenes/npc/hunter/hunter_eyeclose.vcd", + "scenes/npc/hunter/hunter_roar.vcd", + "scenes/npc/hunter/hunter_pain.vcd" + }; + + int nIndex = random->RandomInt( 0, 3 ); + //Msg( "RANDOM Expression: %s\n", szExpressions[nIndex] ); + return szExpressions[nIndex]; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::PlayExpressionForState( NPC_STATE state ) +{ + if ( m_bFlashlightInEyes ) + { + return; + } + + BaseClass::PlayExpressionForState( state ); +} + + +//----------------------------------------------------------------------------- +// TODO: remove if we're not doing striderbuster stuff +//----------------------------------------------------------------------------- +void CNPC_Hunter::StriderBusterAttached( CBaseEntity *pAttached ) +{ + // Add another to the list + m_hAttachedBusters.AddToTail( pAttached ); + + SetCondition( COND_HUNTER_HIT_BY_STICKYBOMB ); + if (m_hAttachedBusters.Count() == 1) + { + EmitSound( "NPC_Hunter.Alert" ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::StriderBusterDetached( CBaseEntity *pAttached ) +{ + int elem = m_hAttachedBusters.Find(pAttached); + if (elem >= 0) + { + m_hAttachedBusters.FastRemove(elem); + } +} + + +//----------------------------------------------------------------------------- +// Set direction that the hunter aims his body and eyes (guns). +//----------------------------------------------------------------------------- +void CNPC_Hunter::SetAim( const Vector &aimDir, float flInterval ) +{ + QAngle angDir; + VectorAngles( aimDir, angDir ); + float curPitch = GetPoseParameter( gm_nBodyPitchPoseParam ); + float curYaw = GetPoseParameter( gm_nBodyYawPoseParam ); + + float newPitch; + float newYaw; + + if ( GetEnemy() ) + { + // clamp and dampen movement + newPitch = curPitch + 0.8 * UTIL_AngleDiff( UTIL_ApproachAngle( angDir.x, curPitch, 20 ), curPitch ); + + float flRelativeYaw = UTIL_AngleDiff( angDir.y, GetAbsAngles().y ); + newYaw = curYaw + UTIL_AngleDiff( flRelativeYaw, curYaw ); + } + else + { + // Sweep your weapon more slowly if you're not fighting someone + newPitch = curPitch + 0.6 * UTIL_AngleDiff( UTIL_ApproachAngle( angDir.x, curPitch, 20 ), curPitch ); + + float flRelativeYaw = UTIL_AngleDiff( angDir.y, GetAbsAngles().y ); + newYaw = curYaw + 0.6 * UTIL_AngleDiff( flRelativeYaw, curYaw ); + } + + newPitch = AngleNormalize( newPitch ); + newYaw = AngleNormalize( newYaw ); + + //Msg( "pitch=%f, yaw=%f\n", newPitch, newYaw ); + + SetPoseParameter( gm_nAimPitchPoseParam, 0 ); + SetPoseParameter( gm_nAimYawPoseParam, 0 ); + + SetPoseParameter( gm_nBodyPitchPoseParam, clamp( newPitch, -45, 45 ) ); + SetPoseParameter( gm_nBodyYawPoseParam, clamp( newYaw, -45, 45 ) ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::RelaxAim( float flInterval ) +{ + float curPitch = GetPoseParameter( gm_nBodyPitchPoseParam ); + float curYaw = GetPoseParameter( gm_nBodyYawPoseParam ); + + // dampen existing aim + float newPitch = AngleNormalize( UTIL_ApproachAngle( 0, curPitch, 3 ) ); + float newYaw = AngleNormalize( UTIL_ApproachAngle( 0, curYaw, 2 ) ); + + SetPoseParameter( gm_nAimPitchPoseParam, 0 ); + SetPoseParameter( gm_nAimYawPoseParam, 0 ); + + SetPoseParameter( gm_nBodyPitchPoseParam, clamp( newPitch, -45, 45 ) ); + SetPoseParameter( gm_nBodyYawPoseParam, clamp( newYaw, -45, 45 ) ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CNPC_Hunter::UpdateAim() +{ + if ( !GetModelPtr() || !GetModelPtr()->SequencesAvailable() ) + return; + + float flInterval = GetAnimTimeInterval(); + + // Some activities look bad if we're giving our enemy the stinkeye. + int eActivity = GetActivity(); + + if ( GetEnemy() && + GetState() != NPC_STATE_SCRIPT && + ( eActivity != ACT_HUNTER_CHARGE_CRASH ) && + ( eActivity != ACT_HUNTER_CHARGE_HIT ) ) + { + Vector vecShootOrigin; + + vecShootOrigin = Weapon_ShootPosition(); + Vector vecShootDir = GetShootEnemyDir( vecShootOrigin, false ); + + SetAim( vecShootDir, flInterval ); + } + else + { + RelaxAim( flInterval ); + } +} + + +//----------------------------------------------------------------------------- +// Don't become a ragdoll until we've finished our death anim +//----------------------------------------------------------------------------- +bool CNPC_Hunter::CanBecomeRagdoll() +{ + return ( m_nKillingDamageType & DMG_CRUSH ) || + IsCurSchedule( SCHED_DIE, false ) || // Finished playing death anim, time to ragdoll + IsCurSchedule( SCHED_HUNTER_CHARGE_ENEMY, false ) || // While moving, it looks better to ragdoll instantly + IsCurSchedule( SCHED_SCRIPTED_RUN, false ) || + ( GetActivity() == ACT_WALK ) || ( GetActivity() == ACT_RUN ) || + GetCurSchedule() == NULL; // Failsafe +} + + +//----------------------------------------------------------------------------- +// Determines the best type of death anim to play based on how we died. +//----------------------------------------------------------------------------- +Activity CNPC_Hunter::GetDeathActivity() +{ + return ACT_DIESIMPLE; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_HunterEscortBehavior::OnDamage( const CTakeDamageInfo &info ) +{ + if ( info.GetDamage() > 0 && info.GetAttacker()->IsPlayer() && + GetFollowTarget() && ( AIGetNumFollowers( GetFollowTarget() ) > 1 ) && + ( GetOuter()->GetSquad()->GetSquadSoundWaitTime() <= gpGlobals->curtime ) ) // && !FarFromFollowTarget() + { + // Start the clock ticking. We'll return the the strider when the timer elapses. + m_flTimeEscortReturn = gpGlobals->curtime + random->RandomFloat( 15.0f, 25.0f ); + GetOuter()->GetSquad()->SetSquadSoundWaitTime( m_flTimeEscortReturn + 1.0 ); // prevent others from breaking escort + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_HunterEscortBehavior::BuildScheduleTestBits() +{ + BaseClass::BuildScheduleTestBits(); + + if ( ( m_flTimeEscortReturn != 0 ) && ( gpGlobals->curtime > m_flTimeEscortReturn ) ) + { + // We're delinquent! Return to strider! + GetOuter()->ClearCustomInterruptCondition( COND_NEW_ENEMY ); + GetOuter()->ClearCustomInterruptCondition( COND_SEE_ENEMY ); + GetOuter()->ClearCustomInterruptCondition( COND_SEE_HATE ); + GetOuter()->ClearCustomInterruptCondition( COND_CAN_RANGE_ATTACK1 ); + GetOuter()->ClearCustomInterruptCondition( COND_CAN_RANGE_ATTACK2 ); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_HunterEscortBehavior::CheckBreakEscort() +{ + if ( m_flTimeEscortReturn != 0 && ( FarFromFollowTarget() || gpGlobals->curtime >= m_flTimeEscortReturn ) ) + { + if ( FarFromFollowTarget() ) + { + m_flTimeEscortReturn = gpGlobals->curtime; + } + else + { + m_flTimeEscortReturn = 0; + } + if ( GetOuter()->GetSquad() ) + { + GetOuter()->GetSquad()->SetSquadSoundWaitTime( gpGlobals->curtime + random->RandomFloat( 5.0f, 12.0f ) ); + } + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_HunterEscortBehavior::GatherConditionsNotActive( void ) +{ + if ( m_bEnabled ) + { + DistributeFreeHunters(); + } + + BaseClass::GatherConditionsNotActive(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_HunterEscortBehavior::GatherConditions( void ) +{ + m_bEnabled = true; + + DistributeFreeHunters(); + + BaseClass::GatherConditions(); + + if ( GetEnemy() && GetEnemy()->IsPlayer() && HasCondition( COND_SEE_ENEMY ) ) + { + if ( GetOuter()->GetSquad()->GetSquadSoundWaitTime() <= gpGlobals->curtime && ((CBasePlayer *)GetEnemy())->IsInAVehicle() ) + { + m_flTimeEscortReturn = gpGlobals->curtime + random->RandomFloat( 15.0f, 25.0f ); + GetOuter()->GetSquad()->SetSquadSoundWaitTime( m_flTimeEscortReturn + 1.0 ); // prevent others from breaking escort + } + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool CAI_HunterEscortBehavior::ShouldFollow() +{ + if ( IsStriderBuster( GetEnemy() ) ) + return false; + + if ( HasCondition( COND_HEAR_PHYSICS_DANGER ) ) + return false; + + if ( m_flTimeEscortReturn <= gpGlobals->curtime ) + { + return CAI_FollowBehavior::ShouldFollow(); + } + + return false; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_HunterEscortBehavior::BeginScheduleSelection() +{ + BaseClass::BeginScheduleSelection(); + Assert( m_SavedDistTooFar == GetOuter()->m_flDistTooFar ); + GetOuter()->m_flDistTooFar *= 2; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CAI_HunterEscortBehavior::SelectSchedule() +{ + if( m_FollowDelay.IsRunning() && !m_FollowDelay.Expired() ) + { + return FollowCallBaseSelectSchedule(); + } + return BaseClass::SelectSchedule(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CAI_HunterEscortBehavior::FollowCallBaseSelectSchedule() +{ + if ( GetOuter()->GetState() == NPC_STATE_COMBAT ) + { + return GetOuter()->SelectCombatSchedule(); + } + + return BaseClass::FollowCallBaseSelectSchedule(); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_HunterEscortBehavior::StartTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_MOVE_TO_FOLLOW_POSITION: + { + if ( GetEnemy() ) + { + if ( GetOuter()->OccupyStrategySlot( SQUAD_SLOT_RUN_SHOOT ) ) + { + if ( GetOuter()->GetSquad()->GetSquadMemberNearestTo( GetEnemy()->GetAbsOrigin() ) == GetOuter() ) + { + GetOuter()->BeginVolley( NUM_FLECHETTE_VOLLEY_ON_FOLLOW, gpGlobals->curtime + 1.0 + random->RandomFloat( 0, .25 ) + random->RandomFloat( 0, .25 ) ); + } + else + { + GetOuter()->VacateStrategySlot(); + } + } + } + BaseClass::StartTask( pTask ); + break; + } + + default: + BaseClass::StartTask( pTask ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_HunterEscortBehavior::RunTask( const Task_t *pTask ) +{ + switch( pTask->iTask ) + { + case TASK_MOVE_TO_FOLLOW_POSITION: + { + if ( !GetFollowTarget() ) + { + TaskFail( FAIL_NO_TARGET ); + } + else + { + if ( GetEnemy() ) + { + CNPC_Hunter *pHunter = GetOuter(); + Vector vecEnemyLKP = pHunter->GetEnemyLKP(); + pHunter->AddFacingTarget( pHunter->GetEnemy(), vecEnemyLKP, 1.0, 0.8 ); + bool bVacate = false; + + bool bHasSlot = pHunter->HasStrategySlot( SQUAD_SLOT_RUN_SHOOT ); + if ( HasCondition( COND_SEE_ENEMY ) ) + { + float maxDist = hunter_flechette_max_range.GetFloat() * 3; + float distSq = ( pHunter->GetAbsOrigin() - pHunter->GetEnemy()->GetAbsOrigin() ).Length2DSqr(); + + if ( distSq < Square( maxDist ) ) + { + if ( gpGlobals->curtime >= pHunter->m_flNextFlechetteTime ) + { + if ( !bHasSlot ) + { + if ( GetOuter()->OccupyStrategySlot( SQUAD_SLOT_RUN_SHOOT ) ) + { + if ( GetOuter()->GetSquad()->GetSquadMemberNearestTo( GetEnemy()->GetAbsOrigin() ) == GetOuter() ) + { + bHasSlot = true; + } + else + { + GetOuter()->VacateStrategySlot(); + } + } + } + + if ( bHasSlot ) + { + // Start the firing sound. + //CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + //if ( controller.SoundGetVolume( pHunter->m_pGunFiringSound ) == 0.0f ) + //{ + // controller.SoundChangeVolume( pHunter->m_pGunFiringSound, 1.0f, 0.0f ); + //} + + pHunter->ShootFlechette( GetEnemy(), true ); + + if ( --pHunter->m_nFlechettesQueued > 0 ) + { + pHunter->m_flNextFlechetteTime = gpGlobals->curtime + hunter_flechette_delay.GetFloat(); + } + else + { + // Stop the firing sound. + //CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); + //controller.SoundChangeVolume( pHunter->m_pGunFiringSound, 0, 0.01f ); + + bVacate = true; + pHunter->BeginVolley( NUM_FLECHETTE_VOLLEY_ON_FOLLOW, gpGlobals->curtime + 1.0 + random->RandomFloat( 0, .25 ) + random->RandomFloat( 0, .25 ) ); + } + } + } + } + else if ( bHasSlot ) + { + bVacate = true; + } + } + else if ( bHasSlot ) + { + bVacate = true; + } + + if ( bVacate ) + { + pHunter->VacateStrategySlot(); + } + } + + if ( m_FollowAttackTimer.Expired() && IsFollowTargetInRange( .8 )) + { + m_FollowAttackTimer.Set( 8, 24 ); + TaskComplete(); + } + else + { + BaseClass::RunTask( pTask ); + } + } + break; + } + + default: + BaseClass::RunTask( pTask ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_HunterEscortBehavior::FindFreeHunters( CUtlVector<CNPC_Hunter *> *pFreeHunters ) +{ + pFreeHunters->EnsureCapacity( g_Hunters.Count() ); + int i; + + for ( i = 0; i < g_Hunters.Count(); i++ ) + { + CNPC_Hunter *pHunter = g_Hunters[i]; + if ( pHunter->IsAlive() && pHunter->m_EscortBehavior.m_bEnabled ) + { + if ( pHunter->m_EscortBehavior.GetFollowTarget() == NULL || !pHunter->m_EscortBehavior.GetFollowTarget()->IsAlive() ) + { + pFreeHunters->AddToTail( pHunter); + } + } + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_HunterEscortBehavior::DistributeFreeHunters() +{ + if ( g_TimeLastDistributeFreeHunters != -1 && gpGlobals->curtime - g_TimeLastDistributeFreeHunters < FREE_HUNTER_DISTRIBUTE_INTERVAL ) + { + return; + } + + g_TimeLastDistributeFreeHunters = gpGlobals->curtime; + + CUtlVector<CNPC_Hunter *> freeHunters; + int i; + FindFreeHunters( &freeHunters ); + + CAI_BaseNPC **ppNPCs = g_AI_Manager.AccessAIs(); + for ( i = 0; i < g_AI_Manager.NumAIs() && freeHunters.Count(); i++ ) + { + int nToAdd; + CNPC_Strider *pStrider = ( ppNPCs[i]->IsAlive() ) ? dynamic_cast<CNPC_Strider *>( ppNPCs[i] ) : NULL; + if ( pStrider && !pStrider->CarriedByDropship() ) + { + if ( ( nToAdd = 3 - AIGetNumFollowers( pStrider ) ) > 0 ) + { + for ( int j = freeHunters.Count() - 1; j >= 0 && nToAdd > 0; --j ) + { + DevMsg( "npc_hunter %d assigned to npc_strider %d\n", freeHunters[j]->entindex(), pStrider->entindex() ); + freeHunters[j]->FollowStrider( pStrider ); + freeHunters.FastRemove( j ); + nToAdd--; + } + } + } + } + + for ( i = 0; i < freeHunters.Count(); i++ ) + { + //DevMsg( "npc_hunter %d assigned to free_hunters_squad\n", freeHunters[i]->entindex() ); + freeHunters[i]->m_EscortBehavior.SetFollowTarget( NULL ); + freeHunters[i]->AddToSquad( AllocPooledString( "free_hunters_squad" ) ); + } + +#if 0 + CBaseEntity *pHunterMaker = gEntList.FindEntityByClassname( NULL, "npc_hunter_maker" ); // TODO: this picks the same one every time! + if ( pHunterMaker ) + { + for ( i = 0; i < freeHunters.Count(); i++ ) + { + freeHunters[i]->m_EscortBehavior.SetFollowTarget( pHunterMaker ); + } + } +#endif +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CAI_HunterEscortBehavior::DrawDebugGeometryOverlays() +{ + if ( !GetFollowTarget() ) + return; + + Vector vecFollowPos = GetGoalPosition(); + if ( FarFromFollowTarget() ) + { + if ( gpGlobals->curtime >= m_flTimeEscortReturn ) + { + NDebugOverlay::HorzArrow( GetOuter()->GetAbsOrigin(), vecFollowPos, 16.0f, 255, 0, 0, 0, true, 0 ); + } + else + { + NDebugOverlay::HorzArrow( GetOuter()->GetAbsOrigin(), vecFollowPos, 16.0f, 255, 255, 0, 0, true, 0 ); + } + } + else + { + NDebugOverlay::HorzArrow( GetOuter()->GetAbsOrigin(), vecFollowPos, 16.0f, 0, 255, 0, 0, true, 0 ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool Hunter_IsHunter(CBaseEntity *pEnt) +{ + return dynamic_cast<CNPC_Hunter *>(pEnt) != NULL; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void Hunter_StriderBusterLaunched( CBaseEntity *pBuster ) +{ + CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs(); + int nAIs = g_AI_Manager.NumAIs(); + + for ( int i = 0; i < nAIs; i++ ) + { + CAI_BaseNPC *pNPC = ppAIs[ i ]; + if ( pNPC && ( pNPC->Classify() == CLASS_COMBINE_HUNTER ) && pNPC->m_lifeState == LIFE_ALIVE ) + { + if ( !pNPC->GetEnemy() || !IsStriderBuster( pNPC->GetEnemy() ) ) + { + Vector vecDelta = pNPC->GetAbsOrigin() - pBuster->GetAbsOrigin(); + if ( vecDelta.Length2DSqr() < 9437184.0f ) // 3072 * 3072 + { + pNPC->SetEnemy( pBuster ); + pNPC->SetState( NPC_STATE_COMBAT ); + pNPC->UpdateEnemyMemory( pBuster, pBuster->GetAbsOrigin() ); + + // Stop whatever we're doing. + pNPC->SetCondition( COND_SCHEDULE_DONE ); + } + } + } + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void Hunter_StriderBusterAttached( CBaseEntity *pHunter, CBaseEntity *pAttached ) +{ + Assert(dynamic_cast<CNPC_Hunter *>(pHunter)); + + static_cast<CNPC_Hunter *>(pHunter)->StriderBusterAttached(pAttached); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void Hunter_StriderBusterDetached( CBaseEntity *pHunter, CBaseEntity *pAttached ) +{ + Assert(dynamic_cast<CNPC_Hunter *>(pHunter)); + + static_cast<CNPC_Hunter *>(pHunter)->StriderBusterDetached(pAttached); +} + +//------------------------------------------------------------------------------------------------- +// +// ep2_outland_12 custom npc makers +// +//------------------------------------------------------------------------------------------------- + +class CHunterMaker : public CTemplateNPCMaker +{ + typedef CTemplateNPCMaker BaseClass; +public: + void MakeMultipleNPCS( int nNPCs ) + { + const float MIN_HEALTH_PCT = 0.2; + + CUtlVector<CNPC_Hunter *> candidates; + CUtlVectorFixed<CNPC_Hunter *, 3> freeHunters; + CAI_HunterEscortBehavior::FindFreeHunters( &candidates ); + + freeHunters.EnsureCapacity( 3 ); + int i; + + for ( i = 0; i < candidates.Count() && freeHunters.Count() < 3; i++ ) + { + if ( candidates[i]->GetHealth() > candidates[i]->GetMaxHealth() * MIN_HEALTH_PCT ) + { + freeHunters.AddToTail( candidates[i] ); + } + } + + int nRequested = nNPCs; + if ( nNPCs < 3 ) + { + nNPCs = MIN( 3, nNPCs + freeHunters.Count() ); + } + + int nSummoned = 0; + for ( i = 0; i < freeHunters.Count() && nNPCs; i++ ) + { + freeHunters[i]->m_EscortBehavior.SetFollowTarget( this ); // this will make them not "free" + freeHunters[i]->SetName( m_iszTemplateName ); // this will force the hunter to get the FollowStrider input + nNPCs--; + nSummoned++; + } + + DevMsg( "Requested %d to spawn, Summoning %d free hunters, spawning %d new hunters\n", nRequested, nSummoned, nNPCs ); + if ( nNPCs ) + { + BaseClass::MakeMultipleNPCS( nNPCs ); + } + } +}; + +LINK_ENTITY_TO_CLASS( npc_hunter_maker, CHunterMaker ); + + +//------------------------------------------------------------------------------------------------- +// +// Schedules +// +//------------------------------------------------------------------------------------------------- +AI_BEGIN_CUSTOM_NPC( npc_hunter, CNPC_Hunter ) + + DECLARE_TASK( TASK_HUNTER_AIM ) + DECLARE_TASK( TASK_HUNTER_FIND_DODGE_POSITION ) + DECLARE_TASK( TASK_HUNTER_DODGE ) + DECLARE_TASK( TASK_HUNTER_PRE_RANGE_ATTACK2 ) + DECLARE_TASK( TASK_HUNTER_SHOOT_COMMIT ) + DECLARE_TASK( TASK_HUNTER_ANNOUNCE_FLANK ) + DECLARE_TASK( TASK_HUNTER_BEGIN_FLANK ) + DECLARE_TASK( TASK_HUNTER_STAGGER ) + DECLARE_TASK( TASK_HUNTER_CORNERED_TIMER ) + DECLARE_TASK( TASK_HUNTER_FIND_SIDESTEP_POSITION ) + DECLARE_TASK( TASK_HUNTER_CHARGE ) + DECLARE_TASK( TASK_HUNTER_FINISH_RANGE_ATTACK ) + DECLARE_TASK( TASK_HUNTER_WAIT_FOR_MOVEMENT_FACING_ENEMY ) + DECLARE_TASK( TASK_HUNTER_CHARGE_DELAY ) + + DECLARE_ACTIVITY( ACT_HUNTER_DEPLOYRA2 ) + DECLARE_ACTIVITY( ACT_HUNTER_DODGER ) + DECLARE_ACTIVITY( ACT_HUNTER_DODGEL ) + DECLARE_ACTIVITY( ACT_HUNTER_GESTURE_SHOOT ) + DECLARE_ACTIVITY( ACT_HUNTER_FLINCH_STICKYBOMB ) + DECLARE_ACTIVITY( ACT_HUNTER_STAGGER ) + DECLARE_ACTIVITY( ACT_DI_HUNTER_MELEE ) + DECLARE_ACTIVITY( ACT_DI_HUNTER_THROW ) + DECLARE_ACTIVITY( ACT_HUNTER_MELEE_ATTACK1_VS_PLAYER ) + DECLARE_ACTIVITY( ACT_HUNTER_ANGRY ) + DECLARE_ACTIVITY( ACT_HUNTER_WALK_ANGRY ) + DECLARE_ACTIVITY( ACT_HUNTER_FOUND_ENEMY ) + DECLARE_ACTIVITY( ACT_HUNTER_FOUND_ENEMY_ACK ) + DECLARE_ACTIVITY( ACT_HUNTER_CHARGE_START ) + DECLARE_ACTIVITY( ACT_HUNTER_CHARGE_RUN ) + DECLARE_ACTIVITY( ACT_HUNTER_CHARGE_STOP ) + DECLARE_ACTIVITY( ACT_HUNTER_CHARGE_CRASH ) + DECLARE_ACTIVITY( ACT_HUNTER_CHARGE_HIT ) + DECLARE_ACTIVITY( ACT_HUNTER_RANGE_ATTACK2_UNPLANTED ) + DECLARE_ACTIVITY( ACT_HUNTER_IDLE_PLANTED ) + DECLARE_ACTIVITY( ACT_HUNTER_FLINCH_N ) + DECLARE_ACTIVITY( ACT_HUNTER_FLINCH_S ) + DECLARE_ACTIVITY( ACT_HUNTER_FLINCH_E ) + DECLARE_ACTIVITY( ACT_HUNTER_FLINCH_W ) + + DECLARE_INTERACTION( g_interactionHunterFoundEnemy ); + + DECLARE_SQUADSLOT( SQUAD_SLOT_HUNTER_CHARGE ) + DECLARE_SQUADSLOT( SQUAD_SLOT_HUNTER_FLANK_FIRST ) + DECLARE_SQUADSLOT( SQUAD_SLOT_RUN_SHOOT ) + + DECLARE_CONDITION( COND_HUNTER_SHOULD_PATROL ) + DECLARE_CONDITION( COND_HUNTER_FORCED_FLANK_ENEMY ) + DECLARE_CONDITION( COND_HUNTER_CAN_CHARGE_ENEMY ) + DECLARE_CONDITION( COND_HUNTER_STAGGERED ) + DECLARE_CONDITION( COND_HUNTER_IS_INDOORS ) + DECLARE_CONDITION( COND_HUNTER_HIT_BY_STICKYBOMB ) + DECLARE_CONDITION( COND_HUNTER_SEE_STRIDERBUSTER ) + DECLARE_CONDITION( COND_HUNTER_FORCED_DODGE ) + DECLARE_CONDITION( COND_HUNTER_INCOMING_VEHICLE ) + DECLARE_CONDITION( COND_HUNTER_NEW_HINTGROUP ) + DECLARE_CONDITION( COND_HUNTER_CANT_PLANT ) + DECLARE_CONDITION( COND_HUNTER_SQUADMATE_FOUND_ENEMY ) + + DECLARE_ANIMEVENT( AE_HUNTER_FOOTSTEP_LEFT ) + DECLARE_ANIMEVENT( AE_HUNTER_FOOTSTEP_RIGHT ) + DECLARE_ANIMEVENT( AE_HUNTER_FOOTSTEP_BACK ) + DECLARE_ANIMEVENT( AE_HUNTER_MELEE_ANNOUNCE ) + DECLARE_ANIMEVENT( AE_HUNTER_MELEE_ATTACK_LEFT ) + DECLARE_ANIMEVENT( AE_HUNTER_MELEE_ATTACK_RIGHT ) + DECLARE_ANIMEVENT( AE_HUNTER_DIE ) + DECLARE_ANIMEVENT( AE_HUNTER_SPRAY_BLOOD ) + DECLARE_ANIMEVENT( AE_HUNTER_START_EXPRESSION ) + DECLARE_ANIMEVENT( AE_HUNTER_END_EXPRESSION ) + + //========================================================= + // Attack (Deploy/shoot/finish) + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_RANGE_ATTACK1, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_HUNTER_SHOOT_COMMIT 0" + " TASK_RANGE_ATTACK1 0" + " " + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_LOST_ENEMY" + " COND_ENEMY_OCCLUDED" + " COND_WEAPON_SIGHT_OCCLUDED" + " COND_TOO_CLOSE_TO_ATTACK" + " COND_TOO_FAR_TO_ATTACK" + " COND_NOT_FACING_ATTACK" + ) + + //========================================================= + // Attack (Deploy/shoot/finish) + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_RANGE_ATTACK2, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_HUNTER_PRE_RANGE_ATTACK2 0" + " TASK_HUNTER_SHOOT_COMMIT 0" + " TASK_RANGE_ATTACK2 0" + " TASK_HUNTER_FINISH_RANGE_ATTACK 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT 0.4" + " TASK_WAIT_RANDOM 0.2" + " " + " Interrupts" + " COND_NEW_ENEMY" + ) + + //========================================================= + // Shoot at striderbuster. Distinct from generic range attack + // because of BuildScheduleTestBits. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_RANGE_ATTACK2_VS_STRIDERBUSTER, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_HUNTER_SHOOT_COMMIT 0" + " TASK_RANGE_ATTACK2 0" + " " + " Interrupts" + ) + + //========================================================= + // Shoot at striderbuster with a little latency beforehand + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_RANGE_ATTACK2_VS_STRIDERBUSTER_LATENT, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_HUNTER_SHOOT_COMMIT 0" + " TASK_WAIT 0.2" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_RANGE_ATTACK2" + " TASK_RANGE_ATTACK2 0" + " " + " Interrupts" + ) + + //========================================================= + // Dodge Incoming vehicle + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_DODGE, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_HUNTER_FAIL_DODGE" + " TASK_HUNTER_FIND_DODGE_POSITION 0" + " TASK_HUNTER_DODGE 0" + "" + " Interrupts" + ) + + //========================================================= + // Dodge Incoming vehicle + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_FAIL_DODGE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_FACE_ENEMY 0" + "" + " Interrupts" + ) + + //================================================== + // > SCHED_HUNTER_CHARGE_ENEMY + // Rush at my enemy and head-butt them. + //================================================== + DEFINE_SCHEDULE + ( + SCHED_HUNTER_CHARGE_ENEMY, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_HUNTER_FAIL_CHARGE_ENEMY" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_HUNTER_CHARGE 0" + "" + " Interrupts" + " COND_TASK_FAILED" + " COND_ENEMY_DEAD" + ) + + DEFINE_SCHEDULE + ( + SCHED_HUNTER_FAIL_CHARGE_ENEMY, + + " Tasks" + " TASK_HUNTER_CHARGE_DELAY 10" + ) + + //========================================================= + // Chase the enemy with intent to claw + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_CHASE_ENEMY_MELEE, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ESTABLISH_LINE_OF_FIRE" + " TASK_STOP_MOVING 0" + " TASK_GET_CHASE_PATH_TO_ENEMY 300" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_FACE_ENEMY 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_ENEMY_UNREACHABLE" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_MELEE_ATTACK2" + //" COND_TOO_CLOSE_TO_ATTACK" + " COND_LOST_ENEMY" + ) + + //========================================================= + // Chase my enemy, shoot or claw when possible to do so. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_CHASE_ENEMY, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ESTABLISH_LINE_OF_FIRE" + " TASK_STOP_MOVING 0" + " TASK_GET_CHASE_PATH_TO_ENEMY 300" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_FACE_ENEMY 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + " COND_ENEMY_UNREACHABLE" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_MELEE_ATTACK2" + " COND_TOO_CLOSE_TO_ATTACK" + " COND_LOST_ENEMY" + ) + + //========================================================= + // Move to a flanking position, then shoot if possible. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_FLANK_ENEMY, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ESTABLISH_LINE_OF_FIRE" + " TASK_STOP_MOVING 0" + " TASK_HUNTER_BEGIN_FLANK 0" + " TASK_GET_FLANK_ARC_PATH_TO_ENEMY_LOS 30" + " TASK_HUNTER_ANNOUNCE_FLANK 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_FACE_ENEMY 0" + //" TASK_HUNTER_END_FLANK 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + //" COND_CAN_RANGE_ATTACK1" + //" COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_MELEE_ATTACK2" + " COND_ENEMY_DEAD" + " COND_ENEMY_UNREACHABLE" + " COND_TOO_CLOSE_TO_ATTACK" + " COND_LOST_ENEMY" + ) + + //========================================================= + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_COMBAT_FACE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_WAIT_FACE_ENEMY 1" + "" + " Interrupts" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_MELEE_ATTACK2" + " COND_NEW_ENEMY" + " COND_ENEMY_DEAD" + ) + + //========================================================= + // Like the base class, only don't stop in the middle of + // swinging if the enemy is killed, hides, or new enemy. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_MELEE_ATTACK1, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_MELEE_ATTACK1 0" + //" TASK_SET_SCHEDULE SCHEDULE:SCHED_HUNTER_POST_MELEE_WAIT" + "" + " Interrupts" + ) + + //========================================================= + // In a fight with nothing to do. Make busy! + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_CHANGE_POSITION, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_WANDER 720432" // 6 feet to 36 feet + " TASK_RUN_PATH 0" + " TASK_HUNTER_WAIT_FOR_MOVEMENT_FACING_ENEMY 0" + " TASK_STOP_MOVING 0" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_HUNTER_CHANGE_POSITION_FINISH" + "" + " Interrupts" + " COND_ENEMY_DEAD" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_MELEE_ATTACK2" + " COND_HEAR_DANGER" + " COND_HEAR_MOVE_AWAY" + " COND_NEW_ENEMY" + ) + + //========================================================= + // In a fight with nothing to do. Make busy! + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_CHANGE_POSITION_FINISH, + + " Tasks" + " TASK_FACE_ENEMY 0" + " TASK_WAIT_FACE_ENEMY_RANDOM 5" + "" + " Interrupts" + " COND_ENEMY_DEAD" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_MELEE_ATTACK2" + " COND_HEAR_DANGER" + " COND_HEAR_MOVE_AWAY" + " COND_NEW_ENEMY" + ) + + //========================================================= + // In a fight with nothing to do. Make busy! + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_SIDESTEP, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_HUNTER_FAIL_IMMEDIATE" // used because sched_fail includes a one second pause. ick! + " TASK_STOP_MOVING 0" + " TASK_HUNTER_FIND_SIDESTEP_POSITION 0" + " TASK_GET_PATH_TO_SAVEPOSITION 0" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_FACE_ENEMY 0" + "" + " Interrupts" + ) + + //========================================================= + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_PATROL, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_WANDER 720432" // 6 feet to 36 feet + " TASK_WALK_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_STOP_MOVING 0" + " TASK_FACE_REASONABLE 0" + " TASK_WAIT_RANDOM 3" + "" + " Interrupts" + " COND_ENEMY_DEAD" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + " COND_HEAR_DANGER" + " COND_HEAR_COMBAT" + " COND_HEAR_PLAYER" + " COND_HEAR_BULLET_IMPACT" + " COND_HEAR_MOVE_AWAY" + " COND_NEW_ENEMY" + " COND_SEE_ENEMY" + " COND_CAN_RANGE_ATTACK1" + " COND_CAN_RANGE_ATTACK2" + " COND_CAN_MELEE_ATTACK1" + " COND_CAN_MELEE_ATTACK2" + ) + + //========================================================= + // Stagger because I got hit by something heavy + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_STAGGER, + + " Tasks" + " TASK_HUNTER_STAGGER 0" + "" + " Interrupts" + ) + + //========================================================= + // Run around randomly until we detect an enemy + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_PATROL_RUN, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_COMBAT_FACE" + " TASK_SET_ROUTE_SEARCH_TIME 5" // Spend 5 seconds trying to build a path if stuck + " TASK_GET_PATH_TO_RANDOM_NODE 200" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + "" + " Interrupts" + " COND_CAN_RANGE_ATTACK1 " + " COND_CAN_RANGE_ATTACK2 " + " COND_CAN_MELEE_ATTACK1 " + " COND_CAN_MELEE_ATTACK2" + " COND_GIVE_WAY" + " COND_NEW_ENEMY" + " COND_HEAR_COMBAT" + " COND_HEAR_DANGER" + " COND_HEAR_PLAYER" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_TAKE_COVER_FROM_ENEMY, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_HUNTER_CHASE_ENEMY_MELEE" + " TASK_HUNTER_CORNERED_TIMER 10.0" + " TASK_WAIT 0.0" + // " TASK_SET_TOLERANCE_DISTANCE 24" + // " TASK_FIND_COVER_FROM_ENEMY 0" + " TASK_FIND_FAR_NODE_COVER_FROM_ENEMY 200.0" + " TASK_RUN_PATH 0" + " TASK_HUNTER_CORNERED_TIMER 0.0" + // " TASK_CLEAR_FAIL_SCHEDULE 0" // not used because sched_fail includes a one second pause. ick! + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_HUNTER_FAIL_IMMEDIATE" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_REMEMBER MEMORY:INCOVER" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_HUNTER_HIDE_UNDER_COVER" + /* + " TASK_FACE_ENEMY 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" // Translated to cover + " TASK_WAIT 1" + */ + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_HEAR_DANGER" + ) + + //========================================================= + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_HIDE_UNDER_COVER, + + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_HUNTER_FAIL_IMMEDIATE" // used because sched_fail includes a one second pause. ick! + " TASK_FACE_ENEMY 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" // Translated to cover + " TASK_WAIT 1" + "" + " Interrupts" + " COND_NEW_ENEMY" + " COND_HEAR_DANGER" + " COND_HAVE_ENEMY_LOS" + ) + + //========================================================= + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_FOUND_ENEMY, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_HUNTER_FOUND_ENEMY" + "" + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_FOUND_ENEMY_ACK, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_WAIT_RANDOM 0.75" + " TASK_FACE_ENEMY 0" + " TASK_PLAY_SEQUENCE_FACE_ENEMY ACTIVITY:ACT_HUNTER_FOUND_ENEMY_ACK" + "" + " Interrupts" + " COND_LIGHT_DAMAGE" + " COND_HEAVY_DAMAGE" + ) + + //========================================================= + // An empty schedule that immediately bails out, faster than + // SCHED_FAIL which stops moving and waits for one second. + //========================================================= + DEFINE_SCHEDULE + ( + SCHED_HUNTER_FAIL_IMMEDIATE, + + " Tasks" + " TASK_WAIT 0" + + ) + + DEFINE_SCHEDULE + ( + SCHED_HUNTER_GOTO_HINT, + " Tasks" + " TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_HUNTER_CLEAR_HINTNODE" // used because sched_fail includes a one second pause. ick! + " TASK_GET_PATH_TO_HINTNODE 1" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_CLEAR_HINTNODE 0" + "" + "" + " Interrupts" + ) + + DEFINE_SCHEDULE + ( + SCHED_HUNTER_CLEAR_HINTNODE, + " Tasks" + " TASK_CLEAR_HINTNODE 0" + "" + "" + " Interrupts" + ) + + DEFINE_SCHEDULE + ( + SCHED_HUNTER_SIEGE_STAND, + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_FACE_PLAYER 0" + " TASK_WAIT 10" + " TASK_WAIT_RANDOM 2" + " TASK_SET_SCHEDULE SCHEDULE:SCHED_HUNTER_CHANGE_POSITION_SIEGE" + "" + "" + " Interrupts" + " COND_SEE_PLAYER" + " COND_NEW_ENEMY" + ) + + DEFINE_SCHEDULE + ( + SCHED_HUNTER_CHANGE_POSITION_SIEGE, + + " Tasks" + " TASK_STOP_MOVING 0" + " TASK_WANDER 2400480" + " TASK_RUN_PATH 0" + " TASK_WAIT_FOR_MOVEMENT 0" + " TASK_STOP_MOVING 0" + " TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" + " TASK_FACE_PLAYER 0" + "" + " Interrupts" + " COND_NEW_ENEMY" + ) + + // formula is MIN_DIST * 10000 + MAX_DIST + +AI_END_CUSTOM_NPC() diff --git a/game/server/episodic/npc_hunter.h b/game/server/episodic/npc_hunter.h new file mode 100644 index 0000000..b68cedf --- /dev/null +++ b/game/server/episodic/npc_hunter.h @@ -0,0 +1,25 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Expose an IsAHunter function +// +//=============================================================================// + +#ifndef NPC_HUNTER_H +#define NPC_HUNTER_H + +#if defined( _WIN32 ) +#pragma once +#endif + +class CBaseEntity; + +/// true if given entity pointer is a hunter. +bool Hunter_IsHunter(CBaseEntity *pEnt); + +// call throughs for member functions + +void Hunter_StriderBusterAttached( CBaseEntity *pHunter, CBaseEntity *pAttached ); +void Hunter_StriderBusterDetached( CBaseEntity *pHunter, CBaseEntity *pAttached ); +void Hunter_StriderBusterLaunched( CBaseEntity *pBuster ); + +#endif diff --git a/game/server/episodic/npc_magnusson.cpp b/game/server/episodic/npc_magnusson.cpp new file mode 100644 index 0000000..3100835 --- /dev/null +++ b/game/server/episodic/npc_magnusson.cpp @@ -0,0 +1,127 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Dr. Magnusson, a grumpy bastard who builds satellites and rockets +// at the White Forest missile silo. Instantly unlikeable, he is also +// the inventor of the Magnusson Device aka "strider buster", which +// is purported to resemble his cantelope-like head. +// +//============================================================================= + + +//----------------------------------------------------------------------------- +// Generic NPC - purely for scripted sequence work. +//----------------------------------------------------------------------------- +#include "cbase.h" +#include "npcevent.h" +#include "ai_basenpc.h" +#include "ai_hull.h" +#include "ai_baseactor.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// NPC's Anim Events Go Here +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +class CNPC_Magnusson : public CAI_BaseActor +{ +public: + DECLARE_CLASS( CNPC_Magnusson, CAI_BaseActor ); + + void Spawn( void ); + void Precache( void ); + Class_T Classify ( void ); + void HandleAnimEvent( animevent_t *pEvent ); + int GetSoundInterests ( void ); +}; + +LINK_ENTITY_TO_CLASS( npc_magnusson, CNPC_Magnusson ); + + +//----------------------------------------------------------------------------- +// Classify - indicates this NPC's place in the +// relationship table. +//----------------------------------------------------------------------------- +Class_T CNPC_Magnusson::Classify ( void ) +{ + return CLASS_PLAYER_ALLY_VITAL; +} + + +//----------------------------------------------------------------------------- +// HandleAnimEvent - catches the NPC-specific messages +// that occur when tagged animation frames are played. +//----------------------------------------------------------------------------- +void CNPC_Magnusson::HandleAnimEvent( animevent_t *pEvent ) +{ + switch( pEvent->event ) + { + case 1: + default: + BaseClass::HandleAnimEvent( pEvent ); + break; + } +} + +//----------------------------------------------------------------------------- +// GetSoundInterests - generic NPC can't hear. +//----------------------------------------------------------------------------- +int CNPC_Magnusson::GetSoundInterests ( void ) +{ + return NULL; +} + +//----------------------------------------------------------------------------- +// Spawn +//----------------------------------------------------------------------------- +void CNPC_Magnusson::Spawn() +{ + // Allow custom model usage (mostly for monitors) + char *szModel = (char *)STRING( GetModelName() ); + if (!szModel || !*szModel) + { + szModel = "models/magnusson.mdl"; + SetModelName( AllocPooledString(szModel) ); + } + + Precache(); + SetModel( szModel ); + + BaseClass::Spawn(); + + SetHullType(HULL_HUMAN); + SetHullSizeNormal(); + + SetSolid( SOLID_BBOX ); + AddSolidFlags( FSOLID_NOT_STANDABLE ); + SetMoveType( MOVETYPE_STEP ); + SetBloodColor( BLOOD_COLOR_RED ); + m_iHealth = 8; + m_flFieldOfView = 0.5;// indicates the width of this NPC's forward view cone ( as a dotproduct result ) + m_NPCState = NPC_STATE_NONE; + + CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_OPEN_DOORS | bits_CAP_ANIMATEDFACE | bits_CAP_TURN_HEAD ); + CapabilitiesAdd( bits_CAP_FRIENDLY_DMG_IMMUNE ); + + AddEFlags( EFL_NO_DISSOLVE | EFL_NO_MEGAPHYSCANNON_RAGDOLL | EFL_NO_PHYSCANNON_INTERACTION ); + + NPCInit(); +} + +//----------------------------------------------------------------------------- +// Precache - precaches all resources this NPC needs +//----------------------------------------------------------------------------- +void CNPC_Magnusson::Precache() +{ + PrecacheModel( STRING( GetModelName() ) ); + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// AI Schedules Specific to this NPC +//----------------------------------------------------------------------------- diff --git a/game/server/episodic/npc_puppet.cpp b/game/server/episodic/npc_puppet.cpp new file mode 100644 index 0000000..8d4f366 --- /dev/null +++ b/game/server/episodic/npc_puppet.cpp @@ -0,0 +1,121 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: NPC Puppet +// +//============================================================================= + +#include "cbase.h" +#include "ai_basenpc.h" + +// Must be the last file included +#include "memdbgon.h" + +class CNPC_Puppet : public CAI_BaseNPC +{ + DECLARE_CLASS( CNPC_Puppet, CAI_BaseNPC ); +public: + + virtual void Spawn( void ); + virtual void Precache( void ); + + void InputSetAnimationTarget( inputdata_t &inputdata ); + +private: + + string_t m_sAnimTargetname; + string_t m_sAnimAttachmentName; + + CNetworkVar( EHANDLE, m_hAnimationTarget ); // NPC that will drive what animation we're playing + CNetworkVar( int, m_nTargetAttachment ); // Attachment point to match to on the target + + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); +}; + +LINK_ENTITY_TO_CLASS( npc_puppet, CNPC_Puppet ); + +BEGIN_DATADESC( CNPC_Puppet ) + DEFINE_KEYFIELD( m_sAnimTargetname, FIELD_STRING, "animationtarget" ), + DEFINE_KEYFIELD( m_sAnimAttachmentName, FIELD_STRING, "attachmentname" ), + + DEFINE_FIELD( m_nTargetAttachment, FIELD_INTEGER ), + DEFINE_FIELD( m_hAnimationTarget, FIELD_EHANDLE ), + DEFINE_INPUTFUNC( FIELD_STRING, "SetAnimationTarget", InputSetAnimationTarget ), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CNPC_Puppet, DT_NPC_Puppet ) + SendPropEHandle( SENDINFO( m_hAnimationTarget ) ), + SendPropInt( SENDINFO( m_nTargetAttachment) ), +END_SEND_TABLE() + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Puppet::Precache( void ) +{ + BaseClass::Precache(); + PrecacheModel( STRING( GetModelName() ) ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_Puppet::Spawn( void ) +{ + BaseClass::Spawn(); + + Precache(); + + SetModel( STRING( GetModelName() ) ); + + NPCInit(); + + SetHealth( 100 ); + + // Find our animation target + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, m_sAnimTargetname ); + m_hAnimationTarget = pTarget; + if ( pTarget ) + { + CBaseAnimating *pAnimating = pTarget->GetBaseAnimating(); + if ( pAnimating ) + { + m_nTargetAttachment = pAnimating->LookupAttachment( STRING( m_sAnimAttachmentName ) ); + } + } + + // Always be scripted + SetInAScript( true ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &inputdata - +//----------------------------------------------------------------------------- +void CNPC_Puppet::InputSetAnimationTarget( inputdata_t &inputdata ) +{ + // Take the new name + m_sAnimTargetname = MAKE_STRING( inputdata.value.String() ); + + // Find our animation target + CBaseEntity *pTarget = gEntList.FindEntityByName( NULL, m_sAnimTargetname ); + if ( pTarget == NULL ) + { + Warning("Failed to find animation target %s for npc_puppet (%s)\n", STRING( m_sAnimTargetname ), STRING( GetEntityName() ) ); + return; + } + + m_hAnimationTarget = pTarget; + + CBaseAnimating *pAnimating = pTarget->GetBaseAnimating(); + if ( pAnimating ) + { + // Cache off our target attachment + m_nTargetAttachment = pAnimating->LookupAttachment( STRING( m_sAnimAttachmentName ) ); + } + + // Stuff us at the owner's core for visibility reasons + SetParent( pTarget ); + SetLocalOrigin( vec3_origin ); + SetLocalAngles( vec3_angle ); +} diff --git a/game/server/episodic/prop_scalable.cpp b/game/server/episodic/prop_scalable.cpp new file mode 100644 index 0000000..a311535 --- /dev/null +++ b/game/server/episodic/prop_scalable.cpp @@ -0,0 +1,150 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Big pulsating ball inside the core of the citadel +// +//=============================================================================// + +#include "cbase.h" +#include "baseentity.h" + +#define COREBALL_MODEL "models/props_combine/coreball.mdl" + +class CPropScalable : public CBaseAnimating +{ +public: + DECLARE_CLASS( CPropScalable, CBaseAnimating ); + DECLARE_DATADESC(); + DECLARE_SERVERCLASS(); + + CPropScalable(); + + virtual void Spawn( void ); + virtual void Precache( void ); + + CNetworkVar( float, m_flScaleX ); + CNetworkVar( float, m_flScaleY ); + CNetworkVar( float, m_flScaleZ ); + + CNetworkVar( float, m_flLerpTimeX ); + CNetworkVar( float, m_flLerpTimeY ); + CNetworkVar( float, m_flLerpTimeZ ); + + CNetworkVar( float, m_flGoalTimeX ); + CNetworkVar( float, m_flGoalTimeY ); + CNetworkVar( float, m_flGoalTimeZ ); + + void InputSetScaleX( inputdata_t &inputdata ); + void InputSetScaleY( inputdata_t &inputdata ); + void InputSetScaleZ( inputdata_t &inputdata ); +}; + +LINK_ENTITY_TO_CLASS( prop_coreball, CPropScalable ); +LINK_ENTITY_TO_CLASS( prop_scalable, CPropScalable ); + +BEGIN_DATADESC( CPropScalable ) + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetScaleX", InputSetScaleX ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetScaleY", InputSetScaleY ), + DEFINE_INPUTFUNC( FIELD_VECTOR, "SetScaleZ", InputSetScaleZ ), + + DEFINE_FIELD( m_flScaleX, FIELD_FLOAT ), + DEFINE_FIELD( m_flScaleY, FIELD_FLOAT ), + DEFINE_FIELD( m_flScaleZ, FIELD_FLOAT ), + + DEFINE_FIELD( m_flLerpTimeX, FIELD_FLOAT ), + DEFINE_FIELD( m_flLerpTimeY, FIELD_FLOAT ), + DEFINE_FIELD( m_flLerpTimeZ, FIELD_FLOAT ), + + DEFINE_FIELD( m_flGoalTimeX, FIELD_FLOAT ), + DEFINE_FIELD( m_flGoalTimeY, FIELD_FLOAT ), + DEFINE_FIELD( m_flGoalTimeZ, FIELD_FLOAT ), +END_DATADESC() + +IMPLEMENT_SERVERCLASS_ST( CPropScalable, DT_PropScalable ) + SendPropFloat( SENDINFO(m_flScaleX), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flScaleY), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flScaleZ), 0, SPROP_NOSCALE ), + + SendPropFloat( SENDINFO(m_flLerpTimeX), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flLerpTimeY), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flLerpTimeZ), 0, SPROP_NOSCALE ), + + SendPropFloat( SENDINFO(m_flGoalTimeX), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flGoalTimeY), 0, SPROP_NOSCALE ), + SendPropFloat( SENDINFO(m_flGoalTimeZ), 0, SPROP_NOSCALE ), +END_SEND_TABLE() + +CPropScalable::CPropScalable( void ) +{ + m_flScaleX = 1.0f; + m_flScaleY = 1.0f; + m_flScaleZ = 1.0f; + + UseClientSideAnimation(); +} + +void CPropScalable::Spawn( void ) +{ + // Stomp our model name if we're the coreball (legacy) + if ( FClassnameIs( this, "prop_coreball" ) ) + { + PrecacheModel( COREBALL_MODEL ); + SetModel( COREBALL_MODEL ); + } + else + { + char *szModel = (char *)STRING( GetModelName() ); + if (!szModel || !*szModel) + { + Warning( "prop_scalable at %.0f %.0f %0.f missing modelname\n", GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z ); + UTIL_Remove( this ); + return; + } + + PrecacheModel( szModel ); + SetModel( szModel ); + } + + SetMoveType( MOVETYPE_NONE ); + + BaseClass::Spawn(); + + AddEffects( EF_NOSHADOW ); + + SetSequence( 0 ); + SetPlaybackRate( 1.0f ); +} + +void CPropScalable::Precache( void ) +{ + BaseClass::Precache(); +} + +void CPropScalable::InputSetScaleX( inputdata_t &inputdata ) +{ + Vector vecScale; + inputdata.value.Vector3D( vecScale ); + + m_flScaleX = vecScale.x; + m_flLerpTimeX = vecScale.y; + m_flGoalTimeX = gpGlobals->curtime; +} + +void CPropScalable::InputSetScaleY( inputdata_t &inputdata ) +{ + Vector vecScale; + inputdata.value.Vector3D( vecScale ); + + m_flScaleY = vecScale.x; + m_flLerpTimeY = vecScale.y; + m_flGoalTimeY = gpGlobals->curtime; +} + +void CPropScalable::InputSetScaleZ( inputdata_t &inputdata ) +{ + Vector vecScale; + inputdata.value.Vector3D( vecScale ); + + m_flScaleZ = vecScale.x; + m_flLerpTimeZ = vecScale.y; + m_flGoalTimeZ = gpGlobals->curtime; +}
\ No newline at end of file diff --git a/game/server/episodic/vehicle_jeep_episodic.cpp b/game/server/episodic/vehicle_jeep_episodic.cpp new file mode 100644 index 0000000..368f1b9 --- /dev/null +++ b/game/server/episodic/vehicle_jeep_episodic.cpp @@ -0,0 +1,1764 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// +// +//============================================================================= + +#include "cbase.h" +#include "vehicle_jeep_episodic.h" +#include "collisionutils.h" +#include "npc_alyx_episodic.h" +#include "particle_parse.h" +#include "particle_system.h" +#include "hl2_player.h" +#include "in_buttons.h" +#include "vphysics/friction.h" +#include "vphysicsupdateai.h" +#include "physics_npc_solver.h" +#include "Sprite.h" +#include "weapon_striderbuster.h" +#include "npc_strider.h" +#include "vguiscreen.h" +#include "hl2_vehicle_radar.h" +#include "props.h" +#include "ai_dynamiclink.h" + +extern ConVar phys_upimpactforcescale; + +ConVar jalopy_blocked_exit_max_speed( "jalopy_blocked_exit_max_speed", "50" ); + +#define JEEP_AMMOCRATE_HITGROUP 5 +#define JEEP_AMMO_CRATE_CLOSE_DELAY 2.0f + +// Bodygroups +#define JEEP_RADAR_BODYGROUP 1 +#define JEEP_HOPPER_BODYGROUP 2 +#define JEEP_CARBAR_BODYGROUP 3 + +#define RADAR_PANEL_MATERIAL "vgui/screens/radar" +#define RADAR_PANEL_WRITEZ "engine/writez" + +static const char *s_szHazardSprite = "sprites/light_glow01.vmt"; + +enum +{ + RADAR_MODE_NORMAL = 0, + RADAR_MODE_STICKY, +}; + +//========================================================= +//========================================================= +class CRadarTarget : public CPointEntity +{ + DECLARE_CLASS( CRadarTarget, CPointEntity ); + +public: + void Spawn(); + + bool IsDisabled() { return m_bDisabled; } + int GetType() { return m_iType; } + int GetMode() { return m_iMode; } + void InputEnable( inputdata_t &inputdata ); + void InputDisable( inputdata_t &inputdata ); + int ObjectCaps(); + +private: + bool m_bDisabled; + int m_iType; + int m_iMode; + +public: + float m_flRadius; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( info_radar_target, CRadarTarget ); + +BEGIN_DATADESC( CRadarTarget ) + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "radius" ), + DEFINE_KEYFIELD( m_iType, FIELD_INTEGER, "type" ), + DEFINE_KEYFIELD( m_iMode, FIELD_INTEGER, "mode" ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable",InputDisable ), +END_DATADESC(); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRadarTarget::Spawn() +{ + BaseClass::Spawn(); + + AddEffects( EF_NODRAW ); + SetMoveType( MOVETYPE_NONE ); + SetSolid( SOLID_NONE ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRadarTarget::InputEnable( inputdata_t &inputdata ) +{ + m_bDisabled = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CRadarTarget::InputDisable( inputdata_t &inputdata ) +{ + m_bDisabled = true; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CRadarTarget::ObjectCaps() +{ + return BaseClass::ObjectCaps() | FCAP_ACROSS_TRANSITION; +} + + + + +// +// Trigger which detects entities placed in the cargo hold of the jalopy +// + +class CVehicleCargoTrigger : public CBaseEntity +{ + DECLARE_CLASS( CVehicleCargoTrigger, CBaseEntity ); + +public: + + // + // Creates a trigger with the specified bounds + + static CVehicleCargoTrigger *Create( const Vector &vecOrigin, const Vector &vecMins, const Vector &vecMaxs, CBaseEntity *pOwner ) + { + CVehicleCargoTrigger *pTrigger = (CVehicleCargoTrigger *) CreateEntityByName( "trigger_vehicle_cargo" ); + if ( pTrigger == NULL ) + return NULL; + + UTIL_SetOrigin( pTrigger, vecOrigin ); + UTIL_SetSize( pTrigger, vecMins, vecMaxs ); + pTrigger->SetOwnerEntity( pOwner ); + pTrigger->SetParent( pOwner ); + + pTrigger->Spawn(); + + return pTrigger; + } + + // + // Handles the trigger touching its intended quarry + + void CargoTouch( CBaseEntity *pOther ) + { + // Cannot be ignoring touches + if ( ( m_hIgnoreEntity == pOther ) || ( m_flIgnoreDuration >= gpGlobals->curtime ) ) + return; + + // Make sure this object is being held by the player + if ( pOther->VPhysicsGetObject() == NULL || (pOther->VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD) == false ) + return; + + if ( StriderBuster_NumFlechettesAttached( pOther ) > 0 ) + return; + + AddCargo( pOther ); + } + + bool AddCargo( CBaseEntity *pOther ) + { + // For now, only bother with strider busters + if ( (FClassnameIs( pOther, "weapon_striderbuster" ) == false) && + (FClassnameIs( pOther, "npc_grenade_magna" ) == false) + ) + return false; + + // Must be a physics prop + CPhysicsProp *pProp = dynamic_cast<CPhysicsProp *>(pOther); + if ( pOther == NULL ) + return false; + + CPropJeepEpisodic *pJeep = dynamic_cast< CPropJeepEpisodic * >( GetOwnerEntity() ); + if ( pJeep == NULL ) + return false; + + // Make the player release the item + Pickup_ForcePlayerToDropThisObject( pOther ); + + // Stop colliding with things + pOther->VPhysicsDestroyObject(); + pOther->SetSolidFlags( FSOLID_NOT_SOLID ); + pOther->SetMoveType( MOVETYPE_NONE ); + + // Parent the object to our owner + pOther->SetParent( GetOwnerEntity() ); + + // The car now owns the entity + pJeep->AddPropToCargoHold( pProp ); + + // Notify the buster that it's been added to the cargo hold. + StriderBuster_OnAddToCargoHold( pProp ); + + // Stop touching this item + Disable(); + + return true; + } + + // + // Setup the entity + + void Spawn( void ) + { + BaseClass::Spawn(); + + SetSolid( SOLID_BBOX ); + SetSolidFlags( FSOLID_TRIGGER | FSOLID_NOT_SOLID ); + + SetTouch( &CVehicleCargoTrigger::CargoTouch ); + } + + void Activate() + { + BaseClass::Activate(); + SetSolidFlags( FSOLID_TRIGGER | FSOLID_NOT_SOLID ); // Fixes up old savegames + } + + // + // When we've stopped touching this entity, we ignore it + + void EndTouch( CBaseEntity *pOther ) + { + if ( pOther == m_hIgnoreEntity ) + { + m_hIgnoreEntity = NULL; + } + + BaseClass::EndTouch( pOther ); + } + + // + // Disables the trigger for a set duration + + void IgnoreTouches( CBaseEntity *pIgnoreEntity ) + { + m_hIgnoreEntity = pIgnoreEntity; + m_flIgnoreDuration = gpGlobals->curtime + 0.5f; + } + + void Disable( void ) + { + SetTouch( NULL ); + } + + void Enable( void ) + { + SetTouch( &CVehicleCargoTrigger::CargoTouch ); + } + +protected: + + float m_flIgnoreDuration; + CHandle <CBaseEntity> m_hIgnoreEntity; + + DECLARE_DATADESC(); +}; + +LINK_ENTITY_TO_CLASS( trigger_vehicle_cargo, CVehicleCargoTrigger ); + +BEGIN_DATADESC( CVehicleCargoTrigger ) + DEFINE_FIELD( m_flIgnoreDuration, FIELD_TIME ), + DEFINE_FIELD( m_hIgnoreEntity, FIELD_EHANDLE ), + DEFINE_ENTITYFUNC( CargoTouch ), +END_DATADESC(); + +// +// Transition reference point for the vehicle +// + +class CInfoTargetVehicleTransition : public CPointEntity +{ +public: + DECLARE_CLASS( CInfoTargetVehicleTransition, CPointEntity ); + + void Enable( void ) { m_bDisabled = false; } + void Disable( void ) { m_bDisabled = true; } + + bool IsDisabled( void ) const { return m_bDisabled; } + +private: + + void InputEnable( inputdata_t &data ) { Enable(); } + void InputDisable( inputdata_t &data ) { Disable(); } + + bool m_bDisabled; + + DECLARE_DATADESC(); +}; + +BEGIN_DATADESC( CInfoTargetVehicleTransition ) + DEFINE_KEYFIELD( m_bDisabled, FIELD_BOOLEAN, "StartDisabled" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable",InputDisable ), +END_DATADESC(); + +LINK_ENTITY_TO_CLASS( info_target_vehicle_transition, CInfoTargetVehicleTransition ); + +// +// CPropJeepEpisodic +// + +LINK_ENTITY_TO_CLASS( prop_vehicle_jeep, CPropJeepEpisodic ); + +BEGIN_DATADESC( CPropJeepEpisodic ) + + DEFINE_FIELD( m_bEntranceLocked, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bExitLocked, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hCargoProp, FIELD_EHANDLE ), + DEFINE_FIELD( m_hCargoTrigger, FIELD_EHANDLE ), + DEFINE_FIELD( m_bAddingCargo, FIELD_BOOLEAN ), + DEFINE_ARRAY( m_hWheelDust, FIELD_EHANDLE, NUM_WHEEL_EFFECTS ), + DEFINE_ARRAY( m_hWheelWater, FIELD_EHANDLE, NUM_WHEEL_EFFECTS ), + DEFINE_ARRAY( m_hHazardLights, FIELD_EHANDLE, NUM_HAZARD_LIGHTS ), + DEFINE_FIELD( m_flCargoStartTime, FIELD_TIME ), + DEFINE_FIELD( m_bBlink, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bRadarEnabled, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bRadarDetectsEnemies, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hRadarScreen, FIELD_EHANDLE ), + DEFINE_FIELD( m_hLinkControllerFront, FIELD_EHANDLE ), + DEFINE_FIELD( m_hLinkControllerRear, FIELD_EHANDLE ), + DEFINE_KEYFIELD( m_bBusterHopperVisible, FIELD_BOOLEAN, "CargoVisible" ), + // m_flNextAvoidBroadcastTime + DEFINE_FIELD( m_flNextWaterSound, FIELD_TIME ), + DEFINE_FIELD( m_flNextRadarUpdateTime, FIELD_TIME ), + DEFINE_FIELD( m_iNumRadarContacts, FIELD_INTEGER ), + DEFINE_ARRAY( m_vecRadarContactPos, FIELD_POSITION_VECTOR, RADAR_MAX_CONTACTS ), + DEFINE_ARRAY( m_iRadarContactType, FIELD_INTEGER, RADAR_MAX_CONTACTS ), + + DEFINE_THINKFUNC( HazardBlinkThink ), + + DEFINE_OUTPUT( m_OnCompanionEnteredVehicle, "OnCompanionEnteredVehicle" ), + DEFINE_OUTPUT( m_OnCompanionExitedVehicle, "OnCompanionExitedVehicle" ), + DEFINE_OUTPUT( m_OnHostileEnteredVehicle, "OnHostileEnteredVehicle" ), + DEFINE_OUTPUT( m_OnHostileExitedVehicle, "OnHostileExitedVehicle" ), + + DEFINE_INPUTFUNC( FIELD_VOID, "LockEntrance", InputLockEntrance ), + DEFINE_INPUTFUNC( FIELD_VOID, "UnlockEntrance", InputUnlockEntrance ), + DEFINE_INPUTFUNC( FIELD_VOID, "LockExit", InputLockExit ), + DEFINE_INPUTFUNC( FIELD_VOID, "UnlockExit", InputUnlockExit ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableRadar", InputEnableRadar ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisableRadar", InputDisableRadar ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnableRadarDetectEnemies", InputEnableRadarDetectEnemies ), + DEFINE_INPUTFUNC( FIELD_VOID, "AddBusterToCargo", InputAddBusterToCargo ), + DEFINE_INPUTFUNC( FIELD_VOID, "OutsideTransition", InputOutsideTransition ), + DEFINE_INPUTFUNC( FIELD_VOID, "DisablePhysGun", InputDisablePhysGun ), + DEFINE_INPUTFUNC( FIELD_VOID, "EnablePhysGun", InputEnablePhysGun ), + DEFINE_INPUTFUNC( FIELD_VOID, "CreateLinkController", InputCreateLinkController ), + DEFINE_INPUTFUNC( FIELD_VOID, "DestroyLinkController", InputDestroyLinkController ), + + DEFINE_INPUTFUNC( FIELD_BOOLEAN, "SetCargoHopperVisibility", InputSetCargoVisibility ), + +END_DATADESC(); + +IMPLEMENT_SERVERCLASS_ST(CPropJeepEpisodic, DT_CPropJeepEpisodic) + //CNetworkVar( int, m_iNumRadarContacts ); + SendPropInt( SENDINFO(m_iNumRadarContacts), 8 ), + + //CNetworkArray( Vector, m_vecRadarContactPos, RADAR_MAX_CONTACTS ); + SendPropArray( SendPropVector( SENDINFO_ARRAY(m_vecRadarContactPos), -1, SPROP_COORD), m_vecRadarContactPos ), + + //CNetworkArray( int, m_iRadarContactType, RADAR_MAX_CONTACTS ); + SendPropArray( SendPropInt(SENDINFO_ARRAY(m_iRadarContactType), RADAR_CONTACT_TYPE_BITS ), m_iRadarContactType ), +END_SEND_TABLE() + + +//============================================================================= +// Episodic jeep + +CPropJeepEpisodic::CPropJeepEpisodic( void ) : +m_bEntranceLocked( false ), +m_bExitLocked( false ), +m_bAddingCargo( false ), +m_flNextAvoidBroadcastTime( 0.0f ) +{ + m_bHasGun = false; + m_bUnableToFire = true; + m_bRadarDetectsEnemies = false; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::UpdateOnRemove( void ) +{ + BaseClass::UpdateOnRemove(); + + // Kill our wheel dust + for ( int i = 0; i < NUM_WHEEL_EFFECTS; i++ ) + { + if ( m_hWheelDust[i] != NULL ) + { + UTIL_Remove( m_hWheelDust[i] ); + } + + if ( m_hWheelWater[i] != NULL ) + { + UTIL_Remove( m_hWheelWater[i] ); + } + } + + DestroyHazardLights(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::Precache( void ) +{ + PrecacheMaterial( RADAR_PANEL_MATERIAL ); + PrecacheMaterial( RADAR_PANEL_WRITEZ ); + PrecacheModel( s_szHazardSprite ); + PrecacheScriptSound( "JNK_Radar_Ping_Friendly" ); + PrecacheScriptSound( "Physics.WaterSplash" ); + + PrecacheParticleSystem( "WheelDust" ); + PrecacheParticleSystem( "WheelSplash" ); + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPlayer - +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::EnterVehicle( CBaseCombatCharacter *pPassenger ) +{ + BaseClass::EnterVehicle( pPassenger ); + + // Turn our hazards off! + DestroyHazardLights(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::Spawn( void ) +{ + BaseClass::Spawn(); + + SetBlocksLOS( false ); + + CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); + if ( pPlayer != NULL ) + { + pPlayer->m_Local.m_iHideHUD |= HIDEHUD_VEHICLE_CROSSHAIR; + } + + + SetBodygroup( JEEP_HOPPER_BODYGROUP, m_bBusterHopperVisible ? 1 : 0); + CreateCargoTrigger(); + + // carbar bodygroup is always on + SetBodygroup( JEEP_CARBAR_BODYGROUP, 1 ); + + m_bRadarDetectsEnemies = false; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::Activate() +{ + m_iNumRadarContacts = 0; // Force first contact tone + BaseClass::Activate(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::NPC_FinishedEnterVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ) +{ + // FIXME: This will be moved to the NPCs entering and exiting + // Fire our outputs + if ( bCompanion ) + { + m_OnCompanionEnteredVehicle.FireOutput( this, pPassenger ); + } + else + { + m_OnHostileEnteredVehicle.FireOutput( this, pPassenger ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::NPC_FinishedExitVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ) +{ + // FIXME: This will be moved to the NPCs entering and exiting + // Fire our outputs + if ( bCompanion ) + { + m_OnCompanionExitedVehicle.FireOutput( this, pPassenger ); + } + else + { + m_OnHostileExitedVehicle.FireOutput( this, pPassenger ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPassenger - +// bCompanion - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPropJeepEpisodic::NPC_CanEnterVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ) +{ + // Must be unlocked + if ( bCompanion && m_bEntranceLocked ) + return false; + + return BaseClass::NPC_CanEnterVehicle( pPassenger, bCompanion ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPassenger - +// bCompanion - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPropJeepEpisodic::NPC_CanExitVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ) +{ + // Must be unlocked + if ( bCompanion && m_bExitLocked ) + return false; + + return BaseClass::NPC_CanExitVehicle( pPassenger, bCompanion ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::InputLockEntrance( inputdata_t &data ) +{ + m_bEntranceLocked = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::InputUnlockEntrance( inputdata_t &data ) +{ + m_bEntranceLocked = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::InputLockExit( inputdata_t &data ) +{ + m_bExitLocked = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::InputUnlockExit( inputdata_t &data ) +{ + m_bExitLocked = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Turn on the Jalopy radar device +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::InputEnableRadar( inputdata_t &data ) +{ + if( m_bRadarEnabled ) + return; // Already enabled + + SetBodygroup( JEEP_RADAR_BODYGROUP, 1 ); + + SpawnRadarPanel(); +} + +//----------------------------------------------------------------------------- +// Purpose: Turn off the Jalopy radar device +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::InputDisableRadar( inputdata_t &data ) +{ + if( !m_bRadarEnabled ) + return; // Already disabled + + SetBodygroup( JEEP_RADAR_BODYGROUP, 0 ); + + DestroyRadarPanel(); +} + +//----------------------------------------------------------------------------- +// Purpose: Allow the Jalopy radar to detect Hunters and Striders +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::InputEnableRadarDetectEnemies( inputdata_t &data ) +{ + m_bRadarDetectsEnemies = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::InputAddBusterToCargo( inputdata_t &data ) +{ + if ( m_hCargoProp != NULL) + { + ReleasePropFromCargoHold(); + m_hCargoProp = NULL; + } + + CBaseEntity *pNewBomb = CreateEntityByName( "weapon_striderbuster" ); + if ( pNewBomb ) + { + DispatchSpawn( pNewBomb ); + pNewBomb->Teleport( &m_hCargoTrigger->GetAbsOrigin(), NULL, NULL ); + m_hCargoTrigger->AddCargo( pNewBomb ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CPropJeepEpisodic::PassengerInTransition( void ) +{ + // FIXME: Big hack - we need a way to bridge this data better + // TODO: Get a list of passengers we can traverse instead + CNPC_Alyx *pAlyx = CNPC_Alyx::GetAlyx(); + if ( pAlyx ) + { + if ( pAlyx->GetPassengerState() == PASSENGER_STATE_ENTERING || + pAlyx->GetPassengerState() == PASSENGER_STATE_EXITING ) + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Override velocity if our passenger is transitioning or we're upside-down +//----------------------------------------------------------------------------- +Vector CPropJeepEpisodic::PhysGunLaunchVelocity( const Vector &forward, float flMass ) +{ + // Disallow + if ( PassengerInTransition() ) + return vec3_origin; + + Vector vecPuntDir = BaseClass::PhysGunLaunchVelocity( forward, flMass ); + vecPuntDir.z = 150.0f; + vecPuntDir *= 600.0f; + return vecPuntDir; +} + +//----------------------------------------------------------------------------- +// Purpose: Rolls the vehicle when its trying to upright itself from a punt +//----------------------------------------------------------------------------- +AngularImpulse CPropJeepEpisodic::PhysGunLaunchAngularImpulse( void ) +{ + if ( IsOverturned() ) + return AngularImpulse( 0, 300, 0 ); + + // Don't spin randomly, always spin reliably + return AngularImpulse( 0, 0, 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Get the upright strength based on what state we're in +//----------------------------------------------------------------------------- +float CPropJeepEpisodic::GetUprightStrength( void ) +{ + // Lesser if overturned + if ( IsOverturned() ) + return 2.0f; + + return 0.0f; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::CreateCargoTrigger( void ) +{ + if ( m_hCargoTrigger != NULL ) + return; + + int nAttachment = LookupAttachment( "cargo" ); + if ( nAttachment ) + { + Vector vecAttachOrigin; + Vector vecForward, vecRight, vecUp; + GetAttachment( nAttachment, vecAttachOrigin, &vecForward, &vecRight, &vecUp ); + + // Approx size of the hold + Vector vecMins( -8.0, -6.0, 0 ); + Vector vecMaxs( 8.0, 6.0, 4.0 ); + + // NDebugOverlay::BoxDirection( vecAttachOrigin, vecMins, vecMaxs, vecForward, 255, 0, 0, 64, 4.0f ); + + // Create a trigger that lives for a small amount of time + m_hCargoTrigger = CVehicleCargoTrigger::Create( vecAttachOrigin, vecMins, vecMaxs, this ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: If the player uses the jeep while at the back, he gets ammo from the crate instead +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + // Fall back and get in the vehicle instead, skip giving ammo + BaseClass::BaseClass::Use( pActivator, pCaller, useType, value ); +} + +#define MIN_WHEEL_DUST_SPEED 5 + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::UpdateWheelDust( void ) +{ + // See if this wheel should emit dust + const vehicleparams_t *vehicleData = m_pServerVehicle->GetVehicleParams(); + const vehicle_operatingparams_t *carState = m_pServerVehicle->GetVehicleOperatingParams(); + bool bAllowDust = vehicleData->steering.dustCloud; + + // Car must be active + bool bCarOn = m_VehiclePhysics.IsOn(); + + // Must be moving quickly enough or skidding along the ground + bool bCreateDust = ( bCarOn && + bAllowDust && + ( m_VehiclePhysics.GetSpeed() >= MIN_WHEEL_DUST_SPEED || carState->skidSpeed > DEFAULT_SKID_THRESHOLD ) ); + + // Update our wheel dust + Vector vecPos; + for ( int i = 0; i < NUM_WHEEL_EFFECTS; i++ ) + { + m_pServerVehicle->GetWheelContactPoint( i, vecPos ); + + // Make sure the effect is created + if ( m_hWheelDust[i] == NULL ) + { + // Create the dust effect in place + m_hWheelDust[i] = (CParticleSystem *) CreateEntityByName( "info_particle_system" ); + if ( m_hWheelDust[i] == NULL ) + continue; + + // Setup our basic parameters + m_hWheelDust[i]->KeyValue( "start_active", "0" ); + m_hWheelDust[i]->KeyValue( "effect_name", "WheelDust" ); + m_hWheelDust[i]->SetParent( this ); + m_hWheelDust[i]->SetLocalOrigin( vec3_origin ); + DispatchSpawn( m_hWheelDust[i] ); + if ( gpGlobals->curtime > 0.5f ) + m_hWheelDust[i]->Activate(); + } + + // Make sure the effect is created + if ( m_hWheelWater[i] == NULL ) + { + // Create the dust effect in place + m_hWheelWater[i] = (CParticleSystem *) CreateEntityByName( "info_particle_system" ); + if ( m_hWheelWater[i] == NULL ) + continue; + + // Setup our basic parameters + m_hWheelWater[i]->KeyValue( "start_active", "0" ); + m_hWheelWater[i]->KeyValue( "effect_name", "WheelSplash" ); + m_hWheelWater[i]->SetParent( this ); + m_hWheelWater[i]->SetLocalOrigin( vec3_origin ); + DispatchSpawn( m_hWheelWater[i] ); + if ( gpGlobals->curtime > 0.5f ) + m_hWheelWater[i]->Activate(); + } + + // Turn the dust on or off + if ( bCreateDust ) + { + // Angle the dust out away from the wheels + Vector vecForward, vecRight, vecUp; + GetVectors( &vecForward, &vecRight, &vecUp ); + + const vehicle_controlparams_t *vehicleControls = m_pServerVehicle->GetVehicleControlParams(); + float flWheelDir = ( i & 1 ) ? 1.0f : -1.0f; + QAngle vecAngles; + vecForward += vecRight * flWheelDir; + vecForward += vecRight * (vehicleControls->steering*0.5f) * flWheelDir; + vecForward += vecUp; + VectorAngles( vecForward, vecAngles ); + + // NDebugOverlay::Axis( vecPos, vecAngles, 8.0f, true, 0.1f ); + + if ( m_WaterData.m_bWheelInWater[i] ) + { + m_hWheelDust[i]->StopParticleSystem(); + + // Set us up in the right position + m_hWheelWater[i]->StartParticleSystem(); + m_hWheelWater[i]->SetAbsAngles( vecAngles ); + m_hWheelWater[i]->SetAbsOrigin( vecPos + Vector( 0, 0, 8 ) ); + + if ( m_flNextWaterSound < gpGlobals->curtime ) + { + m_flNextWaterSound = gpGlobals->curtime + random->RandomFloat( 0.25f, 1.0f ); + EmitSound( "Physics.WaterSplash" ); + } + } + else + { + m_hWheelWater[i]->StopParticleSystem(); + + // Set us up in the right position + m_hWheelDust[i]->StartParticleSystem(); + m_hWheelDust[i]->SetAbsAngles( vecAngles ); + m_hWheelDust[i]->SetAbsOrigin( vecPos + Vector( 0, 0, 8 ) ); + } + } + else + { + // Stop emitting + m_hWheelDust[i]->StopParticleSystem(); + m_hWheelWater[i]->StopParticleSystem(); + } + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +ConVar jalopy_radar_test_ent( "jalopy_radar_test_ent", "none" ); + +//----------------------------------------------------------------------------- +// Purpose: Search for things that the radar detects, and stick them in the +// UTILVector that gets sent to the client for radar display. +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::UpdateRadar( bool forceUpdate ) +{ + bool bDetectedDog = false; + + if( !m_bRadarEnabled ) + return; + + if( !forceUpdate && gpGlobals->curtime < m_flNextRadarUpdateTime ) + return; + + // Count the targets on radar. If any more targets come on the radar, we beep. + int m_iNumOldRadarContacts = m_iNumRadarContacts; + + m_flNextRadarUpdateTime = gpGlobals->curtime + RADAR_UPDATE_FREQUENCY; + m_iNumRadarContacts = 0; + + CBaseEntity *pEnt = gEntList.FirstEnt(); + string_t iszRadarTarget = FindPooledString( "info_radar_target" ); + string_t iszStriderName = FindPooledString( "npc_strider" ); + string_t iszHunterName = FindPooledString( "npc_hunter" ); + + string_t iszTestName = FindPooledString( jalopy_radar_test_ent.GetString() ); + + Vector vecJalopyOrigin = WorldSpaceCenter(); + + while( pEnt != NULL ) + { + int type = RADAR_CONTACT_NONE; + + if( pEnt->m_iClassname == iszRadarTarget ) + { + CRadarTarget *pTarget = dynamic_cast<CRadarTarget*>(pEnt); + + if( pTarget != NULL && !pTarget->IsDisabled() ) + { + if( pTarget->m_flRadius < 0 || vecJalopyOrigin.DistToSqr(pTarget->GetAbsOrigin()) <= Square(pTarget->m_flRadius) ) + { + // This item has been detected. + type = pTarget->GetType(); + + if( type == RADAR_CONTACT_DOG ) + bDetectedDog = true;// used to prevent Alyx talking about the radar (see below) + + if( pTarget->GetMode() == RADAR_MODE_STICKY ) + { + // This beacon was just detected. Now change the radius to infinite + // so that it will never go off the radar due to distance. + pTarget->m_flRadius = -1; + } + } + } + } + else if ( m_bRadarDetectsEnemies ) + { + if ( pEnt->m_iClassname == iszStriderName ) + { + CNPC_Strider *pStrider = dynamic_cast<CNPC_Strider*>(pEnt); + + if( !pStrider || !pStrider->CarriedByDropship() ) + { + // Ignore striders which are carried by dropships. + type = RADAR_CONTACT_LARGE_ENEMY; + } + } + + if ( pEnt->m_iClassname == iszHunterName ) + { + type = RADAR_CONTACT_ENEMY; + } + } + + if( type != RADAR_CONTACT_NONE ) + { + Vector vecPos = pEnt->WorldSpaceCenter(); + + m_vecRadarContactPos.Set( m_iNumRadarContacts, vecPos ); + m_iRadarContactType.Set( m_iNumRadarContacts, type ); + m_iNumRadarContacts++; + + if( m_iNumRadarContacts == RADAR_MAX_CONTACTS ) + break; + } + + pEnt = gEntList.NextEnt(pEnt); + } + + if( m_iNumRadarContacts > m_iNumOldRadarContacts ) + { + // Play a bleepy sound + if( !bDetectedDog ) + { + EmitSound( "JNK_Radar_Ping_Friendly" ); + } + + //Notify Alyx so she can talk about the radar contact + CNPC_Alyx *pAlyx = CNPC_Alyx::GetAlyx(); + + if( !bDetectedDog && pAlyx != NULL && pAlyx->GetVehicle() ) + { + pAlyx->SpeakIfAllowed( TLK_PASSENGER_NEW_RADAR_CONTACT ); + } + } + + if( bDetectedDog ) + { + // Update the radar much more frequently when dog is around. + m_flNextRadarUpdateTime = gpGlobals->curtime + RADAR_UPDATE_FREQUENCY_FAST; + } + + //Msg("Server detected %d objects\n", m_iNumRadarContacts ); + + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + CSingleUserRecipientFilter filter(pPlayer); + UserMessageBegin( filter, "UpdateJalopyRadar" ); + WRITE_BYTE( 0 ); // end marker + MessageEnd(); // send message +} + +ConVar jalopy_cargo_anim_time( "jalopy_cargo_anim_time", "1.0" ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::UpdateCargoEntry( void ) +{ + // Don't bother if we have no prop to move + if ( m_hCargoProp == NULL ) + return; + + // If we're past our animation point, then we're already done + if ( m_flCargoStartTime + jalopy_cargo_anim_time.GetFloat() < gpGlobals->curtime ) + { + // Close the hold immediately if we're finished + if ( m_bAddingCargo ) + { + m_flAmmoCrateCloseTime = gpGlobals->curtime; + m_bAddingCargo = false; + } + + return; + } + + // Get our target point + int nAttachment = LookupAttachment( "cargo" ); + Vector vecTarget, vecOut; + QAngle vecAngles; + GetAttachmentLocal( nAttachment, vecTarget, vecAngles ); + + // Find where we are in the blend and bias it for a fast entry and slow ease-out + float flPerc = (jalopy_cargo_anim_time.GetFloat()) ? (( gpGlobals->curtime - m_flCargoStartTime ) / jalopy_cargo_anim_time.GetFloat()) : 1.0f; + flPerc = Bias( flPerc, 0.75f ); + VectorLerp( m_hCargoProp->GetLocalOrigin(), vecTarget, flPerc, vecOut ); + + // Get our target orientation + CPhysicsProp *pProp = dynamic_cast<CPhysicsProp *>(m_hCargoProp.Get()); + if ( pProp == NULL ) + return; + + // Slerp our quaternions to find where we are this frame + Quaternion qtTarget; + QAngle qa( 0, 90, 0 ); + qa += pProp->PreferredCarryAngles(); + AngleQuaternion( qa, qtTarget ); // FIXME: Find the real offset to make this sit properly + Quaternion qtCurrent; + AngleQuaternion( pProp->GetLocalAngles(), qtCurrent ); + + Quaternion qtOut; + QuaternionSlerp( qtCurrent, qtTarget, flPerc, qtOut ); + + // Put it back to angles + QuaternionAngles( qtOut, vecAngles ); + + // Finally, take these new position + m_hCargoProp->SetLocalOrigin( vecOut ); + m_hCargoProp->SetLocalAngles( vecAngles ); + + // Push the closing out into the future to make sure we don't try and close at the same time + m_flAmmoCrateCloseTime += gpGlobals->frametime; +} + +#define VEHICLE_AVOID_BROADCAST_RATE 0.5f + +//----------------------------------------------------------------------------- +// Purpose: This function isn't really what we want +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::CreateAvoidanceZone( void ) +{ + if ( m_flNextAvoidBroadcastTime > gpGlobals->curtime ) + return; + + // Only do this when we're stopped + if ( m_VehiclePhysics.GetSpeed() > 5.0f ) + return; + + float flHullRadius = CollisionProp()->BoundingRadius2D(); + + Vector vecPos; + CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.33f, 0.25f ), &vecPos ); + CSoundEnt::InsertSound( SOUND_MOVE_AWAY, vecPos, (flHullRadius*0.4f), VEHICLE_AVOID_BROADCAST_RATE, this ); + // NDebugOverlay::Sphere( vecPos, vec3_angle, flHullRadius*0.4f, 255, 0, 0, 0, true, VEHICLE_AVOID_BROADCAST_RATE ); + + CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.66f, 0.25f ), &vecPos ); + CSoundEnt::InsertSound( SOUND_MOVE_AWAY, vecPos, (flHullRadius*0.4f), VEHICLE_AVOID_BROADCAST_RATE, this ); + // NDebugOverlay::Sphere( vecPos, vec3_angle, flHullRadius*0.4f, 255, 0, 0, 0, true, VEHICLE_AVOID_BROADCAST_RATE ); + + // Don't broadcast again until these are done + m_flNextAvoidBroadcastTime = gpGlobals->curtime + VEHICLE_AVOID_BROADCAST_RATE; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::Think( void ) +{ + BaseClass::Think(); + + // If our passenger is transitioning, then don't let the player drive off + CNPC_Alyx *pAlyx = CNPC_Alyx::GetAlyx(); + if ( pAlyx && pAlyx->GetPassengerState() == PASSENGER_STATE_EXITING ) + { + m_throttleDisableTime = gpGlobals->curtime + 0.25f; + } + + // Update our cargo entering our hold + UpdateCargoEntry(); + + // See if the wheel dust should be on or off + UpdateWheelDust(); + + // Update the radar, of course. + UpdateRadar(); + + if ( m_hCargoTrigger && !m_hCargoProp && !m_hCargoTrigger->m_pfnTouch ) + { + m_hCargoTrigger->Enable(); + } + + CreateAvoidanceZone(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEntity - +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::AddPropToCargoHold( CPhysicsProp *pProp ) +{ + // The hold must be empty to add something to it + if ( m_hCargoProp != NULL ) + { + Assert( 0 ); + return; + } + + // Take the prop as our cargo + m_hCargoProp = pProp; + m_flCargoStartTime = gpGlobals->curtime; + m_bAddingCargo = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Drops the cargo from the hold +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::ReleasePropFromCargoHold( void ) +{ + // Pull the object free! + m_hCargoProp->SetParent( NULL ); + m_hCargoProp->CreateVPhysics(); + + if ( m_hCargoTrigger ) + { + m_hCargoTrigger->Enable(); + m_hCargoTrigger->IgnoreTouches( m_hCargoProp ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: If the player is trying to pull the cargo out of the hold using the physcannon, let him +// Output : Returns the cargo to pick up, if all the conditions are met +//----------------------------------------------------------------------------- +CBaseEntity *CPropJeepEpisodic::OnFailedPhysGunPickup( Vector vPhysgunPos ) +{ + // Make sure we're available to open + if ( m_hCargoProp != NULL ) + { + // Player's forward direction + Vector vecPlayerForward; + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + if ( pPlayer == NULL ) + return NULL; + + pPlayer->EyeVectors( &vecPlayerForward ); + + // Origin and facing of the cargo hold + Vector vecCargoOrigin; + Vector vecCargoForward; + GetAttachment( "cargo", vecCargoOrigin, &vecCargoForward ); + + // Direction from the cargo to the player's position + Vector vecPickupDir = ( vecCargoOrigin - vPhysgunPos ); + float flDist = VectorNormalize( vecPickupDir ); + + // We need to make sure the player's position is within a cone near the opening and that they're also facing the right way + bool bInCargoRange = ( (flDist < (15.0f * 12.0f)) && DotProduct( vecCargoForward, vecPickupDir ) < 0.1f ); + bool bFacingCargo = DotProduct( vecPlayerForward, vecPickupDir ) > 0.975f; + + // If we're roughly pulling at the item, pick that up + if ( bInCargoRange && bFacingCargo ) + { + // Save this for later + CBaseEntity *pCargo = m_hCargoProp; + + // Drop the cargo + ReleasePropFromCargoHold(); + + // Forget the item but pass it back as the object to pick up + m_hCargoProp = NULL; + return pCargo; + } + } + + return BaseClass::OnFailedPhysGunPickup( vPhysgunPos ); +} + +// adds a collision solver for any small props that are stuck under the vehicle +static void SolveBlockingProps( CPropJeepEpisodic *pVehicleEntity, IPhysicsObject *pVehiclePhysics ) +{ + CUtlVector<CBaseEntity *> solveList; + float vehicleMass = pVehiclePhysics->GetMass(); + Vector vehicleUp; + pVehicleEntity->GetVectors( NULL, NULL, &vehicleUp ); + IPhysicsFrictionSnapshot *pSnapshot = pVehiclePhysics->CreateFrictionSnapshot(); + while ( pSnapshot->IsValid() ) + { + IPhysicsObject *pOther = pSnapshot->GetObject(1); + float otherMass = pOther->GetMass(); + CBaseEntity *pOtherEntity = static_cast<CBaseEntity *>(pOther->GetGameData()); + Assert(pOtherEntity); + if ( pOtherEntity && pOtherEntity->GetMoveType() == MOVETYPE_VPHYSICS && pOther->IsMoveable() && (otherMass*4.0f) < vehicleMass ) + { + Vector normal; + pSnapshot->GetSurfaceNormal(normal); + // this points down in the car's reference frame, then it's probably trapped under the car + if ( DotProduct(normal, vehicleUp) < -0.9f ) + { + Vector point, pointLocal; + pSnapshot->GetContactPoint(point); + VectorITransform( point, pVehicleEntity->EntityToWorldTransform(), pointLocal ); + Vector bottomPoint = physcollision->CollideGetExtent( pVehiclePhysics->GetCollide(), vec3_origin, vec3_angle, Vector(0,0,-1) ); + // make sure it's under the bottom of the car + float bottomPlane = DotProduct(bottomPoint,vehicleUp)+8; // 8 inches above bottom + if ( DotProduct( pointLocal, vehicleUp ) <= bottomPlane ) + { + //Msg("Solved %s\n", pOtherEntity->GetClassname()); + if ( solveList.Find(pOtherEntity) < 0 ) + { + solveList.AddToTail(pOtherEntity); + } + } + } + } + pSnapshot->NextFrictionData(); + } + pVehiclePhysics->DestroyFrictionSnapshot( pSnapshot ); + if ( solveList.Count() ) + { + for ( int i = 0; i < solveList.Count(); i++ ) + { + EntityPhysics_CreateSolver( pVehicleEntity, solveList[i], true, 4.0f ); + } + pVehiclePhysics->RecheckContactPoints(); + } +} + +static void SimpleCollisionResponse( Vector velocityIn, const Vector &normal, float coefficientOfRestitution, Vector *pVelocityOut ) +{ + Vector Vn = DotProduct(velocityIn,normal) * normal; + Vector Vt = velocityIn - Vn; + *pVelocityOut = Vt - coefficientOfRestitution * Vn; +} + +static void KillBlockingEnemyNPCs( CBasePlayer *pPlayer, CBaseEntity *pVehicleEntity, IPhysicsObject *pVehiclePhysics ) +{ + Vector velocity; + pVehiclePhysics->GetVelocity( &velocity, NULL ); + float vehicleMass = pVehiclePhysics->GetMass(); + + // loop through the contacts and look for enemy NPCs that we're pushing on + CUtlVector<CAI_BaseNPC *> npcList; + CUtlVector<Vector> forceList; + CUtlVector<Vector> contactList; + IPhysicsFrictionSnapshot *pSnapshot = pVehiclePhysics->CreateFrictionSnapshot(); + while ( pSnapshot->IsValid() ) + { + IPhysicsObject *pOther = pSnapshot->GetObject(1); + float otherMass = pOther->GetMass(); + CBaseEntity *pOtherEntity = static_cast<CBaseEntity *>(pOther->GetGameData()); + CAI_BaseNPC *pNPC = pOtherEntity ? pOtherEntity->MyNPCPointer() : NULL; + // Is this an enemy NPC with a small enough mass? + if ( pNPC && pPlayer->IRelationType(pNPC) != D_LI && ((otherMass*2.0f) < vehicleMass) ) + { + // accumulate the stress force for this NPC in the lsit + float force = pSnapshot->GetNormalForce(); + Vector normal; + pSnapshot->GetSurfaceNormal(normal); + normal *= force; + int index = npcList.Find(pNPC); + if ( index < 0 ) + { + vphysicsupdateai_t *pUpdate = NULL; + if ( pNPC->VPhysicsGetObject() && pNPC->VPhysicsGetObject()->GetShadowController() && pNPC->GetMoveType() == MOVETYPE_STEP ) + { + if ( pNPC->HasDataObjectType(VPHYSICSUPDATEAI) ) + { + pUpdate = static_cast<vphysicsupdateai_t *>(pNPC->GetDataObject(VPHYSICSUPDATEAI)); + // kill this guy if I've been pushing him for more than half a second and I'm + // still pushing in his direction + if ( (gpGlobals->curtime - pUpdate->startUpdateTime) > 0.5f && DotProduct(velocity,normal) > 0) + { + index = npcList.AddToTail(pNPC); + forceList.AddToTail( normal ); + Vector pos; + pSnapshot->GetContactPoint(pos); + contactList.AddToTail(pos); + } + } + else + { + pUpdate = static_cast<vphysicsupdateai_t *>(pNPC->CreateDataObject( VPHYSICSUPDATEAI )); + pUpdate->startUpdateTime = gpGlobals->curtime; + } + // update based on vphysics for the next second + // this allows the car to push the NPC + pUpdate->stopUpdateTime = gpGlobals->curtime + 1.0f; + float maxAngular; + pNPC->VPhysicsGetObject()->GetShadowController()->GetMaxSpeed( &pUpdate->savedShadowControllerMaxSpeed, &maxAngular ); + pNPC->VPhysicsGetObject()->GetShadowController()->MaxSpeed( 1.0f, maxAngular ); + } + } + else + { + forceList[index] += normal; + } + } + pSnapshot->NextFrictionData(); + } + pVehiclePhysics->DestroyFrictionSnapshot( pSnapshot ); + // now iterate the list and check each cumulative force against the threshold + if ( npcList.Count() ) + { + for ( int i = npcList.Count(); --i >= 0; ) + { + Vector damageForce; + npcList[i]->VPhysicsGetObject()->GetVelocity( &damageForce, NULL ); + Vector vel; + pVehiclePhysics->GetVelocityAtPoint( contactList[i], &vel ); + damageForce -= vel; + Vector normal = forceList[i]; + VectorNormalize(normal); + SimpleCollisionResponse( damageForce, normal, 1.0, &damageForce ); + damageForce += (normal * 300.0f); + damageForce *= npcList[i]->VPhysicsGetObject()->GetMass(); + float len = damageForce.Length(); + damageForce.z += len*phys_upimpactforcescale.GetFloat(); + Vector vehicleForce = -damageForce; + + CTakeDamageInfo dmgInfo( pVehicleEntity, pVehicleEntity, damageForce, contactList[i], 200.0f, DMG_CRUSH|DMG_VEHICLE ); + npcList[i]->TakeDamage( dmgInfo ); + pVehiclePhysics->ApplyForceOffset( vehicleForce, contactList[i] ); + PhysCollisionSound( pVehicleEntity, npcList[i]->VPhysicsGetObject(), CHAN_BODY, pVehiclePhysics->GetMaterialIndex(), npcList[i]->VPhysicsGetObject()->GetMaterialIndex(), gpGlobals->frametime, 200.0f ); + } + } +} + +void CPropJeepEpisodic::DriveVehicle( float flFrameTime, CUserCmd *ucmd, int iButtonsDown, int iButtonsReleased ) +{ + /* The car headlight hurts perf, there's no timer to turn it off automatically, + and we haven't built any gameplay around it. + + Furthermore, I don't think I've ever seen a playtester turn it on. + + if ( ucmd->impulse == 100 ) + { + if (HeadlightIsOn()) + { + HeadlightTurnOff(); + } + else + { + HeadlightTurnOn(); + } + }*/ + + if ( ucmd->forwardmove != 0.0f ) + { + //Msg("Push V: %.2f, %.2f, %.2f\n", ucmd->forwardmove, carState->engineRPM, carState->speed ); + CBasePlayer *pPlayer = ToBasePlayer(GetDriver()); + + if ( pPlayer && VPhysicsGetObject() ) + { + KillBlockingEnemyNPCs( pPlayer, this, VPhysicsGetObject() ); + SolveBlockingProps( this, VPhysicsGetObject() ); + } + } + BaseClass::DriveVehicle(flFrameTime, ucmd, iButtonsDown, iButtonsReleased); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::CreateHazardLights( void ) +{ + static const char *s_szAttach[NUM_HAZARD_LIGHTS] = + { + "rearlight_r", + "rearlight_l", + "headlight_r", + "headlight_l", + }; + + // Turn on the hazards! + for ( int i = 0; i < NUM_HAZARD_LIGHTS; i++ ) + { + if ( m_hHazardLights[i] == NULL ) + { + m_hHazardLights[i] = CSprite::SpriteCreate( s_szHazardSprite, GetLocalOrigin(), false ); + if ( m_hHazardLights[i] ) + { + m_hHazardLights[i]->SetTransparency( kRenderWorldGlow, 255, 220, 40, 255, kRenderFxNoDissipation ); + m_hHazardLights[i]->SetAttachment( this, LookupAttachment( s_szAttach[i] ) ); + m_hHazardLights[i]->SetGlowProxySize( 2.0f ); + m_hHazardLights[i]->TurnOff(); + if ( i < 2 ) + { + // Rear lights are red + m_hHazardLights[i]->SetColor( 255, 0, 0 ); + m_hHazardLights[i]->SetScale( 1.0f ); + } + else + { + // Font lights are white + m_hHazardLights[i]->SetScale( 1.0f ); + } + } + } + } + + // We start off + m_bBlink = false; + + // Setup our blink + SetContextThink( &CPropJeepEpisodic::HazardBlinkThink, gpGlobals->curtime + 0.1f, "HazardBlink" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::DestroyHazardLights( void ) +{ + for ( int i = 0; i < NUM_HAZARD_LIGHTS; i++ ) + { + if ( m_hHazardLights[i] != NULL ) + { + UTIL_Remove( m_hHazardLights[i] ); + } + } + + SetContextThink( NULL, gpGlobals->curtime, "HazardBlink" ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : nRole - +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::ExitVehicle( int nRole ) +{ + BaseClass::ExitVehicle( nRole ); + + CreateHazardLights(); +} + +void CPropJeepEpisodic::SetBusterHopperVisibility(bool visible) +{ + // if we're there already do nothing + if (visible == m_bBusterHopperVisible) + return; + + SetBodygroup( JEEP_HOPPER_BODYGROUP, visible ? 1 : 0); + m_bBusterHopperVisible = visible; +} + + +void CPropJeepEpisodic::InputSetCargoVisibility( inputdata_t &data ) +{ + bool visible = data.value.Bool(); + + SetBusterHopperVisibility( visible ); +} + +//----------------------------------------------------------------------------- +// THIS CODE LIFTED RIGHT OUT OF TF2, to defer the pain of making vgui-on-an-entity +// code available to all CBaseAnimating. +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::SpawnRadarPanel() +{ + // FIXME: Deal with dynamically resizing control panels? + + // If we're attached to an entity, spawn control panels on it instead of use + CBaseAnimating *pEntityToSpawnOn = this; + char *pOrgLL = "controlpanel0_ll"; + char *pOrgUR = "controlpanel0_ur"; + + Assert( pEntityToSpawnOn ); + + // Lookup the attachment point... + int nLLAttachmentIndex = pEntityToSpawnOn->LookupAttachment(pOrgLL); + + if (nLLAttachmentIndex <= 0) + { + return; + } + + int nURAttachmentIndex = pEntityToSpawnOn->LookupAttachment(pOrgUR); + if (nURAttachmentIndex <= 0) + { + return; + } + + const char *pScreenName = "jalopy_radar_panel"; + const char *pScreenClassname = "vgui_screen"; + + // Compute the screen size from the attachment points... + matrix3x4_t panelToWorld; + pEntityToSpawnOn->GetAttachment( nLLAttachmentIndex, panelToWorld ); + + matrix3x4_t worldToPanel; + MatrixInvert( panelToWorld, worldToPanel ); + + // Now get the lower right position + transform into panel space + Vector lr, lrlocal; + pEntityToSpawnOn->GetAttachment( nURAttachmentIndex, panelToWorld ); + MatrixGetColumn( panelToWorld, 3, lr ); + VectorTransform( lr, worldToPanel, lrlocal ); + + float flWidth = lrlocal.x; + float flHeight = lrlocal.y; + + CVGuiScreen *pScreen = CreateVGuiScreen( pScreenClassname, pScreenName, pEntityToSpawnOn, this, nLLAttachmentIndex ); + pScreen->SetActualSize( flWidth, flHeight ); + pScreen->SetActive( true ); + pScreen->SetOverlayMaterial( RADAR_PANEL_WRITEZ ); + pScreen->SetTransparency( true ); + + m_hRadarScreen.Set( pScreen ); + + m_bRadarEnabled = true; + m_iNumRadarContacts = 0; + m_flNextRadarUpdateTime = gpGlobals->curtime - 1.0f; +} + +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::DestroyRadarPanel() +{ + Assert( m_hRadarScreen != NULL ); + m_hRadarScreen->SUB_Remove(); + m_bRadarEnabled = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::HazardBlinkThink( void ) +{ + if ( m_bBlink ) + { + for ( int i = 0; i < NUM_HAZARD_LIGHTS; i++ ) + { + if ( m_hHazardLights[i] ) + { + m_hHazardLights[i]->SetBrightness( 0, 0.1f ); + } + } + + SetContextThink( &CPropJeepEpisodic::HazardBlinkThink, gpGlobals->curtime + 0.25f, "HazardBlink" ); + } + else + { + for ( int i = 0; i < NUM_HAZARD_LIGHTS; i++ ) + { + if ( m_hHazardLights[i] ) + { + m_hHazardLights[i]->SetBrightness( 255, 0.1f ); + m_hHazardLights[i]->TurnOn(); + } + } + + SetContextThink( &CPropJeepEpisodic::HazardBlinkThink, gpGlobals->curtime + 0.5f, "HazardBlink" ); + } + + m_bBlink = !m_bBlink; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::HandleWater( void ) +{ + // Only check the wheels and engine in water if we have a driver (player). + if ( !GetDriver() ) + return; + + // Update our internal state + CheckWater(); + + // Save of data from last think. + for ( int iWheel = 0; iWheel < JEEP_WHEEL_COUNT; ++iWheel ) + { + m_WaterData.m_bWheelWasInWater[iWheel] = m_WaterData.m_bWheelInWater[iWheel]; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Report our lock state +//----------------------------------------------------------------------------- +int CPropJeepEpisodic::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if ( m_debugOverlays & OVERLAY_TEXT_BIT ) + { + EntityText( text_offset, CFmtStr("Entrance: %s", m_bEntranceLocked ? "Locked" : "Unlocked" ), 0 ); + text_offset++; + + EntityText( text_offset, CFmtStr("Exit: %s", m_bExitLocked ? "Locked" : "Unlocked" ), 0 ); + text_offset++; + } + + return text_offset; +} + +#define TRANSITION_SEARCH_RADIUS (100*12) + +//----------------------------------------------------------------------------- +// Purpose: Teleport the car to a destination that will cause it to transition if it's not going to otherwise +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::InputOutsideTransition( inputdata_t &inputdata ) +{ + // Teleport into the new map + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + Vector vecTeleportPos; + QAngle vecTeleportAngles; + + // Get our bounds + Vector vecSurroundMins, vecSurroundMaxs; + CollisionProp()->WorldSpaceSurroundingBounds( &vecSurroundMins, &vecSurroundMaxs ); + vecSurroundMins -= WorldSpaceCenter(); + vecSurroundMaxs -= WorldSpaceCenter(); + + Vector vecBestPos; + QAngle vecBestAngles; + + CInfoTargetVehicleTransition *pEntity = NULL; + bool bSucceeded = false; + + // Find all entities of the correct name and try and sit where they're at + while ( ( pEntity = (CInfoTargetVehicleTransition *) gEntList.FindEntityByClassname( pEntity, "info_target_vehicle_transition" ) ) != NULL ) + { + // Must be enabled + if ( pEntity->IsDisabled() ) + continue; + + // Must be within range + if ( ( pEntity->GetAbsOrigin() - pPlayer->GetAbsOrigin() ).LengthSqr() > Square( TRANSITION_SEARCH_RADIUS ) ) + continue; + + vecTeleportPos = pEntity->GetAbsOrigin(); + vecTeleportAngles = pEntity->GetAbsAngles() + QAngle( 0, -90, 0 ); // Vehicle is always off by 90 degrees + + // Rotate to face the destination angles + Vector vecMins; + Vector vecMaxs; + VectorRotate( vecSurroundMins, vecTeleportAngles, vecMins ); + VectorRotate( vecSurroundMaxs, vecTeleportAngles, vecMaxs ); + + if ( vecMaxs.x < vecMins.x ) + V_swap( vecMins.x, vecMaxs.x ); + + if ( vecMaxs.y < vecMins.y ) + V_swap( vecMins.y, vecMaxs.y ); + + if ( vecMaxs.z < vecMins.z ) + V_swap( vecMins.z, vecMaxs.z ); + + // Move up + vecTeleportPos.z += ( vecMaxs.z - vecMins.z ); + + trace_t tr; + UTIL_TraceHull( vecTeleportPos, vecTeleportPos - Vector( 0, 0, 128 ), vecMins, vecMaxs, MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); + if ( tr.startsolid == false && tr.allsolid == false && tr.fraction < 1.0f ) + { + // Store this off + vecBestPos = tr.endpos; + vecBestAngles = vecTeleportAngles; + bSucceeded = true; + + // If this point isn't visible, then stop looking and use it + if ( pPlayer->FInViewCone( tr.endpos ) == false ) + break; + } + } + + // See if we're finished + if ( bSucceeded ) + { + Teleport( &vecTeleportPos, &vecTeleportAngles, NULL ); + return; + } + + // TODO: We found no valid teleport points, so try to find them dynamically + Warning("No valid vehicle teleport points!\n"); +} + +//----------------------------------------------------------------------------- +// Purpose: Stop players punting the car around. +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::InputDisablePhysGun( inputdata_t &data ) +{ + AddEFlags( EFL_NO_PHYSCANNON_INTERACTION ); +} +//----------------------------------------------------------------------------- +// Purpose: Return to normal +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::InputEnablePhysGun( inputdata_t &data ) +{ + RemoveEFlags( EFL_NO_PHYSCANNON_INTERACTION ); +} + +//----------------------------------------------------------------------------- +// Create and parent two radial node link controllers. +//----------------------------------------------------------------------------- +void CPropJeepEpisodic::InputCreateLinkController( inputdata_t &data ) +{ + Vector vecFront, vecRear; + Vector vecWFL, vecWFR; // Front wheels + Vector vecWRL, vecWRR; // Back wheels + + GetAttachment( "wheel_fr", vecWFR ); + GetAttachment( "wheel_fl", vecWFL ); + + GetAttachment( "wheel_rr", vecWRR ); + GetAttachment( "wheel_rl", vecWRL ); + + vecFront = (vecWFL + vecWFR) * 0.5f; + vecRear = (vecWRL + vecWRR) * 0.5f; + + float flRadius = ( (vecFront - vecRear).Length() ) * 0.6f; + + CAI_RadialLinkController *pLinkController = (CAI_RadialLinkController *)CreateEntityByName( "info_radial_link_controller" ); + if( pLinkController != NULL && m_hLinkControllerFront.Get() == NULL ) + { + pLinkController->m_flRadius = flRadius; + pLinkController->Spawn(); + pLinkController->SetAbsOrigin( vecFront ); + pLinkController->SetOwnerEntity( this ); + pLinkController->SetParent( this ); + pLinkController->Activate(); + m_hLinkControllerFront.Set( pLinkController ); + + //NDebugOverlay::Circle( vecFront, Vector(0,1,0), Vector(1,0,0), flRadius, 255, 255, 255, 128, false, 100 ); + } + + pLinkController = (CAI_RadialLinkController *)CreateEntityByName( "info_radial_link_controller" ); + if( pLinkController != NULL && m_hLinkControllerRear.Get() == NULL ) + { + pLinkController->m_flRadius = flRadius; + pLinkController->Spawn(); + pLinkController->SetAbsOrigin( vecRear ); + pLinkController->SetOwnerEntity( this ); + pLinkController->SetParent( this ); + pLinkController->Activate(); + m_hLinkControllerRear.Set( pLinkController ); + + //NDebugOverlay::Circle( vecRear, Vector(0,1,0), Vector(1,0,0), flRadius, 255, 255, 255, 128, false, 100 ); + } +} + +void CPropJeepEpisodic::InputDestroyLinkController( inputdata_t &data ) +{ + if( m_hLinkControllerFront.Get() != NULL ) + { + CAI_RadialLinkController *pLinkController = dynamic_cast<CAI_RadialLinkController*>(m_hLinkControllerFront.Get()); + if( pLinkController != NULL ) + { + pLinkController->ModifyNodeLinks(false); + UTIL_Remove( pLinkController ); + m_hLinkControllerFront.Set(NULL); + } + } + + if( m_hLinkControllerRear.Get() != NULL ) + { + CAI_RadialLinkController *pLinkController = dynamic_cast<CAI_RadialLinkController*>(m_hLinkControllerRear.Get()); + if( pLinkController != NULL ) + { + pLinkController->ModifyNodeLinks(false); + UTIL_Remove( pLinkController ); + m_hLinkControllerRear.Set(NULL); + } + } +} + + +bool CPropJeepEpisodic::AllowBlockedExit( CBaseCombatCharacter *pPassenger, int nRole ) +{ + // Wait until we've settled down before we resort to blocked exits. + // This keeps us from doing blocked exits in mid-jump, which can cause mayhem like + // sticking the player through player clips or into geometry. + return GetSmoothedVelocity().IsLengthLessThan( jalopy_blocked_exit_max_speed.GetFloat() ); +} + diff --git a/game/server/episodic/vehicle_jeep_episodic.h b/game/server/episodic/vehicle_jeep_episodic.h new file mode 100644 index 0000000..70cb589 --- /dev/null +++ b/game/server/episodic/vehicle_jeep_episodic.h @@ -0,0 +1,150 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef VEHICLE_JEEP_EPISODIC_H +#define VEHICLE_JEEP_EPISODIC_H +#ifdef _WIN32 +#pragma once +#endif + +#include "vehicle_jeep.h" +#include "ai_basenpc.h" +#include "hl2_vehicle_radar.h" + +class CParticleSystem; +class CVehicleCargoTrigger; +class CSprite; + +#define NUM_WHEEL_EFFECTS 2 +#define NUM_HAZARD_LIGHTS 4 + +//============================================================================= +// Episodic jeep + +class CPropJeepEpisodic : public CPropJeep +{ + DECLARE_CLASS( CPropJeepEpisodic, CPropJeep ); + DECLARE_SERVERCLASS(); + +public: + CPropJeepEpisodic( void ); + + virtual void Spawn( void ); + virtual void Activate( void ); + virtual void Think( void ); + virtual void UpdateOnRemove( void ); + + virtual void NPC_FinishedEnterVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ); + virtual void NPC_FinishedExitVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ); + + virtual bool NPC_CanEnterVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ); + virtual bool NPC_CanExitVehicle( CAI_BaseNPC *pPassenger, bool bCompanion ); + virtual void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ); + virtual void Precache( void ); + virtual void EnterVehicle( CBaseCombatCharacter *pPassenger ); + virtual void ExitVehicle( int nRole ); + virtual bool AllowBlockedExit( CBaseCombatCharacter *pPassenger, int nRole ); + + // Passengers take no damage except what we pass them + virtual bool PassengerShouldReceiveDamage( CTakeDamageInfo &info ) + { + if ( GetServerVehicle() && GetServerVehicle()->IsPassengerExiting() ) + return false; + + return ( info.GetDamageType() & DMG_VEHICLE ) != 0; + } + + virtual int ObjectCaps( void ) { return (BaseClass::ObjectCaps() | FCAP_NOTIFY_ON_TRANSITION); } + + void SpawnRadarPanel(); + void DestroyRadarPanel(); + int NumRadarContacts() { return m_iNumRadarContacts; } + + void AddPropToCargoHold( CPhysicsProp *pProp ); + + virtual CBaseEntity *OnFailedPhysGunPickup( Vector vPhysgunPos ); + virtual void DriveVehicle( float flFrameTime, CUserCmd *ucmd, int iButtonsDown, int iButtonsReleased ); + virtual int DrawDebugTextOverlays( void ); + + DECLARE_DATADESC(); + +protected: + void HazardBlinkThink( void ); + void CreateHazardLights( void ); + void DestroyHazardLights( void ); + + void UpdateCargoEntry( void ); + void ReleasePropFromCargoHold( void ); + void CreateCargoTrigger( void ); + virtual float GetUprightTime( void ) { return 1.0f; } + virtual float GetUprightStrength( void ); + virtual bool ShouldPuntUseLaunchForces( PhysGunForce_t reason ) { return ( reason == PHYSGUN_FORCE_PUNTED ); } + virtual void HandleWater( void ); + + virtual AngularImpulse PhysGunLaunchAngularImpulse( void ); + virtual Vector PhysGunLaunchVelocity( const Vector &forward, float flMass ); + bool PassengerInTransition( void ); + + void SetBusterHopperVisibility(bool visible); + +private: + + void UpdateWheelDust( void ); + void UpdateRadar( bool forceUpdate = false ); + + void InputLockEntrance( inputdata_t &data ); + void InputUnlockEntrance( inputdata_t &data ); + void InputLockExit( inputdata_t &data ); + void InputUnlockExit( inputdata_t &data ); + void InputEnableRadar( inputdata_t &data ); + void InputDisableRadar( inputdata_t &data ); + void InputEnableRadarDetectEnemies( inputdata_t &data ); + void InputAddBusterToCargo( inputdata_t &data ); + void InputSetCargoVisibility( inputdata_t &data ); + void InputOutsideTransition( inputdata_t &data ); + void InputDisablePhysGun( inputdata_t &data ); + void InputEnablePhysGun( inputdata_t &data ); + void InputCreateLinkController( inputdata_t &data ); + void InputDestroyLinkController( inputdata_t &data ); + void CreateAvoidanceZone( void ); + + bool m_bEntranceLocked; + bool m_bExitLocked; + bool m_bAddingCargo; + bool m_bBlink; + + float m_flCargoStartTime; // Time when the cargo was first added to the vehicle (used for animating into hold) + float m_flNextAvoidBroadcastTime; // Next time we'll warn entity to move out of us + + COutputEvent m_OnCompanionEnteredVehicle; // Passenger has completed entering the vehicle + COutputEvent m_OnCompanionExitedVehicle; // Passenger has completed exited the vehicle + COutputEvent m_OnHostileEnteredVehicle; // Passenger has completed entering the vehicle + COutputEvent m_OnHostileExitedVehicle; // Passenger has completed exited the vehicle + + CHandle< CParticleSystem > m_hWheelDust[NUM_WHEEL_EFFECTS]; + CHandle< CParticleSystem > m_hWheelWater[NUM_WHEEL_EFFECTS]; + CHandle< CVehicleCargoTrigger > m_hCargoTrigger; + CHandle< CPhysicsProp > m_hCargoProp; + + CHandle< CSprite > m_hHazardLights[NUM_HAZARD_LIGHTS]; + float m_flNextWaterSound; + + bool m_bRadarEnabled; + bool m_bRadarDetectsEnemies; + float m_flNextRadarUpdateTime; + EHANDLE m_hRadarScreen; + + EHANDLE m_hLinkControllerFront; + EHANDLE m_hLinkControllerRear; + + bool m_bBusterHopperVisible; // is the hopper assembly visible on the vehicle? please do not set this directly - use the accessor funct. + + CNetworkVar( int, m_iNumRadarContacts ); + CNetworkArray( Vector, m_vecRadarContactPos, RADAR_MAX_CONTACTS ); + CNetworkArray( int, m_iRadarContactType, RADAR_MAX_CONTACTS ); +}; + +#endif // VEHICLE_JEEP_EPISODIC_H diff --git a/game/server/episodic/weapon_hopwire.cpp b/game/server/episodic/weapon_hopwire.cpp new file mode 100644 index 0000000..798f6eb --- /dev/null +++ b/game/server/episodic/weapon_hopwire.cpp @@ -0,0 +1,508 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#include "cbase.h" +#include "basehlcombatweapon.h" +#include "player.h" +#include "gamerules.h" +#include "grenade_frag.h" +#include "npcevent.h" +#include "engine/IEngineSound.h" +#include "items.h" +#include "in_buttons.h" +#include "soundent.h" +#include "grenade_hopwire.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define GRENADE_TIMER 2.0f //Seconds + +#define GRENADE_PAUSED_NO 0 +#define GRENADE_PAUSED_PRIMARY 1 +#define GRENADE_PAUSED_SECONDARY 2 + +#define GRENADE_RADIUS 4.0f // inches + +//----------------------------------------------------------------------------- +// Fragmentation grenades +//----------------------------------------------------------------------------- +class CWeaponHopwire: public CBaseHLCombatWeapon +{ + DECLARE_CLASS( CWeaponHopwire, CBaseHLCombatWeapon ); +public: + DECLARE_SERVERCLASS(); + + void Precache( void ); + void Operator_HandleAnimEvent( animevent_t *pEvent, CBaseCombatCharacter *pOperator ); + void PrimaryAttack( void ); + void SecondaryAttack( void ); + void DecrementAmmo( CBaseCombatCharacter *pOwner ); + void ItemPostFrame( void ); + + void HandleFireOnEmpty( void ); + bool HasAnyAmmo( void ); + bool Deploy( void ); + bool Holster( CBaseCombatWeapon *pSwitchingTo = NULL ); + + int CapabilitiesGet( void ) { return bits_CAP_WEAPON_RANGE_ATTACK1; } + + bool Reload( void ); + +private: + void ThrowGrenade( CBasePlayer *pPlayer ); + void RollGrenade( CBasePlayer *pPlayer ); + void LobGrenade( CBasePlayer *pPlayer ); + // check a throw from vecSrc. If not valid, move the position back along the line to vecEye + void CheckThrowPosition( CBasePlayer *pPlayer, const Vector &vecEye, Vector &vecSrc ); + + bool m_bRedraw; //Draw the weapon again after throwing a grenade + + int m_AttackPaused; + bool m_fDrawbackFinished; + + CHandle<CGrenadeHopwire> m_hActiveHopWire; + + DECLARE_ACTTABLE(); + + DECLARE_DATADESC(); +}; + + +BEGIN_DATADESC( CWeaponHopwire ) + DEFINE_FIELD( m_bRedraw, FIELD_BOOLEAN ), + DEFINE_FIELD( m_AttackPaused, FIELD_INTEGER ), + DEFINE_FIELD( m_fDrawbackFinished, FIELD_BOOLEAN ), + DEFINE_FIELD( m_hActiveHopWire, FIELD_EHANDLE ), +END_DATADESC() + +acttable_t CWeaponHopwire::m_acttable[] = +{ + { ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_SLAM, true }, +}; + +IMPLEMENT_ACTTABLE(CWeaponHopwire); + +IMPLEMENT_SERVERCLASS_ST(CWeaponHopwire, DT_WeaponHopwire) +END_SEND_TABLE() + +LINK_ENTITY_TO_CLASS( weapon_hopwire, CWeaponHopwire ); +PRECACHE_WEAPON_REGISTER(weapon_hopwire); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponHopwire::Precache( void ) +{ + BaseClass::Precache(); + + UTIL_PrecacheOther( "npc_grenade_hopwire" ); + + PrecacheScriptSound( "WeaponFrag.Throw" ); + PrecacheScriptSound( "WeaponFrag.Roll" ); + + m_bRedraw = false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CWeaponHopwire::Deploy( void ) +{ + m_bRedraw = false; + m_fDrawbackFinished = false; + + return BaseClass::Deploy(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CWeaponHopwire::Holster( CBaseCombatWeapon *pSwitchingTo ) +{ + if ( m_hActiveHopWire != NULL ) + return false; + + m_bRedraw = false; + m_fDrawbackFinished = false; + + return BaseClass::Holster( pSwitchingTo ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEvent - +// *pOperator - +//----------------------------------------------------------------------------- +void CWeaponHopwire::Operator_HandleAnimEvent( animevent_t *pEvent, CBaseCombatCharacter *pOperator ) +{ + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + bool fThrewGrenade = false; + + switch( pEvent->event ) + { + case EVENT_WEAPON_SEQUENCE_FINISHED: + m_fDrawbackFinished = true; + break; + + case EVENT_WEAPON_THROW: + ThrowGrenade( pOwner ); + DecrementAmmo( pOwner ); + fThrewGrenade = true; + break; + + case EVENT_WEAPON_THROW2: + RollGrenade( pOwner ); + DecrementAmmo( pOwner ); + fThrewGrenade = true; + break; + + case EVENT_WEAPON_THROW3: + LobGrenade( pOwner ); + DecrementAmmo( pOwner ); + fThrewGrenade = true; + break; + + default: + BaseClass::Operator_HandleAnimEvent( pEvent, pOperator ); + break; + } + +#define RETHROW_DELAY 0.5 + if( fThrewGrenade ) + { + m_flNextPrimaryAttack = gpGlobals->curtime + RETHROW_DELAY; + m_flNextSecondaryAttack = gpGlobals->curtime + RETHROW_DELAY; + m_flTimeWeaponIdle = FLT_MAX; //NOTE: This is set once the animation has finished up! + + // Make a sound designed to scare snipers back into their holes! + CBaseCombatCharacter *pOwner = GetOwner(); + + if( pOwner ) + { + Vector vecSrc = pOwner->Weapon_ShootPosition(); + Vector vecDir; + + AngleVectors( pOwner->EyeAngles(), &vecDir ); + + trace_t tr; + + UTIL_TraceLine( vecSrc, vecSrc + vecDir * 1024, MASK_SOLID_BRUSHONLY, pOwner, COLLISION_GROUP_NONE, &tr ); + + CSoundEnt::InsertSound( SOUND_DANGER_SNIPERONLY, tr.endpos, 384, 0.2, pOwner ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Override the ammo behavior so we never disallow pulling the weapon out +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CWeaponHopwire::HasAnyAmmo( void ) +{ + if ( m_hActiveHopWire != NULL ) + return true; + + return BaseClass::HasAnyAmmo(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CWeaponHopwire::Reload( void ) +{ + if ( !HasPrimaryAmmo() ) + return false; + + if ( ( m_bRedraw ) && ( m_flNextPrimaryAttack <= gpGlobals->curtime ) && ( m_flNextSecondaryAttack <= gpGlobals->curtime ) ) + { + //Redraw the weapon + SendWeaponAnim( ACT_VM_DRAW ); + + //Update our times + m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration(); + m_flNextSecondaryAttack = gpGlobals->curtime + SequenceDuration(); + m_flTimeWeaponIdle = gpGlobals->curtime + SequenceDuration(); + + //Mark this as done + m_bRedraw = false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponHopwire::SecondaryAttack( void ) +{ + /* + if ( m_bRedraw ) + return; + + if ( !HasPrimaryAmmo() ) + return; + + CBaseCombatCharacter *pOwner = GetOwner(); + + if ( pOwner == NULL ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( pOwner ); + + if ( pPlayer == NULL ) + return; + + // Note that this is a secondary attack and prepare the grenade attack to pause. + m_AttackPaused = GRENADE_PAUSED_SECONDARY; + SendWeaponAnim( ACT_VM_PULLBACK_LOW ); + + // Don't let weapon idle interfere in the middle of a throw! + m_flTimeWeaponIdle = FLT_MAX; + m_flNextSecondaryAttack = FLT_MAX; + + // If I'm now out of ammo, switch away + if ( !HasPrimaryAmmo() ) + { + pPlayer->SwitchToNextBestWeapon( this ); + } + */ +} + +//----------------------------------------------------------------------------- +// Purpose: Allow activation even if this is our last piece of ammo +//----------------------------------------------------------------------------- +void CWeaponHopwire::HandleFireOnEmpty( void ) +{ + if ( m_hActiveHopWire!= NULL ) + { + // FIXME: This toggle is hokey + m_bRedraw = false; + PrimaryAttack(); + m_bRedraw = true; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponHopwire::PrimaryAttack( void ) +{ + if ( m_bRedraw ) + return; + + CBaseCombatCharacter *pOwner = GetOwner(); + if ( pOwner == NULL ) + return; + + CBasePlayer *pPlayer = ToBasePlayer( GetOwner() );; + if ( !pPlayer ) + return; + + // See if we're in trap mode + if ( hopwire_trap.GetBool() && ( m_hActiveHopWire != NULL ) ) + { + // Spring the trap + m_hActiveHopWire->Detonate(); + m_hActiveHopWire = NULL; + + // Don't allow another throw for awhile + m_flTimeWeaponIdle = m_flNextPrimaryAttack = gpGlobals->curtime + 2.0f; + + return; + } + + // Note that this is a primary attack and prepare the grenade attack to pause. + /* + m_AttackPaused = GRENADE_PAUSED_PRIMARY; + SendWeaponAnim( ACT_VM_PULLBACK_HIGH ); + */ + m_AttackPaused = GRENADE_PAUSED_SECONDARY; + SendWeaponAnim( ACT_VM_PULLBACK_LOW ); + + // Put both of these off indefinitely. We do not know how long + // the player will hold the grenade. + m_flTimeWeaponIdle = FLT_MAX; + m_flNextPrimaryAttack = FLT_MAX; + + // If I'm now out of ammo, switch away + /* + if ( !HasPrimaryAmmo() ) + { + pPlayer->SwitchToNextBestWeapon( this ); + } + */ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pOwner - +//----------------------------------------------------------------------------- +void CWeaponHopwire::DecrementAmmo( CBaseCombatCharacter *pOwner ) +{ + pOwner->RemoveAmmo( 1, m_iPrimaryAmmoType ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponHopwire::ItemPostFrame( void ) +{ + if( m_fDrawbackFinished ) + { + CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); + + if (pOwner) + { + switch( m_AttackPaused ) + { + case GRENADE_PAUSED_PRIMARY: + if( !(pOwner->m_nButtons & IN_ATTACK) ) + { + SendWeaponAnim( ACT_VM_THROW ); + m_fDrawbackFinished = false; + } + break; + + case GRENADE_PAUSED_SECONDARY: + if( !(pOwner->m_nButtons & (IN_ATTACK|IN_ATTACK2)) ) + { + //See if we're ducking + if ( pOwner->m_nButtons & IN_DUCK ) + { + //Send the weapon animation + SendWeaponAnim( ACT_VM_SECONDARYATTACK ); + } + else + { + //Send the weapon animation + SendWeaponAnim( ACT_VM_HAULBACK ); + } + + m_fDrawbackFinished = false; + } + break; + + default: + break; + } + } + } + + BaseClass::ItemPostFrame(); + + if ( m_bRedraw ) + { + if ( IsViewModelSequenceFinished() ) + { + Reload(); + } + } +} + + // check a throw from vecSrc. If not valid, move the position back along the line to vecEye +void CWeaponHopwire::CheckThrowPosition( CBasePlayer *pPlayer, const Vector &vecEye, Vector &vecSrc ) +{ + trace_t tr; + + UTIL_TraceHull( vecEye, vecSrc, -Vector(GRENADE_RADIUS+2,GRENADE_RADIUS+2,GRENADE_RADIUS+2), Vector(GRENADE_RADIUS+2,GRENADE_RADIUS+2,GRENADE_RADIUS+2), + pPlayer->PhysicsSolidMaskForEntity(), pPlayer, pPlayer->GetCollisionGroup(), &tr ); + + if ( tr.DidHit() ) + { + vecSrc = tr.endpos; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPlayer - +//----------------------------------------------------------------------------- +void CWeaponHopwire::ThrowGrenade( CBasePlayer *pPlayer ) +{ + Vector vecEye = pPlayer->EyePosition(); + Vector vForward, vRight; + + pPlayer->EyeVectors( &vForward, &vRight, NULL ); + Vector vecSrc = vecEye + vForward * 18.0f + vRight * 8.0f; + CheckThrowPosition( pPlayer, vecEye, vecSrc ); + vForward[2] += 0.1f; + + Vector vecThrow; + pPlayer->GetVelocity( &vecThrow, NULL ); + vecThrow += vForward * 1200; + m_hActiveHopWire = static_cast<CGrenadeHopwire *> (HopWire_Create( vecSrc, vec3_angle, vecThrow, AngularImpulse(600,random->RandomInt(-1200,1200),0), pPlayer, GRENADE_TIMER )); + + m_bRedraw = true; + + WeaponSound( SINGLE ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPlayer - +//----------------------------------------------------------------------------- +void CWeaponHopwire::LobGrenade( CBasePlayer *pPlayer ) +{ + Vector vecEye = pPlayer->EyePosition(); + Vector vForward, vRight; + + pPlayer->EyeVectors( &vForward, &vRight, NULL ); + Vector vecSrc = vecEye + vForward * 18.0f + vRight * 8.0f + Vector( 0, 0, -8 ); + CheckThrowPosition( pPlayer, vecEye, vecSrc ); + + Vector vecThrow; + pPlayer->GetVelocity( &vecThrow, NULL ); + vecThrow += vForward * 350 + Vector( 0, 0, 50 ); + m_hActiveHopWire = static_cast<CGrenadeHopwire *> (HopWire_Create( vecSrc, vec3_angle, vecThrow, AngularImpulse(200,random->RandomInt(-600,600),0), pPlayer, GRENADE_TIMER )); + + WeaponSound( WPN_DOUBLE ); + + m_bRedraw = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pPlayer - +//----------------------------------------------------------------------------- +void CWeaponHopwire::RollGrenade( CBasePlayer *pPlayer ) +{ + // BUGBUG: Hardcoded grenade width of 4 - better not change the model :) + Vector vecSrc; + pPlayer->CollisionProp()->NormalizedToWorldSpace( Vector( 0.5f, 0.5f, 0.0f ), &vecSrc ); + vecSrc.z += GRENADE_RADIUS; + + Vector vecFacing = pPlayer->BodyDirection2D( ); + // no up/down direction + vecFacing.z = 0; + VectorNormalize( vecFacing ); + trace_t tr; + UTIL_TraceLine( vecSrc, vecSrc - Vector(0,0,16), MASK_PLAYERSOLID, pPlayer, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction != 1.0 ) + { + // compute forward vec parallel to floor plane and roll grenade along that + Vector tangent; + CrossProduct( vecFacing, tr.plane.normal, tangent ); + CrossProduct( tr.plane.normal, tangent, vecFacing ); + } + vecSrc += (vecFacing * 18.0); + CheckThrowPosition( pPlayer, pPlayer->WorldSpaceCenter(), vecSrc ); + + Vector vecThrow; + pPlayer->GetVelocity( &vecThrow, NULL ); + vecThrow += vecFacing * 700; + // put it on its side + QAngle orientation(0,pPlayer->GetLocalAngles().y,-90); + // roll it + AngularImpulse rotSpeed(0,0,720); + m_hActiveHopWire = static_cast<CGrenadeHopwire *> (HopWire_Create( vecSrc, orientation, vecThrow, rotSpeed, pPlayer, GRENADE_TIMER )); + + WeaponSound( SPECIAL1 ); + + m_bRedraw = true; +} diff --git a/game/server/episodic/weapon_oldmanharpoon.cpp b/game/server/episodic/weapon_oldmanharpoon.cpp new file mode 100644 index 0000000..497fe22 --- /dev/null +++ b/game/server/episodic/weapon_oldmanharpoon.cpp @@ -0,0 +1,36 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#include "cbase.h" +#include "weapon_citizenpackage.h" + +//----------------------------------------------------------------------------- +// Purpose: Old Man Harpoon - Lost Coast. +//----------------------------------------------------------------------------- +class CWeaponOldManHarpoon : public CWeaponCitizenPackage +{ + DECLARE_CLASS( CWeaponOldManHarpoon, CWeaponCitizenPackage ); +public: + DECLARE_SERVERCLASS(); + DECLARE_DATADESC(); + DECLARE_ACTTABLE(); +}; + +IMPLEMENT_SERVERCLASS_ST( CWeaponOldManHarpoon, DT_WeaponOldManHarpoon ) +END_SEND_TABLE() + +BEGIN_DATADESC( CWeaponOldManHarpoon ) +END_DATADESC() + +LINK_ENTITY_TO_CLASS( weapon_oldmanharpoon, CWeaponOldManHarpoon ); +PRECACHE_WEAPON_REGISTER( weapon_oldmanharpoon ); + +acttable_t CWeaponOldManHarpoon::m_acttable[] = +{ + { ACT_IDLE, ACT_IDLE_SUITCASE, false }, + { ACT_WALK, ACT_WALK_SUITCASE, false }, +}; +IMPLEMENT_ACTTABLE( CWeaponOldManHarpoon ); diff --git a/game/server/episodic/weapon_striderbuster.cpp b/game/server/episodic/weapon_striderbuster.cpp new file mode 100644 index 0000000..3c766c5 --- /dev/null +++ b/game/server/episodic/weapon_striderbuster.cpp @@ -0,0 +1,1175 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// An ingenious device. We call it "The Magnusson Device". Not my chosen label, +// you understand, but it seemed to please the personnel. +// +// From your point of view you simply throw it at a strider and then blow it up. +// +//============================================================================= + +#include "cbase.h" +#include "props.h" +#include "vphysics/constraints.h" +#include "physics_saverestore.h" +#include "model_types.h" +#include "ai_utils.h" +#include "particle_system.h" +#include "Sprite.h" +#include "citadel_effects_shared.h" +#include "soundent.h" +#include "SpriteTrail.h" +#include "te_effect_dispatch.h" +#include "beam_shared.h" +#include "npc_strider.h" +#include "npc_hunter.h" +#include "particle_parse.h" +#include "gameweaponmanager.h" +#include "gamestats.h" + +extern ConVar hunter_hate_held_striderbusters; +extern ConVar hunter_hate_thrown_striderbusters; +extern ConVar hunter_hate_attached_striderbusters; + + +ConVar striderbuster_health( "striderbuster_health", "14" ); +ConVar striderbuster_autoaim_radius( "striderbuster_autoaim_radius", "64.0f" ); +ConVar striderbuster_shot_velocity( "striderbuster_shot_velocity", "2500.0", FCVAR_NONE, "Speed at which launch the bomb from the physcannon" ); +ConVar striderbuster_allow_all_damage( "striderbuster_allow_all_damage", "0", FCVAR_NONE, "If set to '1' the bomb will detonate on any damage taken. Otherwise only the player may trigger it." ); + +//ConVar striderbuster_magnetic_radius("striderbuster_magnetic_radius","400.0f", FCVAR_NONE,"Maximum distance at which magnade experiences attraction to a target. Set to 0 to disable magnetism."); +ConVar striderbuster_magnetic_force_strider("striderbuster_magnetic_force_strider", "750000.0f", FCVAR_NONE,"Intensity of magnade's attraction to a strider."); +ConVar striderbuster_magnetic_force_hunter("striderbuster_magnetic_force_hunter","1750000.0f",FCVAR_NONE,"Intensity of magnade's attraction to a hunter."); +ConVar striderbuster_falloff_power("striderbuster_falloff_power","4",FCVAR_NONE,"Order of the distance falloff. 1 = linear 2 = quadratic"); +ConVar striderbuster_leg_stick_dist( "striderbuster_leg_stick_dist", "80.0", FCVAR_NONE, "If the buster hits a strider's leg, the max distance from the head at which it sticks anyway." ); +ConVar striderbuster_debugseek( "striderbuster_debugseek", "0" ); + +ConVar sk_striderbuster_magnet_multiplier( "sk_striderbuster_magnet_multiplier", "2.25" ); + +ConVar striderbuster_die_detach( "striderbuster_die_detach", "1" ); // Drop off the strider if a hunter shoots me. (Instead of exploding) +ConVar striderbuster_dive_force( "striderbuster_dive_force", "-200" ); // How much force to apply to a nosediving (dead in the air) striderbuster + +ConVar striderbuster_use_particle_flare( "striderbuster_use_particle_flare", "1" ); + +#define STRIDERBUSTER_FLAG_KNOCKED_OFF_STRIDER 0x00000001 // We were knocked off of a strider after the player attached me. + +#define SF_DONT_WEAPON_MANAGE 0x800000 + +#define STRIDERBUSTER_SPRITE_TRAIL "sprites/bluelaser1.vmt" + +string_t g_iszVehicle; + +#define BUSTER_PING_SOUND_FREQ 3.0f // How often (seconds) to issue the ping sound to remind players we are attached + +static const char *s_pBusterPingThinkContext = "BusterPing"; + +class CWeaponStriderBuster : public CPhysicsProp +{ + DECLARE_CLASS( CWeaponStriderBuster, CPhysicsProp ); + DECLARE_DATADESC(); + +public: + CWeaponStriderBuster( void ); + + virtual void Precache( void ); + virtual void Spawn( void ); + virtual void Activate( void ); + + // Treat as a live target so hunters can attack us + virtual bool IsAlive() { return true; } + + virtual void OnRestore( void ); + virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); + virtual void UpdateOnRemove( void ); + virtual int OnTakeDamage( const CTakeDamageInfo &info ); + virtual bool ShouldPuntUseLaunchForces( PhysGunForce_t reason ) { return ( reason == PHYSGUN_FORCE_LAUNCHED ); } + virtual QAngle PreferredCarryAngles( void ) { return m_CarryAngles; } + virtual bool HasPreferredCarryAnglesForPlayer( CBasePlayer *pPlayer ) { return true; } + + virtual void OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ); + virtual void OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ); + virtual Vector PhysGunLaunchVelocity( const Vector &forward, float flMass ); + virtual float GetAutoAimRadius( void ) { return striderbuster_autoaim_radius.GetFloat(); } + virtual void BusterTouch( CBaseEntity *pOther ); + + virtual bool ShouldAttractAutoAim( CBaseEntity *pAimingEnt ) { return IsAttachedToStrider(); } + + void InputConstraintBroken( inputdata_t &inputdata ); + void BusterFlyThink(); + void BusterDetachThink(); + void BusterPingThink(); + + void OnAddToCargoHold(); + void OnFlechetteAttach( Vector &vecForceDir ); + int NumFlechettesAttached() { return m_nAttachedFlechettes; } + + float GetPickupTime() { return m_PickupTime; } + + int GetStriderBusterFlags() { return m_iBusterFlags; } // I added a flags field so we don't have to keep added bools for all of these contingencies (sjb) + +private: + + void Launch( CBasePlayer *pPhysGunUser ); + void Detonate( void ); + void Shatter( CBaseEntity *pAttacker ); + bool StickToEntity( CBaseEntity *pOther ); + bool CreateConstraintToObject( CBaseEntity *pObject ); + void DestroyConstraint( void ); + bool ShouldStickToEntity( CBaseEntity *pEntity ); + void CreateDestroyedEffect( void ); + + inline bool IsAttachedToStrider( void ) const; + + bool m_bDud; + bool m_bLaunched; + bool m_bNoseDiving; // No magnetism, nosedive and break. Hunter flechettes set this. + int m_nAttachedFlechettes; + float m_flCollisionSpeedSqr; + int m_nAttachedBoneFollowerIndex; + float m_PickupTime; + + IPhysicsConstraint *m_pConstraint; + EHANDLE m_hConstrainedEntity; + + CHandle<CSprite> m_hGlowSprite; + CHandle<CSprite> m_hMainGlow; + + //CHandle<CParticleSystem> m_hGlowTrail; + EHANDLE m_hParticleEffect; + + int m_nRingTexture; + + QAngle m_CarryAngles; + + int m_iBusterFlags; + + COutputEvent m_OnAttachToStrider; + COutputEvent m_OnDetonate; + COutputEvent m_OnShatter; + COutputEvent m_OnShotDown; + +friend bool StriderBuster_IsAttachedStriderBuster( CBaseEntity *pEntity, CBaseEntity * ); + +}; + +LINK_ENTITY_TO_CLASS( prop_stickybomb, CWeaponStriderBuster ); +LINK_ENTITY_TO_CLASS( weapon_striderbuster, CWeaponStriderBuster ); + +BEGIN_DATADESC( CWeaponStriderBuster ) + DEFINE_KEYFIELD( m_bDud, FIELD_BOOLEAN, "dud" ), + + DEFINE_FIELD( m_bLaunched, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bNoseDiving, FIELD_BOOLEAN ), + DEFINE_FIELD( m_nAttachedFlechettes, FIELD_INTEGER ), + DEFINE_FIELD( m_flCollisionSpeedSqr, FIELD_FLOAT ), + DEFINE_FIELD( m_hConstrainedEntity, FIELD_EHANDLE ), + DEFINE_FIELD( m_hGlowSprite, FIELD_EHANDLE ), + DEFINE_FIELD( m_hMainGlow, FIELD_EHANDLE ), + //DEFINE_FIELD( m_hGlowTrail, FIELD_EHANDLE ), + + DEFINE_FIELD( m_nRingTexture, FIELD_INTEGER ), + DEFINE_FIELD( m_nAttachedBoneFollowerIndex, FIELD_INTEGER ), + + DEFINE_FIELD( m_PickupTime, FIELD_TIME ), + + DEFINE_FIELD( m_hParticleEffect, FIELD_EHANDLE ), + + DEFINE_FIELD( m_CarryAngles, FIELD_VECTOR ), + + DEFINE_FIELD( m_iBusterFlags, FIELD_INTEGER ), + DEFINE_PHYSPTR( m_pConstraint ), + + DEFINE_INPUTFUNC( FIELD_VOID, "ConstraintBroken", InputConstraintBroken ), + + DEFINE_OUTPUT( m_OnAttachToStrider, "OnAttachToStrider" ), + DEFINE_OUTPUT( m_OnDetonate, "OnDetonate" ), + DEFINE_OUTPUT( m_OnShatter, "OnShatter" ), + DEFINE_OUTPUT( m_OnShotDown, "OnShotDown" ), + + DEFINE_ENTITYFUNC( BusterTouch ), + DEFINE_THINKFUNC( BusterFlyThink ), + DEFINE_THINKFUNC( BusterDetachThink ), + DEFINE_THINKFUNC( BusterPingThink ), +END_DATADESC() + +CWeaponStriderBuster::CWeaponStriderBuster( void ) : + m_pConstraint( NULL ), + m_flCollisionSpeedSqr( -1.0f ), + m_hConstrainedEntity( NULL ), + m_nAttachedBoneFollowerIndex( -1 ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::Precache( void ) +{ + PrecacheScriptSound( "Weapon_StriderBuster.StickToEntity" ); + PrecacheScriptSound( "Weapon_StriderBuster.Detonate" ); + PrecacheScriptSound( "Weapon_StriderBuster.Dud_Detonate" ); + PrecacheScriptSound( "Weapon_StriderBuster.Ping" ); + + PrecacheModel("sprites/orangeflare1.vmt"); + + UTIL_PrecacheOther( "env_citadel_energy_core" ); + UTIL_PrecacheOther( "sparktrail" ); + + m_nRingTexture = PrecacheModel( "sprites/lgtning.vmt" ); + + PrecacheParticleSystem( "striderbuster_attach" ); + PrecacheParticleSystem( "striderbuster_attached_pulse" ); + PrecacheParticleSystem( "striderbuster_explode_core" ); + PrecacheParticleSystem( "striderbuster_explode_dummy_core" ); + PrecacheParticleSystem( "striderbuster_break_flechette" ); + PrecacheParticleSystem( "striderbuster_trail" ); + PrecacheParticleSystem( "striderbuster_shotdown_trail" ); + PrecacheParticleSystem( "striderbuster_break" ); + PrecacheParticleSystem( "striderbuster_flechette_attached" ); + + SetModelName( AllocPooledString("models/magnusson_device.mdl") ); + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::Spawn( void ) +{ + SetModelName( AllocPooledString("models/magnusson_device.mdl") ); + BaseClass::Spawn(); + + // Setup for being shot by the player + m_takedamage = DAMAGE_EVENTS_ONLY; + + // Ignore touches until launched. + SetTouch ( NULL ); + + AddFlag( FL_AIMTARGET|FL_OBJECT ); + + m_hParticleEffect = CreateEntityByName( "info_particle_system" ); + if ( m_hParticleEffect ) + { + m_hParticleEffect->KeyValue( "start_active", "1" ); + m_hParticleEffect->KeyValue( "effect_name", "striderbuster_smoke" ); + DispatchSpawn( m_hParticleEffect ); + if ( gpGlobals->curtime > 0.2f ) + { + m_hParticleEffect->Activate(); + } + m_hParticleEffect->SetAbsOrigin( GetAbsOrigin() ); + m_hParticleEffect->SetParent( this ); + } + + SetHealth( striderbuster_health.GetFloat() ); + + SetNextThink(gpGlobals->curtime + 0.01f); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::Activate( void ) +{ + g_iszVehicle = AllocPooledString( "prop_vehicle_jeep" ); + BaseClass::Activate(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::OnRestore( void ) +{ + BaseClass::OnRestore(); + + // If we have an entity we're attached to, attempt to reconstruct our bone follower setup + if ( m_hConstrainedEntity != NULL ) + { + CNPC_Strider *pStrider = dynamic_cast<CNPC_Strider *>(m_hConstrainedEntity.Get()); + if ( pStrider != NULL ) + { + // Make sure we've done this step or we'll have no controller to attach to + pStrider->InitBoneFollowers(); + + // Attempt to make a connection to the same bone follower we attached to previously + CBoneFollower *pBoneFollower = pStrider->GetBoneFollowerByIndex( m_nAttachedBoneFollowerIndex ); + if ( CreateConstraintToObject( pBoneFollower ) == false ) + { + Msg( "Failed to reattach to bone follower %d\n", m_nAttachedBoneFollowerIndex ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::DestroyConstraint( void ) +{ + // Destroy the constraint + if ( m_pConstraint != NULL ) + { + physenv->DestroyConstraint( m_pConstraint ); + m_pConstraint = NULL; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Create a constraint between this object and another +// Input : *pObject - Object to constrain ourselves to +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CWeaponStriderBuster::CreateConstraintToObject( CBaseEntity *pObject ) +{ + if ( m_pConstraint != NULL ) + { + // Should we destroy the constraint and make a new one at this point? + Assert( 0 ); + return false; + } + + if ( pObject == NULL ) + return false; + + IPhysicsObject *pPhysObject = pObject->VPhysicsGetObject(); + if ( pPhysObject == NULL ) + return false; + + IPhysicsObject *pMyPhysObject = VPhysicsGetObject(); + if ( pPhysObject == NULL ) + return false; + + // Create the fixed constraint + constraint_fixedparams_t fixedConstraint; + fixedConstraint.Defaults(); + fixedConstraint.InitWithCurrentObjectState( pPhysObject, pMyPhysObject ); + + IPhysicsConstraint *pConstraint = physenv->CreateFixedConstraint( pPhysObject, pMyPhysObject, NULL, fixedConstraint ); + if ( pConstraint == NULL ) + return false; + + // Hold on to us + m_pConstraint = pConstraint; + pConstraint->SetGameData( (void *)this ); + m_hConstrainedEntity = pObject->GetOwnerEntity();; + + // Disable collisions between the two ents + PhysDisableObjectCollisions( pPhysObject, pMyPhysObject ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Physics system has just told us our constraint has been broken +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::InputConstraintBroken( inputdata_t &inputdata ) +{ + // Shatter with no real explosion effect + Shatter( NULL ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::UpdateOnRemove( void ) +{ + DestroyConstraint(); + + if ( m_hGlowSprite != NULL ) + { + m_hGlowSprite->FadeAndDie( 0.5f ); + m_hGlowSprite = NULL; + } + + if ( m_hParticleEffect ) + { + UTIL_Remove( m_hParticleEffect ); + } + + BaseClass::UpdateOnRemove(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEntity - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CWeaponStriderBuster::ShouldStickToEntity( CBaseEntity *pEntity ) +{ + if ( pEntity == NULL ) + return false; + + // Must have a follow parent + CBaseEntity *pFollowParent = pEntity->GetOwnerEntity(); + if ( pFollowParent == NULL ) + return false; + + // Must be a strider + CNPC_Strider *pStrider = dynamic_cast<CNPC_Strider *>(pFollowParent); + if ( pStrider == NULL ) + return false; + + if( m_bNoseDiving ) + return false; + + // Don't attach to legs + CBoneFollower *pFollower = static_cast<CBoneFollower *>(pEntity); + if ( pStrider->IsLegBoneFollower( pFollower ) ) + { + Vector vecDelta = pStrider->GetAdjustedOrigin() - GetAbsOrigin(); + if ( vecDelta.Length() > striderbuster_leg_stick_dist.GetFloat() ) + { + return false; + } + } + + // Ick, this is kind of ugly, but it's also ugly having to pass pointer into this to avoid multiple castings! + // Save this to patch up save/restore later + m_nAttachedBoneFollowerIndex = pStrider->GetBoneFollowerIndex( pFollower ); + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Stick to an entity (using hierarchy if we can) +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CWeaponStriderBuster::StickToEntity( CBaseEntity *pOther ) +{ + // Make sure the object is travelling fast enough to stick + if ( m_flCollisionSpeedSqr > 50 && !m_bNoseDiving ) + { + // See if this is a valid strider bit + if ( ShouldStickToEntity( pOther ) ) + { + // Attempt to constraint to it + if ( CreateConstraintToObject( pOther ) ) + { + // Only works for striders, at the moment + CBaseEntity *pFollowParent = pOther->GetOwnerEntity(); + if ( pFollowParent == NULL ) + return false; + + // Allows us to identify our constrained object later + SetOwnerEntity( pFollowParent ); + + // Make a sound + EmitSound( "Weapon_StriderBuster.StickToEntity" ); + + DispatchParticleEffect( "striderbuster_attach", GetAbsOrigin(), GetAbsAngles(), NULL ); + + if( striderbuster_use_particle_flare.GetBool() ) + { + // We don't have to save any pointers or handles to this because it's parented to the buster. + // So it will die when the buster dies. Yay. + CParticleSystem *pFlare = (CParticleSystem *) CreateEntityByName( "info_particle_system" ); + + if ( pFlare != NULL ) + { + pFlare->KeyValue( "start_active", "1" ); + pFlare->KeyValue( "effect_name", "striderbuster_attached_pulse" ); + pFlare->SetParent( this ); + pFlare->SetLocalOrigin( vec3_origin ); + DispatchSpawn( pFlare ); + pFlare->Activate(); + } + } + else + { + // Create a glow sprite + m_hGlowSprite = CSprite::SpriteCreate( "sprites/orangeflare1.vmt", GetLocalOrigin(), false ); + + Assert( m_hGlowSprite ); + if ( m_hGlowSprite != NULL ) + { + m_hGlowSprite->TurnOn(); + m_hGlowSprite->SetTransparency( kRenderWorldGlow, 255, 255, 255, 255, kRenderFxNoDissipation ); + m_hGlowSprite->SetAbsOrigin( GetAbsOrigin() ); + m_hGlowSprite->SetScale( 5.0f ); + m_hGlowSprite->m_nRenderFX = kRenderFxStrobeFaster; + m_hGlowSprite->SetGlowProxySize( 16.0f ); + m_hGlowSprite->SetParent( this ); + } + } + + // Stop touching things + SetTouch( NULL ); + + // Must be a strider + CNPC_Strider *pStrider = dynamic_cast<CNPC_Strider *>(pFollowParent); + if ( pStrider == NULL ) + return false; + + // Notify the strider we're attaching to him + pStrider->StriderBusterAttached( this ); + + m_OnAttachToStrider.FireOutput( this, this ); + + // Start the ping sound. + SetContextThink( &CWeaponStriderBuster::BusterPingThink, gpGlobals->curtime + BUSTER_PING_SOUND_FREQ, s_pBusterPingThinkContext ); + + // Don't autodelete this one! + WeaponManager_RemoveManaged( this ); + + return true; + } + + return false; + } + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Create the explosion effect for the final big boom +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::CreateDestroyedEffect( void ) +{ + CBaseEntity *pTrail; + + StopParticleEffects( this ); + + for ( int i = 0; i < 3; i++ ) + { + pTrail = CreateEntityByName( "sparktrail" ); + pTrail->SetOwnerEntity( this ); + DispatchSpawn( pTrail ); + } + + DispatchParticleEffect( "striderbuster_explode_core", GetAbsOrigin(), GetAbsAngles() ); + + // Create liquid fountain gushtacular effect here! + CEffectData data; + + int nNumSteps = 6; + float flRadStep = (2*M_PI) / nNumSteps; + for ( int i = 0; i < nNumSteps; i++ ) + { + data.m_vOrigin = GetAbsOrigin() + RandomVector( -32.0f, 32.0f ); + data.m_vNormal.x = cos( flRadStep*i ); + data.m_vNormal.y = sin( flRadStep*i ); + data.m_vNormal.z = 0.0f; + data.m_flScale = ( random->RandomInt( 0, 5 ) == 0 ) ? 1 : 2; + + DispatchEffect( "StriderBlood", data ); + } + + // More effects + UTIL_ScreenShake( GetAbsOrigin(), 20.0f, 150.0, 1.0, 1250.0f, SHAKE_START ); + + data.m_vOrigin = GetAbsOrigin(); + DispatchEffect( "cball_explode", data ); +} + +//----------------------------------------------------------------------------- +// Purpose: Handle a collision using our special behavior +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) +{ + // Find out what we hit. + // Don't do anything special if we're already attached to a strider. + CBaseEntity *pVictim = pEvent->pEntities[!index]; + if ( pVictim == NULL || m_pConstraint != NULL ) + { + BaseClass::VPhysicsCollision( index, pEvent ); + return; + } + + // Don't attach if we're being held by the player + if ( VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) + { + BaseClass::VPhysicsCollision( index, pEvent ); + return; + } + + // Save off the speed of the object + m_flCollisionSpeedSqr = ( pEvent->preVelocity[ index ] ).LengthSqr(); + + // Break if we hit the world while going fast enough. + // Launched duds detonate if they hit the world at any speed. + if ( pVictim->IsWorld() && ( ( m_bDud && m_bLaunched ) || m_flCollisionSpeedSqr > Square( 500 ) ) ) + { + m_OnShatter.FireOutput( this, this ); + Shatter( pVictim ); + return; + } + + // We'll handle this later in our touch call + if ( ShouldStickToEntity( pVictim ) ) + return; + + // Determine if we should shatter + CBaseEntity *pOwnerEntity = pVictim->GetOwnerEntity(); + bool bVictimIsStrider = ( ( pOwnerEntity != NULL ) && FClassnameIs( pOwnerEntity, "npc_strider" ) ); + + // Break if we hit anything other than a strider while going fast enough. + // Launched duds detonate if they hit anything other than a strider any speed. + if ( ( bVictimIsStrider == false ) && ( ( m_bDud && m_bLaunched ) || m_flCollisionSpeedSqr > Square( 500 ) ) ) + { + m_OnShatter.FireOutput( this, this ); + Shatter( pVictim ); + return; + } + + // Just bounce + BaseClass::VPhysicsCollision( index, pEvent ); +} + +//----------------------------------------------------------------------------- +// Purpose: Called to see if we should attach to the victim +// Input : *pOther - the victim +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::BusterTouch( CBaseEntity *pOther ) +{ + // Attempt to stick to the entity + StickToEntity( pOther ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +inline bool CWeaponStriderBuster::IsAttachedToStrider( void ) const +{ + CBaseEntity *pAttachedEnt = GetOwnerEntity(); + if ( pAttachedEnt && FClassnameIs( pAttachedEnt, "npc_strider" ) ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::Detonate( void ) +{ + CBaseEntity *pVictim = GetOwnerEntity(); + if ( !m_bDud && pVictim ) + { + // Kill the strider (with magic effect) + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + CTakeDamageInfo info( pPlayer, this, RandomVector( -100.0f, 100.0f ), GetAbsOrigin(), pVictim->GetHealth(), DMG_GENERIC ); + pVictim->TakeDamage( info ); + + gamestats->Event_WeaponHit( ToBasePlayer( pPlayer ), true, GetClassname(), info ); + + // Tracker 62293: There's a bug where the inflictor/attacker are reversed when calling TakeDamage above so the player never gets + // credit for the strider buster kills. The code has a bunch of assumptions lower level, so it's safer to just fix it here by + // crediting a kill to the player directly. + gamestats->Event_PlayerKilledOther( pPlayer, pVictim, info ); + } + + m_OnDetonate.FireOutput( this, this ); + + // Explode + if ( !m_bDud ) + { + CreateDestroyedEffect(); + EmitSound( "Weapon_StriderBuster.Detonate" ); + } + else + { + DispatchParticleEffect( "striderbuster_explode_dummy_core", GetAbsOrigin(), GetAbsAngles() ); + EmitSound( "Weapon_StriderBuster.Dud_Detonate" ); + } + + // Go to bits! + Shatter( pVictim ); +} + +//----------------------------------------------------------------------------- +// Purpose: Intercept damage and decide whether or not we want to trigger +// Input : &info - +//----------------------------------------------------------------------------- +int CWeaponStriderBuster::OnTakeDamage( const CTakeDamageInfo &info ) +{ + // If we're attached, any damage from the player makes us trigger + CBaseEntity *pInflictor = info.GetInflictor(); + CBaseEntity *pAttacker = info.GetAttacker(); + bool bInflictorIsPlayer = ( pInflictor != NULL && pInflictor->IsPlayer() ); + bool bAttackerIsPlayer = ( pAttacker != NULL && pAttacker->IsPlayer() ); + + if ( GetParent() && GetParent()->ClassMatches( g_iszVehicle ) ) + { + return 0; + } + + // Only take damage from a player, for the moment + if ( striderbuster_allow_all_damage.GetBool() || ( IsAttachedToStrider() && ( bAttackerIsPlayer || bInflictorIsPlayer ) ) ) + { + Detonate(); + return 0; + } + + if ( pAttacker && ( pAttacker->Classify() == CLASS_COMBINE || pAttacker->Classify() == CLASS_COMBINE_HUNTER ) ) + { + if ( VPhysicsGetObject() && !VPhysicsGetObject()->IsMoveable() ) + { + return 0; + } + } + + // Hunters are able to destroy strider busters + if ( hunter_hate_held_striderbusters.GetBool() || hunter_hate_thrown_striderbusters.GetBool() || hunter_hate_attached_striderbusters.GetBool() ) + { + if ( ( GetHealth() > 0 ) && ( pInflictor != NULL ) && FClassnameIs( pInflictor, "hunter_flechette" ) ) + { + // + // Flechette impacts don't hurt the striderbuster unless it's attached to a strider, + // but the explosions always do. This is so that held or thrown striderbusters fly + // awry because of the flechette, but attached striderbusters break instantly to make + // the hunters more effective at defending the strider. + // + if ( IsAttachedToStrider() || !( info.GetDamageType() & DMG_NEVERGIB ) ) + { + if( striderbuster_die_detach.GetBool() && IsAttachedToStrider() ) + { + // Make the buster fall off and break. + m_takedamage = DAMAGE_NO; + + CNPC_Strider *pStrider = dynamic_cast<CNPC_Strider *>(GetOwnerEntity()); + Assert( pStrider != NULL ); + pStrider->StriderBusterDetached( this ); + DestroyConstraint(); + + // Amplify some lateral force. + Vector vecForce = info.GetDamageForce(); + vecForce.z = 0.0f; + VPhysicsGetObject()->ApplyForceCenter( vecForce * 5.0f ); + + SetContextThink( NULL, gpGlobals->curtime, s_pBusterPingThinkContext ); + + SetThink( &CWeaponStriderBuster::BusterDetachThink ); + SetNextThink( gpGlobals->curtime ); + m_iBusterFlags |= STRIDERBUSTER_FLAG_KNOCKED_OFF_STRIDER; + + return 0; + } + else + { + // Destroy the buster in place + // Make sure they know it blew up prematurely. + EmitSound( "Weapon_StriderBuster.Dud_Detonate" ); + DispatchParticleEffect( "striderbuster_break_flechette", GetAbsOrigin(), GetAbsAngles() ); + SetHealth( 0 ); + + Shatter( info.GetAttacker() ); + return 0; + } + } + + if ( info.GetDamage() < 5 ) + { + bool bFirst = ( m_CarryAngles.x == 45 && m_CarryAngles.y == 0 && m_CarryAngles.z == 0); + float sinTime = sin( gpGlobals->curtime ); + bool bSubtractX = ( bFirst ) ? ( sinTime < 0 ) : ( m_CarryAngles.x < 45 ); + + m_CarryAngles.x += ( 10.0 + 10.0 * fabsf( sinTime ) + random->RandomFloat( -2.5, 2.5 ) + random->RandomFloat( -2.5, 2.5 ) ) * ( ( bSubtractX ) ? -1.0 : 1.0 ); + m_CarryAngles.y = 15 * ( sin( gpGlobals->curtime ) + cos( gpGlobals->curtime * 0.5 ) ) * .5 + random->RandomFloat( -15, 15 ); + m_CarryAngles.z = 7.5 * ( sin( gpGlobals->curtime ) + sin( gpGlobals->curtime * 2.0 ) ) * .5 + random->RandomFloat( -7.5, 7.5 ); + } + + return 1; + } + } + + // Allow crushing damage + if ( info.GetDamageType() & DMG_CRUSH ) + return BaseClass::OnTakeDamage( info ); + + return 0; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + m_PickupTime = gpGlobals->curtime; + m_CarryAngles.Init( 45, 0, 0 ); + if ( ( reason == PICKED_UP_BY_CANNON ) && ( !HasSpawnFlags( SF_DONT_WEAPON_MANAGE ) ) ) + { + WeaponManager_RemoveManaged( this ); + } + else if ( reason == PUNTED_BY_CANNON ) + { + Launch( pPhysGunUser ); + } + + BaseClass::OnPhysGunPickup( pPhysGunUser, reason ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) +{ + if ( Reason == LAUNCHED_BY_CANNON ) + { + Launch( pPhysGunUser ); + } + else if ( ( Reason == DROPPED_BY_CANNON ) && ( !HasSpawnFlags( SF_DONT_WEAPON_MANAGE ) ) ) + { + // This striderbuster is now fair game for autodeletion. + WeaponManager_AddManaged( this ); + } + + BaseClass::OnPhysGunDrop( pPhysGunUser, Reason ); +} + + +//----------------------------------------------------------------------------- +// Fling the buster with the physcannon either via punt or launch. +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::Launch( CBasePlayer *pPhysGunUser ) +{ + if ( !HasSpawnFlags( SF_DONT_WEAPON_MANAGE ) ) + { + WeaponManager_RemoveManaged( this ); + } + + m_bLaunched = true; + + // Notify all nearby hunters that we were launched. + Hunter_StriderBusterLaunched( this ); + + // Start up the eye glow + m_hMainGlow = CSprite::SpriteCreate( "sprites/blueglow1.vmt", GetLocalOrigin(), false ); + + if ( m_hMainGlow != NULL ) + { + m_hMainGlow->FollowEntity( this ); + m_hMainGlow->SetTransparency( kRenderGlow, 255, 255, 255, 140, kRenderFxNoDissipation ); + m_hMainGlow->SetScale( 2.0f ); + m_hMainGlow->SetGlowProxySize( 8.0f ); + } + + if ( !m_bNoseDiving ) + { + DispatchParticleEffect( "striderbuster_trail", PATTACH_ABSORIGIN_FOLLOW, this ); + } + else + { + DispatchParticleEffect( "striderbuster_shotdown_trail", PATTACH_ABSORIGIN_FOLLOW, this ); + } + + // We get our touch function from the physics system + SetTouch ( &CWeaponStriderBuster::BusterTouch ); + + SetThink( &CWeaponStriderBuster::BusterFlyThink ); + SetNextThink( gpGlobals->curtime ); + + gamestats->Event_WeaponFired( pPhysGunUser, true, GetClassname() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &forward - +// flMass - +// Output : Vector +//----------------------------------------------------------------------------- +Vector CWeaponStriderBuster::PhysGunLaunchVelocity( const Vector &forward, float flMass ) +{ + return ( striderbuster_shot_velocity.GetFloat() * forward ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::Shatter( CBaseEntity *pAttacker ) +{ + if( m_bNoseDiving ) + m_OnShotDown.FireOutput( this, this ); + + m_takedamage = DAMAGE_YES; + + if( !IsAttachedToStrider() ) + { + // Don't display this particular effect if we're attached to a strider. This effect just gets lost + // in the big strider explosion anyway, so let's recover some perf. + DispatchParticleEffect( "striderbuster_break", GetAbsOrigin(), GetAbsAngles() ); + } + + // Buster is useless now. Stop thinking, touching. + SetThink( NULL ); + SetTouch( NULL ); + SetContextThink( NULL, gpGlobals->curtime, s_pBusterPingThinkContext ); + + // Deal deadly damage to ourselves (DMG_CRUSH is allowed, others are blocked) + CTakeDamageInfo info( pAttacker, pAttacker, RandomVector( -100, 100 ), GetAbsOrigin(), 100.0f, DMG_CRUSH ); + TakeDamage( info ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Give the buster a slight attraction to striders. +// Ported back from the magnade. +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::BusterFlyThink() +{ + if (IsAttachedToStrider()) + return; // early out. Think no more. + + // If we're nosediving, forget about magnetism. + if ( m_bNoseDiving ) + { + if ( VPhysicsGetObject() ) + VPhysicsGetObject()->ApplyForceCenter( Vector( 0, 0, striderbuster_dive_force.GetFloat() ) ); + SetNextThink(gpGlobals->curtime + 0.01f); + return; + } + + // seek? + const float magradius = 38.0 * sk_striderbuster_magnet_multiplier.GetFloat(); // radius of strider hull times multiplier + if (magradius > 0 && + GetMoveType() == MOVETYPE_VPHYSICS && + VPhysicsGetObject() + ) + { + // find the nearest enemy. + CBaseEntity *pList[16]; + Vector origin = GetAbsOrigin(); + + // do a find in box ( a little faster than sphere ) + int count; + { + Vector mins,maxs; + mins = origin; + mins -= magradius; + + maxs = origin; + maxs += magradius; + + count = UTIL_EntitiesInBox(pList, 16, mins, maxs, FL_NPC); + } + + float magradiusSq = Square( magradius ); + float nearestDistSq = magradiusSq + 1; + int bestFit = -1; + Vector toTarget; // will be garbage unless something good is found + CNPC_Strider *pBestStrider = NULL; + + for ( int ii = 0 ; ii < count ; ++ii ) + { + CNPC_Strider *pStrider = dynamic_cast<CNPC_Strider *>(pList[ii]); + if ( pStrider && !pStrider->CarriedByDropship() ) // ShouldStickToEntity() doesn't work because the strider NPC isn't what we glue to + { + // get distance squared + VectorSubtract( pStrider->GetAdjustedOrigin(), GetAbsOrigin(), toTarget ); + + //NDebugOverlay::Line( GetAbsOrigin(), GetAbsOrigin() + toTarget, 128, 0, 128, false, 0.1 ); + + float dSq = toTarget.LengthSqr(); + if (dSq < nearestDistSq) + { + bestFit = ii; nearestDistSq = dSq; + pBestStrider = pStrider; + } + } + } + + if (bestFit >= 0) // we found something and should attract towards it. (hysterisis later?) + { + if ( striderbuster_debugseek.GetBool() ) + { + NDebugOverlay::Circle( GetAbsOrigin() + toTarget, magradius, 255, 255, 255, 255, true, .1 ); + NDebugOverlay::Cross3D( GetAbsOrigin() + toTarget, magradius, 255, 255, 255, true, .1 ); + } + + // force magnitude. + float magnitude = GetMass() * striderbuster_magnetic_force_strider.GetFloat(); + int falloff = striderbuster_falloff_power.GetInt(); + switch (falloff) + { + case 1: + VPhysicsGetObject()->ApplyForceCenter( toTarget * (magnitude / nearestDistSq) ); // dividing through by distance squared normalizes toTarget and gives a linear falloff + break; + case 2: + VPhysicsGetObject()->ApplyForceCenter( toTarget * (magnitude / (nearestDistSq * sqrtf(nearestDistSq))) ); // dividing through by distance cubed normalizes toTarget and gives a quadratic falloff + break; + case 3: + VPhysicsGetObject()->ApplyForceCenter( toTarget * (magnitude / (nearestDistSq * nearestDistSq)) ); // dividing through by distance fourth normalizes toTarget and gives a cubic falloff + break; + case 4: + { + Vector toTarget; + pBestStrider->GetAttachment( "buster_target", toTarget ); + + if ( striderbuster_debugseek.GetBool() ) + { + NDebugOverlay::Cross3D( toTarget, magradius, 255, 0, 255, true, .1 ); + NDebugOverlay::Cross3D( toTarget, magradius, 255, 0, 255, true, .1 ); + } + + toTarget -= GetAbsOrigin(); + toTarget.NormalizeInPlace(); + VPhysicsGetObject()->ApplyForceCenter( toTarget * magnitude ); + + } + break; + default: // arbitrary powers + VPhysicsGetObject()->ApplyForceCenter( toTarget * (magnitude * powf(nearestDistSq,(falloff+1.0f)/2)) ); // square root for distance instead of squared, add one to normalize toTarget + break; + } + } + + SetNextThink(gpGlobals->curtime + 0.01f); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::BusterDetachThink() +{ + SetNextThink( gpGlobals->curtime + 0.1f ); + + trace_t tr; + UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() - Vector( 0, 0, 1200), MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); + + if( fabs(tr.startpos.z - tr.endpos.z) < 240.0f ) + { + SetThink(NULL); + EmitSound( "Weapon_StriderBuster.Dud_Detonate" ); + DispatchParticleEffect( "striderbuster_break_flechette", GetAbsOrigin(), GetAbsAngles() ); + SetHealth( 0 ); + CTakeDamageInfo info; + info.SetDamage( 1.0f ); + info.SetAttacker( this ); + info.SetInflictor( this ); + Shatter(this); + } +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::BusterPingThink() +{ + EmitSound( "Weapon_StriderBuster.Ping" ); + + SetContextThink( &CWeaponStriderBuster::BusterPingThink, gpGlobals->curtime + BUSTER_PING_SOUND_FREQ, s_pBusterPingThinkContext ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::OnAddToCargoHold() +{ + if ( !HasSpawnFlags( SF_DONT_WEAPON_MANAGE ) ) + { + WeaponManager_RemoveManaged( this ); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CWeaponStriderBuster::OnFlechetteAttach( Vector &vecFlechetteVelocity ) +{ + if ( m_bLaunched ) + { + Vector vecForce = vecFlechetteVelocity; + VectorNormalize( vecForce ); + + vecForce *= 1000; + vecForce.z = -5000; + + VPhysicsGetObject()->ApplyForceCenter( vecForce ); + } + + if ( !GetParent() || !GetParent()->ClassMatches( g_iszVehicle ) ) + { + if ( !m_bNoseDiving ) + { + //m_hGlowTrail->StopParticleSystem(); + StopParticleEffects( this ); + + if( m_iBusterFlags & STRIDERBUSTER_FLAG_KNOCKED_OFF_STRIDER ) + { + DispatchParticleEffect( "striderbuster_shotdown_trail", PATTACH_ABSORIGIN_FOLLOW, this ); + } + else + { + DispatchParticleEffect( "striderbuster_flechette_attached", PATTACH_ABSORIGIN_FOLLOW, this ); + } + } + + m_bNoseDiving = true; + } + m_nAttachedFlechettes++; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool StriderBuster_IsAttachedStriderBuster( CBaseEntity *pEntity, CBaseEntity *pAttachedTo ) +{ + Assert(dynamic_cast<CWeaponStriderBuster *>(pEntity)); + if ( !pAttachedTo ) + return static_cast<CWeaponStriderBuster *>(pEntity)->m_hConstrainedEntity != NULL; + else + return static_cast<CWeaponStriderBuster *>(pEntity)->m_hConstrainedEntity == pAttachedTo; +} + + +//----------------------------------------------------------------------------- +// Called when the striderbuster is placed in the jalopy's cargo container. +//----------------------------------------------------------------------------- +void StriderBuster_OnAddToCargoHold( CBaseEntity *pEntity ) +{ + CWeaponStriderBuster *pBuster = dynamic_cast <CWeaponStriderBuster *>( pEntity ); + if ( pBuster ) + { + pBuster->OnAddToCargoHold(); + } +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool StriderBuster_OnFlechetteAttach( CBaseEntity *pEntity, Vector &vecFlechetteVelocity ) +{ + CWeaponStriderBuster *pBuster = dynamic_cast <CWeaponStriderBuster *>( pEntity ); + if ( pBuster ) + { + pBuster->OnFlechetteAttach( vecFlechetteVelocity ); + return true; + } + return false; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int StriderBuster_NumFlechettesAttached( CBaseEntity *pEntity ) +{ + CWeaponStriderBuster *pBuster = dynamic_cast <CWeaponStriderBuster *>( pEntity ); + if ( pBuster ) + { + return pBuster->NumFlechettesAttached(); + } + return 0; +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +float StriderBuster_GetPickupTime( CBaseEntity *pEntity ) +{ + CWeaponStriderBuster *pBuster = dynamic_cast <CWeaponStriderBuster *>( pEntity ); + if ( pBuster ) + { + return pBuster->GetPickupTime(); + } + return 0; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +bool StriderBuster_WasKnockedOffStrider( CBaseEntity *pEntity ) +{ + CWeaponStriderBuster *pBuster = dynamic_cast <CWeaponStriderBuster *>( pEntity ); + if ( pBuster ) + { + return ((pBuster->GetStriderBusterFlags() & STRIDERBUSTER_FLAG_KNOCKED_OFF_STRIDER) != 0); + } + + return false; +} diff --git a/game/server/episodic/weapon_striderbuster.h b/game/server/episodic/weapon_striderbuster.h new file mode 100644 index 0000000..fe67034 --- /dev/null +++ b/game/server/episodic/weapon_striderbuster.h @@ -0,0 +1,20 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Helper functions for the striderbuster weapon. +// +//============================================================================= + +#ifndef WEAPON_STRIDERBUSTER_H +#define WEAPON_STRIDERBUSTER_H +#ifdef _WIN32 +#pragma once +#endif + +bool StriderBuster_IsAttachedStriderBuster( CBaseEntity *pEntity, CBaseEntity *pAttachedTo = NULL ); +void StriderBuster_OnAddToCargoHold( CBaseEntity *pEntity ); +bool StriderBuster_OnFlechetteAttach( CBaseEntity *pEntity, Vector &vecForceDir ); +int StriderBuster_NumFlechettesAttached( CBaseEntity *pEntity ); +float StriderBuster_GetPickupTime( CBaseEntity *pEntity ); +bool StriderBuster_WasKnockedOffStrider( CBaseEntity *pEntity ); + +#endif // WEAPON_STRIDERBUSTER_H |