//===== Copyright (c) 1996-2005, Valve Corporation, All rights reserved. ======// // // Purpose: // //===========================================================================// #include "cbase.h" #include "ai_network.h" #include "ai_default.h" #include "ai_schedule.h" #include "ai_hull.h" #include "ai_node.h" #include "ai_task.h" #include "entitylist.h" #include "basecombatweapon.h" #include "soundenvelope.h" #include "gib.h" #include "gamerules.h" #include "ammodef.h" #include "grenade_homer.h" #include "cbasehelicopter.h" #include "engine/IEngineSound.h" #include "IEffects.h" #include "globals.h" #include "explode.h" #include "movevars_shared.h" #include "smoke_trail.h" #include "ar2_explosion.h" #include "collisionutils.h" #include "props.h" #include "EntityFlame.h" #include "decals.h" #include "effect_dispatch_data.h" #include "te_effect_dispatch.h" #include "ai_spotlight.h" #include "vphysics/constraints.h" #include "physics_saverestore.h" #include "ai_memory.h" #include "npc_attackchopper.h" #ifdef HL2_EPISODIC #include "physics_bone_follower.h" #endif // HL2_EPISODIC // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" // ------------------------------------- // Bone controllers // ------------------------------------- #define CHOPPER_DRONE_NAME "models/combine_helicopter/helicopter_bomb01.mdl" #define CHOPPER_MODEL_NAME "models/combine_helicopter.mdl" #define CHOPPER_MODEL_CORPSE_NAME "models/combine_helicopter_broken.mdl" #define CHOPPER_RED_LIGHT_SPRITE "sprites/redglow1.vmt" #define CHOPPER_MAX_SMALL_CHUNKS 1 #define CHOPPER_MAX_CHUNKS 3 static const char *s_pChunkModelName[CHOPPER_MAX_CHUNKS] = { "models/gibs/helicopter_brokenpiece_01.mdl", "models/gibs/helicopter_brokenpiece_02.mdl", "models/gibs/helicopter_brokenpiece_03.mdl", }; #define BOMB_SKIN_LIGHT_ON 1 #define BOMB_SKIN_LIGHT_OFF 0 #define HELICOPTER_CHUNK_COCKPIT "models/gibs/helicopter_brokenpiece_04_cockpit.mdl" #define HELICOPTER_CHUNK_TAIL "models/gibs/helicopter_brokenpiece_05_tailfan.mdl" #define HELICOPTER_CHUNK_BODY "models/gibs/helicopter_brokenpiece_06_body.mdl" #define CHOPPER_MAX_SPEED (60 * 17.6f) #define CHOPPER_MAX_FIRING_SPEED 250.0f #define CHOPPER_MAX_GUN_DIST 2000.0f #define CHOPPER_ACCEL_RATE 500 #define CHOPPER_ACCEL_RATE_BOOST 1500 #define DEFAULT_FREE_KNOWLEDGE_DURATION 5.0f // ------------------------------------- // Pathing data #define CHOPPER_LEAD_DISTANCE 800.0f #define CHOPPER_MIN_CHASE_DIST_DIFF 128.0f // Distance threshold used to determine when a target has moved enough to update our navigation to it #define CHOPPER_MIN_AGGRESSIVE_CHASE_DIST_DIFF 64.0f #define CHOPPER_AVOID_DIST 512.0f #define CHOPPER_ARRIVE_DIST 128.0f #define CHOPPER_MAX_CIRCLE_OF_DEATH_FOLLOW_SPEED 450.0f #define CHOPPER_MIN_CIRCLE_OF_DEATH_RADIUS 150.0f #define CHOPPER_MAX_CIRCLE_OF_DEATH_RADIUS 350.0f #define CHOPPER_BOMB_DROP_COUNT 6 // Bullrush #define CHOPPER_BULLRUSH_MODE_DISTANCE g_helicopter_bullrush_distance.GetFloat() #define CHOPPER_BULLRUSH_ENEMY_BOMB_DISTANCE g_helicopter_bullrush_bomb_enemy_distance.GetFloat() #define CHOPPER_BULLRUSH_ENEMY_BOMB_TIME g_helicopter_bullrush_bomb_time.GetFloat() #define CHOPPER_BULLRUSH_ENEMY_BOMB_SPEED g_helicopter_bullrush_bomb_speed.GetFloat() #define CHOPPER_BULLRUSH_SHOOTING_VERTICAL_OFFSET g_helicopter_bullrush_shoot_height.GetFloat() #define CHOPPER_GUN_CHARGE_TIME g_helicopter_chargetime.GetFloat() #define CHOPPER_GUN_IDLE_TIME g_helicopter_idletime.GetFloat() #define CHOPPER_GUN_MAX_FIRING_DIST g_helicopter_maxfiringdist.GetFloat() #define BULLRUSH_IDLE_PLAYER_FIRE_TIME 6.0f #define DRONE_SPEED sk_helicopter_drone_speed.GetFloat() #define SF_HELICOPTER_LOUD_ROTOR_SOUND 0x00010000 #define SF_HELICOPTER_ELECTRICAL_DRONE 0x00020000 #define SF_HELICOPTER_LIGHTS 0x00040000 #define SF_HELICOPTER_IGNORE_AVOID_FORCES 0x00080000 #define SF_HELICOPTER_AGGRESSIVE 0x00100000 #define SF_HELICOPTER_LONG_SHADOW 0x00200000 #define CHOPPER_SLOW_BOMB_SPEED 250 #define CHOPPER_BULLRUSH_SLOW_SHOOT_SPEED 250 #define CHOPPER_BULLRUSH_SLOW_SHOOT_SPEED_SQ (CHOPPER_BULLRUSH_SLOW_SHOOT_SPEED * CHOPPER_BULLRUSH_SLOW_SHOOT_SPEED) #define CHOPPER_BULLRUSH_SLOW_SHOOT_SPEED_2 450 #define CHOPPER_BULLRUSH_SLOW_SHOOT_SPEED_2_SQ (CHOPPER_BULLRUSH_SLOW_SHOOT_SPEED_2 * CHOPPER_BULLRUSH_SLOW_SHOOT_SPEED_2) // CVars ConVar sk_helicopter_health( "sk_helicopter_health","5600"); ConVar sk_helicopter_firingcone( "sk_helicopter_firingcone","20.0", 0, "The angle in degrees of the cone in which the shots will be fired" ); ConVar sk_helicopter_burstcount( "sk_helicopter_burstcount","12", 0, "How many shot bursts to fire after charging up. The bigger the number, the longer the firing is" ); ConVar sk_helicopter_roundsperburst( "sk_helicopter_roundsperburst","5", 0, "How many shots to fire in a single burst" ); ConVar sk_helicopter_grenadedamage( "sk_helicopter_grenadedamage","25.0", 0, "The amount of damage the helicopter grenade deals." ); ConVar sk_helicopter_grenaderadius( "sk_helicopter_grenaderadius","275.0", 0, "The damage radius of the helicopter grenade." ); ConVar sk_helicopter_grenadeforce( "sk_helicopter_grenadeforce","55000.0", 0, "The physics force that the helicopter grenade exerts." ); ConVar sk_helicopter_grenade_puntscale( "sk_helicopter_grenade_puntscale","1.5", 0, "When physpunting a chopper's grenade, scale its velocity by this much." ); // Number of bomb hits it takes to kill a chopper on each skill level. ConVar sk_helicopter_num_bombs1("sk_helicopter_num_bombs1", "3"); ConVar sk_helicopter_num_bombs2("sk_helicopter_num_bombs2", "5"); ConVar sk_helicopter_num_bombs3("sk_helicopter_num_bombs3", "5"); ConVar sk_npc_dmg_helicopter_to_plr( "sk_npc_dmg_helicopter_to_plr","3", 0, "Damage helicopter shots deal to the player" ); ConVar sk_npc_dmg_helicopter( "sk_npc_dmg_helicopter","6", 0, "Damage helicopter shots deal to everything but the player" ); ConVar sk_helicopter_drone_speed( "sk_helicopter_drone_speed","450.0", 0, "How fast does the zapper drone move?" ); ConVar g_helicopter_chargetime( "g_helicopter_chargetime","2.0", 0, "How much time we have to wait (on average) between the time we start hearing the charging sound + the chopper fires" ); ConVar g_helicopter_bullrush_distance("g_helicopter_bullrush_distance", "5000"); ConVar g_helicopter_bullrush_bomb_enemy_distance("g_helicopter_bullrush_bomb_enemy_distance", "0"); ConVar g_helicopter_bullrush_bomb_time("g_helicopter_bullrush_bomb_time", "10"); ConVar g_helicopter_idletime( "g_helicopter_idletime","3.0", 0, "How much time we have to wait (on average) after we fire before we can charge up again" ); ConVar g_helicopter_maxfiringdist( "g_helicopter_maxfiringdist","2500.0", 0, "The maximum distance the player can be from the chopper before it stops firing" ); ConVar g_helicopter_bullrush_bomb_speed( "g_helicopter_bullrush_bomb_speed","850.0", 0, "The maximum distance the player can be from the chopper before it stops firing" ); ConVar g_helicopter_bullrush_shoot_height( "g_helicopter_bullrush_shoot_height","650.0", 0, "The maximum distance the player can be from the chopper before it stops firing" ); ConVar g_helicopter_bullrush_mega_bomb_health( "g_helicopter_bullrush_mega_bomb_health","0.25", 0, "Fraction of the health of the chopper before it mega-bombs" ); ConVar g_helicopter_bomb_danger_radius( "g_helicopter_bomb_danger_radius", "120" ); Activity ACT_HELICOPTER_DROP_BOMB; Activity ACT_HELICOPTER_CRASHING; static const char *s_pBlinkLightThinkContext = "BlinkLights"; static const char *s_pSpotlightThinkContext = "SpotlightThink"; static const char *s_pRampSoundContext = "RampSound"; static const char *s_pWarningBlinkerContext = "WarningBlinker"; static const char *s_pAnimateThinkContext = "Animate"; #define CHOPPER_LIGHT_BLINK_TIME 1.0f #define CHOPPER_LIGHT_BLINK_TIME_SHORT 0.1f #define BOMB_LIFETIME 2.5f // Don't access this directly. Call GetBombLifetime(); #define BOMB_RAMP_SOUND_TIME 1.0f enum { MAX_HELICOPTER_LIGHTS = 3, }; enum { SF_GRENADE_HELICOPTER_MEGABOMB = 0x1, }; #define GRENADE_HELICOPTER_MODEL "models/combine_helicopter/helicopter_bomb01.mdl" LINK_ENTITY_TO_CLASS( info_target_helicopter_crash, CPointEntity ); //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ static inline float ClampSplineRemapVal( float flValue, float flMinValue, float flMaxValue, float flOutMin, float flOutMax ) { Assert( flMinValue <= flMaxValue ); float flClampedVal = clamp( flValue, flMinValue, flMaxValue ); return SimpleSplineRemapVal( flClampedVal, flMinValue, flMaxValue, flOutMin, flOutMax ); } //----------------------------------------------------------------------------- // The bombs the attack helicopter drops //----------------------------------------------------------------------------- enum { SKIN_REGULAR, SKIN_DUD, }; class CGrenadeHelicopter : public CBaseGrenade { DECLARE_DATADESC(); public: DECLARE_CLASS( CGrenadeHelicopter, CBaseGrenade ); virtual void Precache( ); virtual void Spawn( ); virtual void UpdateOnRemove(); virtual void OnEntityEvent( EntityEvent_t event, void *pEventData ); virtual void PhysicsSimulate( void ); virtual float GetShakeAmplitude( void ) { return 25.0; } virtual float GetShakeRadius( void ) { return sk_helicopter_grenaderadius.GetFloat() * 2; } virtual int OnTakeDamage( const CTakeDamageInfo &info ); virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); void SetExplodeOnContact( bool bExplode ) { m_bExplodeOnContact = bExplode; } virtual QAngle PreferredCarryAngles( void ) { return QAngle( -12, 98, 55 ); } virtual bool HasPreferredCarryAnglesForPlayer( CBasePlayer *pPlayer ) { return true; } float GetBombLifetime(); #ifdef HL2_EPISODIC virtual void OnPhysGunPickup(CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ); virtual void OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t reason ); virtual Class_T Classify( void ) { return CLASS_MISSILE; } void SetCollisionObject( CBaseEntity *pEntity ) { m_hCollisionObject = pEntity; } void SendMissEvent(); bool IsThrownByPlayer(); virtual bool ShouldPuntUseLaunchForces( PhysGunForce_t reason ) { return ( reason == PHYSGUN_FORCE_LAUNCHED ); } virtual Vector PhysGunLaunchVelocity( const Vector &forward, float flMass ); void InputExplodeIn( inputdata_t &inputdata ); #endif // HL2_EPISODIC private: // Pow! void DoExplosion( const Vector &vecOrigin, const Vector &vecVelocity ); void ExplodeThink(); void RampSoundThink(); void WarningBlinkerThink(); void StopWarningBlinker(); void AnimateThink(); void ExplodeConcussion( CBaseEntity *pOther ); void BecomeActive(); void ResolveFlyCollisionCustom( trace_t &trace, Vector &vecVelocity ); bool m_bActivated; bool m_bExplodeOnContact; CSoundPatch *m_pWarnSound; EHANDLE m_hWarningSprite; bool m_bBlinkerAtTop; #ifdef HL2_EPISODIC float m_flLifetime; EHANDLE m_hCollisionObject; // Pointer to object we re-enable collisions with when picked up bool m_bPickedUp; float m_flBlinkFastTime; COutputEvent m_OnPhysGunOnlyPickup; #endif // HL2_EPISODIC }; //----------------------------------------------------------------------------- // The bombs the attack helicopter drops //----------------------------------------------------------------------------- class CBombDropSensor : public CBaseEntity { DECLARE_DATADESC(); public: DECLARE_CLASS( CBombDropSensor, CBaseEntity ); void Spawn(); // Drop a bomb at a particular location void InputDropBomb( inputdata_t &inputdata ); void InputDropBombStraightDown( inputdata_t &inputdata ); void InputDropBombAtTarget( inputdata_t &inputdata ); void InputDropBombAtTargetAlways( inputdata_t &inputdata ); void InputDropBombDelay( inputdata_t &inputdata ); }; //----------------------------------------------------------------------------- // This entity is used to create boxes that the helicopter can't bomb in //----------------------------------------------------------------------------- class CBombSuppressor : public CBaseEntity { DECLARE_DATADESC(); public: DECLARE_CLASS( CBombSuppressor, CBaseEntity ); virtual void Spawn( ); virtual void Activate(); virtual void UpdateOnRemove(); static bool CanBomb( const Vector &vecPosition ); private: typedef CHandle BombSuppressorHandle_t; static CUtlVector< BombSuppressorHandle_t > s_BombSuppressors; }; enum { CHUNK_COCKPIT, CHUNK_BODY, CHUNK_TAIL }; //----------------------------------------------------------------------------- // This entity is used for helicopter gibs with specific properties //----------------------------------------------------------------------------- class CHelicopterChunk : public CBaseAnimating { DECLARE_DATADESC(); public: DECLARE_CLASS( CHelicopterChunk, CBaseAnimating ); virtual void Spawn( void ); virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ); static CHelicopterChunk *CreateHelicopterChunk( const Vector &vecPos, const QAngle &vecAngles, const Vector &vecVelocity, const char *pszModelName, int chunkID ); int m_nChunkID; CHandle m_hMaster; IPhysicsConstraint *m_pTailConstraint; IPhysicsConstraint *m_pCockpitConstraint; protected: void CollisionCallback( CHelicopterChunk *pCaller ); void FallThink( void ); bool m_bLanded; }; //----------------------------------------------------------------------------- // The attack helicopter //----------------------------------------------------------------------------- class CNPC_AttackHelicopter : public CBaseHelicopter { public: DECLARE_CLASS( CNPC_AttackHelicopter, CBaseHelicopter ); DECLARE_DATADESC(); DEFINE_CUSTOM_AI; CNPC_AttackHelicopter(); ~CNPC_AttackHelicopter(); virtual void Precache( void ); virtual void Spawn( void ); virtual void Activate( void ); virtual bool CreateComponents(); virtual int ObjectCaps(); #ifdef HL2_EPISODIC virtual bool CreateVPhysics( void ); #endif // HL2_EPISODIC virtual void UpdateOnRemove(); virtual void StopLoopingSounds(); int BloodColor( void ) { return DONT_BLEED; } Class_T Classify ( void ) { return CLASS_COMBINE_GUNSHIP; } virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); virtual void TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr ); virtual int OnTakeDamage( const CTakeDamageInfo &info ); // Shot spread virtual Vector GetAttackSpread( CBaseCombatWeapon *pWeapon, CBaseEntity *pTarget ); // More Enemy visibility check virtual bool FVisible( CBaseEntity *pEntity, int traceMask = MASK_BLOCKLOS, CBaseEntity **ppBlocker = NULL ); // Think! virtual void PrescheduleThink( void ); // Purpose: Set the gunship's paddles flailing! virtual void Event_Killed( const CTakeDamageInfo &info ); // Drop a bomb at a particular location void InputDropBomb( inputdata_t &inputdata ); void InputDropBombStraightDown( inputdata_t &inputdata ); void InputDropBombAtTarget( inputdata_t &inputdata ); void InputDropBombAtTargetAlways( inputdata_t &inputdata ); void InputDropBombAtTargetInternal( inputdata_t &inputdata, bool bCheckFairness ); void InputDropBombDelay( inputdata_t &inputdata ); void InputStartCarpetBombing( inputdata_t &inputdata ); void InputStopCarpetBombing( inputdata_t &inputdata ); virtual void SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ); virtual const char *GetTracerType( void ); virtual void DoImpactEffect( trace_t &tr, int nDamageType ); virtual void DoMuzzleFlash( void ); // FIXME: Work this back into the base class virtual bool ShouldUseFixedPatrolLogic() { return true; } protected: int m_poseWeapon_Pitch, m_poseWeapon_Yaw, m_poseRudder; virtual void PopulatePoseParameters( void ); private: enum GunState_t { GUN_STATE_IDLE = 0, GUN_STATE_CHARGING, GUN_STATE_FIRING, }; // Gets the max speed of the helicopter virtual float GetMaxSpeed(); virtual float GetMaxSpeedFiring(); // Startup the chopper virtual void Startup(); void InitializeRotorSound( void ); // Weaponry bool FireGun( void ); // Movement: virtual void Flight( void ); // Changes the main thinking method of helicopters virtual void Hunt( void ); // For scripted times where it *has* to shoot void InputResetIdleTime( inputdata_t &inputdata ); void InputSetHealthFraction( inputdata_t &inputdata ); void InputStartBombExplodeOnContact( inputdata_t &inputdata ); void InputStopBombExplodeOnContact( inputdata_t &inputdata ); void InputEnableAlwaysTransition( inputdata_t &inputdata ); void InputDisableAlwaysTransition( inputdata_t &inputdata ); void InputOutsideTransition( inputdata_t &inputdata ); void InputSetOutsideTransitionTarget( inputdata_t &inputdata ); // Turns off the gun void InputGunOff( inputdata_t &inputdata ); // Vehicle attack modes void InputStartBombingVehicle( inputdata_t &inputdata ); void InputStartTrailingVehicle( inputdata_t &inputdata ); void InputStartDefaultBehavior( inputdata_t &inputdata ); void InputStartAlwaysLeadingVehicle( inputdata_t &inputdata ); // Deadly shooting, tex! void InputEnableDeadlyShooting( inputdata_t &inputdata ); void InputDisableDeadlyShooting( inputdata_t &inputdata ); void InputStartNormalShooting( inputdata_t &inputdata ); void InputStartLongCycleShooting( inputdata_t &inputdata ); void InputStartContinuousShooting( inputdata_t &inputdata ); void InputStartFastShooting( inputdata_t &inputdata ); int GetShootingMode( ); bool IsDeadlyShooting(); // Bombing suppression void InputEnableBombing( inputdata_t &inputdata ); void InputDisableBombing( inputdata_t &inputdata ); // Visibility tests void InputDisablePathVisibilityTests( inputdata_t &inputdata ); void InputEnablePathVisibilityTests( inputdata_t &inputdata ); // Death, etc. void InputSelfDestruct( inputdata_t &inputdata ); // Enemy visibility check CBaseEntity *FindTrackBlocker( const Vector &vecViewPoint, const Vector &vecTargetPos ); // Special path navigation when we explicitly want to follow a path void UpdateFollowPathNavigation(); // Find interesting nearby things to shoot int BuildMissTargetList( int nCount, CBaseEntity **ppMissCandidates ); // Shoot when the player's your enemy : void ShootAtPlayer( const Vector &vBasePos, const Vector &vGunDir ); // Shoot when the player's your enemy + he's driving a vehicle void ShootAtVehicle( const Vector &vBasePos, const Vector &vGunDir ); // Shoot where we're facing void ShootAtFacingDirection( const Vector &vBasePos, const Vector &vGunDir, bool bFirstShotAccurate ); // Updates the facing direction void UpdateFacingDirection( const Vector &vecActualDesiredPosition ); // Various states of the helicopter firing... bool PoseGunTowardTargetDirection( const Vector &vTargetDir ); // Compute the position to fire at (vehicle + non-vehicle case) void ComputeFireAtPosition( Vector *pVecActualTargetPosition ); void ComputeVehicleFireAtPosition( Vector *pVecActualTargetPosition ); // Various states of the helicopter firing... bool DoGunIdle( const Vector &vecGunDir, const Vector &vTargetDir ); bool DoGunCharging( ); bool DoGunFiring( const Vector &vBasePos, const Vector &vGunDir, const Vector &vecFireAtPosition ); void FireElectricityGun( ); // Chooses a point within the circle of death to fire in void PickDirectionToCircleOfDeath( const Vector &vBasePos, const Vector &vecFireAtPosition, Vector *pResult ); // Gets a vehicle the enemy is in (if any) CBaseEntity *GetEnemyVehicle(); // Updates the perpendicular path distance for the chopper float UpdatePerpPathDistance( float flMaxPathOffset ); // Purpose : void UpdateEnemyLeading( void ); // Drop those bombs! void DropBombs( ); // Should we drop those bombs? bool ShouldDropBombs( void ); // Returns the max firing distance float GetMaxFiringDistance(); // Make sure we don't hit too many times void FireBullets( const FireBulletsInfo_t &info ); // Is it "fair" to drop this bomb? bool IsBombDropFair( const Vector &vecBombStartPos, const Vector &vecVelocity ); // Actually drops the bomb void CreateBomb( bool bCheckForFairness = true, Vector *pVecVelocity = NULL, bool bMegaBomb = false ); CGrenadeHelicopter *SpawnBombEntity( const Vector &vecPos, const Vector &vecVelocity ); // Spawns the bomb entity and sets it up // Deliberately aims as close as possible w/o hitting void AimCloseToTargetButMiss( CBaseEntity *pTarget, float flMinDist, float flMaxDist, const Vector &shootOrigin, Vector *pResult ); // Pops a shot inside the circle of death using the burst rules void ShootInsideCircleOfDeath( const Vector &vBasePos, const Vector &vecFireAtPosition ); // How easy is the target to hit? void UpdateTargetHittability(); // Add a smoke trail since we've taken more damage void AddSmokeTrail( const Vector &vecPos ); // Destroy all smoke trails void DestroySmokeTrails(); // Creates the breakable husk of an attack chopper void CreateChopperHusk(); // Pow! void ExplodeAndThrowChunk( const Vector &vecExplosionPos ); // Drop a corpse! void DropCorpse( int nDamage ); // Should we trigger a damage effect? bool ShouldTriggerDamageEffect( int nPrevHealth, int nEffectCount ) const; // Become indestructible void InputBecomeIndestructible( inputdata_t &inputdata ); // Purpose : float CreepTowardEnemy( float flSpeed, float flMinSpeed, float flMaxSpeed, float flMinDist, float flMaxDist ); // Start bullrush void InputStartBullrushBehavior( inputdata_t &inputdata ); void GetMaxSpeedAndAccel( float *pMaxSpeed, float *pAccelRate ); void ComputeAngularVelocity( const Vector &vecGoalUp, const Vector &vecFacingDirection ); void ComputeVelocity( const Vector &deltaPos, float flAdditionalHeight, float flMinDistFromSegment, float flMaxDistFromSegment, Vector *pVecAccel ); void FlightDirectlyOverhead( void ); // Methods related to computing leading distance float ComputeBombingLeadingDistance( float flSpeed, float flSpeedAlongPath, bool bEnemyInVehicle ); float ComputeBullrushLeadingDistance( float flSpeed, float flSpeedAlongPath, bool bEnemyInVehicle ); bool IsCarpetBombing() { return m_bIsCarpetBombing == true; } // Update the bullrush state void UpdateBullrushState( void ); // Whether to shoot at or bomb an idle player bool ShouldBombIdlePlayer( void ); // Different bomb-dropping behavior void BullrushBombs( ); // Switch to idle void SwitchToBullrushIdle( void ); // Secondary mode void SetSecondaryMode( int nMode, bool bRetainTime = false ); bool IsInSecondaryMode( int nMode ); float SecondaryModeTime( ) const; // Should the chopper shoot the idle player? bool ShouldShootIdlePlayerInBullrush(); // Shutdown shooting during bullrush void ShutdownGunDuringBullrush( ); // Updates the enemy virtual float EnemySearchDistance( ); // Prototype zapper bool IsValidZapTarget( CBaseEntity *pTarget ); void CreateZapBeam( const Vector &vecTargetPos ); void CreateEntityZapEffect( CBaseEntity *pEnt ); // Blink lights void BlinkLightsThink(); // Spotlights void SpotlightThink(); void SpotlightStartup(); void SpotlightShutdown(); CBaseEntity *GetCrashPoint() { return m_hCrashPoint.Get(); } private: enum { ATTACK_MODE_DEFAULT = 0, ATTACK_MODE_BOMB_VEHICLE, ATTACK_MODE_TRAIL_VEHICLE, ATTACK_MODE_ALWAYS_LEAD_VEHICLE, ATTACK_MODE_BULLRUSH_VEHICLE, }; enum { MAX_SMOKE_TRAILS = 5, MAX_EXPLOSIONS = 13, MAX_CORPSES = 2, }; enum { BULLRUSH_MODE_WAIT_FOR_ENEMY = 0, BULLRUSH_MODE_GET_DISTANCE, BULLRUSH_MODE_DROP_BOMBS_FIXED_SPEED, BULLRUSH_MODE_DROP_BOMBS_FOLLOW_PLAYER, BULLRUSH_MODE_SHOOT_GUN, BULLRUSH_MODE_MEGA_BOMB, BULLRUSH_MODE_SHOOT_IDLE_PLAYER, }; enum { SHOOT_MODE_DEFAULT = 0, SHOOT_MODE_LONG_CYCLE, SHOOT_MODE_CONTINUOUS, SHOOT_MODE_FAST, }; #ifdef HL2_EPISODIC void InitBoneFollowers( void ); CBoneFollowerManager m_BoneFollowerManager; #endif // HL2_EPISODIC CAI_Spotlight m_Spotlight; Vector m_angGun; QAngle m_vecAngAcceleration; int m_iAmmoType; float m_flLastCorpseFall; GunState_t m_nGunState; float m_flChargeTime; float m_flIdleTimeDelay; int m_nRemainingBursts; int m_nGrenadeCount; float m_flPathOffset; float m_flAcrossTime; float m_flCurrPathOffset; int m_nBurstHits; int m_nMaxBurstHits; float m_flCircleOfDeathRadius; int m_nAttackMode; float m_flInputDropBombTime; CHandle m_hSensor; float m_flAvoidMetric; AngularImpulse m_vecLastAngVelocity; CHandle m_hSmokeTrail[MAX_SMOKE_TRAILS]; int m_nSmokeTrailCount; bool m_bIndestructible; float m_flGracePeriod; bool m_bBombsExplodeOnContact; bool m_bNonCombat; int m_nNearShots; int m_nMaxNearShots; // Bomb dropping attachments int m_nGunTipAttachment; int m_nGunBaseAttachment; int m_nBombAttachment; int m_nSpotlightAttachment; float m_flLastFastTime; // Secondary modes int m_nSecondaryMode; float m_flSecondaryModeStartTime; // Bullrush behavior bool m_bRushForward; float m_flBullrushAdditionalHeight; int m_nBullrushBombMode; float m_flNextBullrushBombTime; float m_flNextMegaBombHealth; // Shooting method int m_nShootingMode; bool m_bDeadlyShooting; // Bombing suppression bool m_bBombingSuppressed; // Blinking lights CHandle m_hLights[MAX_HELICOPTER_LIGHTS]; bool m_bShortBlink; // Path behavior bool m_bIgnorePathVisibilityTests; // Teleport bool m_bAlwaysTransition; string_t m_iszTransitionTarget; // Special attacks bool m_bIsCarpetBombing; // Fun damage effects float m_flGoalRollDmg; float m_flGoalYawDmg; // Sounds CSoundPatch *m_pGunFiringSound; // Outputs COutputInt m_OnHealthChanged; COutputEvent m_OnShotDown; // Crashing EHANDLE m_hCrashPoint; }; #ifdef HL2_EPISODIC static const char *pFollowerBoneNames[] = { "Chopper.Body" }; #endif // HL2_EPISODIC LINK_ENTITY_TO_CLASS( npc_helicopter, CNPC_AttackHelicopter ); BEGIN_DATADESC( CNPC_AttackHelicopter ) DEFINE_ENTITYFUNC( FlyTouch ), DEFINE_EMBEDDED( m_Spotlight ), #ifdef HL2_EPISODIC DEFINE_EMBEDDED( m_BoneFollowerManager ), #endif DEFINE_FIELD( m_angGun, FIELD_VECTOR ), DEFINE_FIELD( m_vecAngAcceleration, FIELD_VECTOR ), DEFINE_FIELD( m_iAmmoType, FIELD_INTEGER ), DEFINE_FIELD( m_flLastCorpseFall, FIELD_TIME ), DEFINE_FIELD( m_nGunState, FIELD_INTEGER ), DEFINE_FIELD( m_flChargeTime, FIELD_TIME ), DEFINE_FIELD( m_flIdleTimeDelay, FIELD_FLOAT ), DEFINE_FIELD( m_nRemainingBursts, FIELD_INTEGER ), DEFINE_FIELD( m_nGrenadeCount, FIELD_INTEGER ), DEFINE_FIELD( m_flPathOffset, FIELD_FLOAT ), DEFINE_FIELD( m_flAcrossTime, FIELD_TIME ), DEFINE_FIELD( m_flCurrPathOffset, FIELD_FLOAT ), DEFINE_FIELD( m_nBurstHits, FIELD_INTEGER ), DEFINE_FIELD( m_nMaxBurstHits, FIELD_INTEGER ), DEFINE_FIELD( m_flCircleOfDeathRadius, FIELD_FLOAT ), DEFINE_FIELD( m_nAttackMode, FIELD_INTEGER ), DEFINE_FIELD( m_flInputDropBombTime, FIELD_TIME ), DEFINE_FIELD( m_hSensor, FIELD_EHANDLE ), DEFINE_FIELD( m_flAvoidMetric, FIELD_FLOAT ), DEFINE_FIELD( m_vecLastAngVelocity, FIELD_VECTOR ), DEFINE_AUTO_ARRAY( m_hSmokeTrail, FIELD_EHANDLE ), DEFINE_FIELD( m_nSmokeTrailCount, FIELD_INTEGER ), DEFINE_FIELD( m_nNearShots, FIELD_INTEGER ), DEFINE_FIELD( m_nMaxNearShots, FIELD_INTEGER ), // DEFINE_FIELD( m_nGunTipAttachment, FIELD_INTEGER ), // DEFINE_FIELD( m_nGunBaseAttachment, FIELD_INTEGER ), // DEFINE_FIELD( m_nBombAttachment, FIELD_INTEGER ), // DEFINE_FIELD( m_nSpotlightAttachment, FIELD_INTEGER ), DEFINE_FIELD( m_flLastFastTime, FIELD_TIME ), DEFINE_FIELD( m_nSecondaryMode, FIELD_INTEGER ), DEFINE_FIELD( m_flSecondaryModeStartTime, FIELD_TIME ), DEFINE_FIELD( m_bRushForward, FIELD_BOOLEAN ), DEFINE_FIELD( m_flBullrushAdditionalHeight, FIELD_FLOAT ), DEFINE_FIELD( m_nBullrushBombMode, FIELD_INTEGER ), DEFINE_FIELD( m_flNextBullrushBombTime, FIELD_TIME ), DEFINE_FIELD( m_flNextMegaBombHealth, FIELD_FLOAT ), DEFINE_FIELD( m_nShootingMode, FIELD_INTEGER ), DEFINE_FIELD( m_bDeadlyShooting, FIELD_BOOLEAN ), DEFINE_FIELD( m_bBombingSuppressed, FIELD_BOOLEAN ), DEFINE_SOUNDPATCH( m_pGunFiringSound ), DEFINE_AUTO_ARRAY( m_hLights, FIELD_EHANDLE ), DEFINE_FIELD( m_bIgnorePathVisibilityTests, FIELD_BOOLEAN ), DEFINE_FIELD( m_bShortBlink, FIELD_BOOLEAN ), DEFINE_FIELD( m_bIndestructible, FIELD_BOOLEAN ), DEFINE_FIELD( m_bBombsExplodeOnContact, FIELD_BOOLEAN ), DEFINE_KEYFIELD( m_bAlwaysTransition, FIELD_BOOLEAN, "AlwaysTransition" ), DEFINE_KEYFIELD( m_iszTransitionTarget, FIELD_STRING, "TransitionTarget" ), DEFINE_FIELD( m_bIsCarpetBombing, FIELD_BOOLEAN ), DEFINE_INPUTFUNC( FIELD_VOID, "EnableAlwaysTransition", InputEnableAlwaysTransition ), DEFINE_INPUTFUNC( FIELD_VOID, "DisableAlwaysTransition", InputDisableAlwaysTransition ), DEFINE_INPUTFUNC( FIELD_VOID, "OutsideTransition", InputOutsideTransition ), DEFINE_INPUTFUNC( FIELD_STRING, "SetTransitionTarget", InputSetOutsideTransitionTarget ), DEFINE_KEYFIELD( m_flGracePeriod, FIELD_FLOAT, "GracePeriod" ), DEFINE_KEYFIELD( m_flMaxSpeed, FIELD_FLOAT, "PatrolSpeed" ), DEFINE_KEYFIELD( m_bNonCombat, FIELD_BOOLEAN, "NonCombat" ), DEFINE_FIELD( m_hCrashPoint, FIELD_EHANDLE ), DEFINE_INPUTFUNC( FIELD_VOID, "ResetIdleTime", InputResetIdleTime ), DEFINE_INPUTFUNC( FIELD_VOID, "StartAlwaysLeadingVehicle", InputStartAlwaysLeadingVehicle ), DEFINE_INPUTFUNC( FIELD_VOID, "StartBombingVehicle", InputStartBombingVehicle ), DEFINE_INPUTFUNC( FIELD_VOID, "StartTrailingVehicle", InputStartTrailingVehicle ), DEFINE_INPUTFUNC( FIELD_VOID, "StartDefaultBehavior", InputStartDefaultBehavior ), DEFINE_INPUTFUNC( FIELD_VOID, "StartBullrushBehavior", InputStartBullrushBehavior ), DEFINE_INPUTFUNC( FIELD_VOID, "DropBomb", InputDropBomb ), DEFINE_INPUTFUNC( FIELD_VOID, "DropBombStraightDown", InputDropBombStraightDown ), DEFINE_INPUTFUNC( FIELD_STRING, "DropBombAtTargetAlways", InputDropBombAtTargetAlways ), DEFINE_INPUTFUNC( FIELD_STRING, "DropBombAtTarget", InputDropBombAtTarget ), DEFINE_INPUTFUNC( FIELD_FLOAT, "DropBombDelay", InputDropBombDelay ), DEFINE_INPUTFUNC( FIELD_VOID, "StartCarpetBombing", InputStartCarpetBombing ), DEFINE_INPUTFUNC( FIELD_VOID, "StopCarpetBombing", InputStopCarpetBombing ), DEFINE_INPUTFUNC( FIELD_VOID, "BecomeIndestructible", InputBecomeIndestructible ), DEFINE_INPUTFUNC( FIELD_VOID, "EnableDeadlyShooting", InputEnableDeadlyShooting ), DEFINE_INPUTFUNC( FIELD_VOID, "DisableDeadlyShooting", InputDisableDeadlyShooting ), DEFINE_INPUTFUNC( FIELD_VOID, "StartNormalShooting", InputStartNormalShooting ), DEFINE_INPUTFUNC( FIELD_VOID, "StartLongCycleShooting", InputStartLongCycleShooting ), DEFINE_INPUTFUNC( FIELD_VOID, "StartContinuousShooting", InputStartContinuousShooting ), DEFINE_INPUTFUNC( FIELD_VOID, "StartFastShooting", InputStartFastShooting ), DEFINE_INPUTFUNC( FIELD_VOID, "GunOff", InputGunOff ), DEFINE_INPUTFUNC( FIELD_FLOAT, "SetHealthFraction", InputSetHealthFraction ), DEFINE_INPUTFUNC( FIELD_VOID, "StartBombExplodeOnContact", InputStartBombExplodeOnContact ), DEFINE_INPUTFUNC( FIELD_VOID, "StopBombExplodeOnContact", InputStopBombExplodeOnContact ), DEFINE_INPUTFUNC( FIELD_VOID, "DisablePathVisibilityTests", InputDisablePathVisibilityTests ), DEFINE_INPUTFUNC( FIELD_VOID, "EnablePathVisibilityTests", InputEnablePathVisibilityTests ), DEFINE_INPUTFUNC( FIELD_VOID, "SelfDestruct", InputSelfDestruct ), DEFINE_THINKFUNC( BlinkLightsThink ), DEFINE_THINKFUNC( SpotlightThink ), DEFINE_OUTPUT( m_OnHealthChanged, "OnHealthChanged" ), DEFINE_OUTPUT( m_OnShotDown, "OnShotDown" ), END_DATADESC() //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ CNPC_AttackHelicopter::CNPC_AttackHelicopter() : m_bNonCombat( false ), m_flGracePeriod( 2.0f ), m_bBombsExplodeOnContact( false ) { m_flMaxSpeed = 0; } CNPC_AttackHelicopter::~CNPC_AttackHelicopter(void) { } //----------------------------------------------------------------------------- // Purpose: Shuts down looping sounds when we are killed in combat or deleted. //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::StopLoopingSounds() { BaseClass::StopLoopingSounds(); if ( m_pGunFiringSound ) { CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); controller.SoundDestroy( m_pGunFiringSound ); m_pGunFiringSound = NULL; } } //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ void Chopper_PrecacheChunks( CBaseEntity *pChopper ) { for ( int i = 0; i < CHOPPER_MAX_CHUNKS; ++i ) { pChopper->PrecacheModel( s_pChunkModelName[i] ); } pChopper->PrecacheModel( HELICOPTER_CHUNK_COCKPIT ); pChopper->PrecacheModel( HELICOPTER_CHUNK_TAIL ); pChopper->PrecacheModel( HELICOPTER_CHUNK_BODY ); } //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::Precache( void ) { BaseClass::Precache(); if ( !HasSpawnFlags(SF_HELICOPTER_ELECTRICAL_DRONE) ) { PrecacheModel( CHOPPER_MODEL_NAME ); } else { PrecacheModel( CHOPPER_DRONE_NAME ); } PrecacheModel( CHOPPER_RED_LIGHT_SPRITE ); //PrecacheModel( CHOPPER_MODEL_CORPSE_NAME ); // If we're never going to engage in combat, we don't need to load these assets! if ( m_bNonCombat == false ) { UTIL_PrecacheOther( "grenade_helicopter" ); UTIL_PrecacheOther( "env_fire_trail" ); Chopper_PrecacheChunks( this ); PrecacheModel("models/combine_soldier.mdl"); UTIL_PrecacheOther( "env_explosion" ); } PrecacheScriptSound("NPC_AttackHelicopter.ChargeGun"); if ( HasSpawnFlags( SF_HELICOPTER_LOUD_ROTOR_SOUND ) ) { PrecacheScriptSound("NPC_AttackHelicopter.RotorsLoud"); } else { PrecacheScriptSound("NPC_AttackHelicopter.Rotors"); } PrecacheScriptSound( "NPC_AttackHelicopter.DropMine" ); PrecacheScriptSound( "NPC_AttackHelicopter.BadlyDamagedAlert" ); PrecacheScriptSound( "NPC_AttackHelicopter.CrashingAlarm1" ); PrecacheScriptSound( "NPC_AttackHelicopter.MegabombAlert" ); PrecacheScriptSound( "NPC_AttackHelicopter.RotorBlast" ); PrecacheScriptSound( "NPC_AttackHelicopter.EngineFailure" ); PrecacheScriptSound( "NPC_AttackHelicopter.FireGun" ); PrecacheScriptSound( "NPC_AttackHelicopter.Crash" ); PrecacheScriptSound( "HelicopterBomb.HardImpact" ); PrecacheScriptSound( "ReallyLoudSpark" ); PrecacheScriptSound( "NPC_AttackHelicopterGrenade.Ping" ); PrecacheEffect( "ChopperMuzzleFlash" ); PrecacheEffect( "TeslaZap" ); PrecacheEffect( "TeslaHitboxes" ); PrecacheEffect( "HelicopterMegaBomb" ); const char *pszTracerName = GetTracerType(); if ( pszTracerName ) { PrecacheEffect( pszTracerName ); } } int CNPC_AttackHelicopter::ObjectCaps() { int caps = BaseClass::ObjectCaps(); if ( m_bAlwaysTransition ) caps |= FCAP_NOTIFY_ON_TRANSITION; return caps; } void CNPC_AttackHelicopter::InputOutsideTransition( inputdata_t &inputdata ) { CBaseEntity *pEnt = gEntList.FindEntityByName( NULL, m_iszTransitionTarget ); if ( pEnt ) { Vector teleportLocation = pEnt->GetAbsOrigin(); QAngle teleportAngles = pEnt->GetAbsAngles(); Teleport( &teleportLocation, &teleportAngles, &vec3_origin ); Teleported(); } else { DevMsg( 2, "NPC \"%s\" failed to find a suitable transition a point\n", STRING(GetEntityName()) ); } } void CNPC_AttackHelicopter::InputSetOutsideTransitionTarget( inputdata_t &inputdata ) { m_iszTransitionTarget = MAKE_STRING( inputdata.value.String() ); } //----------------------------------------------------------------------------- // Create components //----------------------------------------------------------------------------- bool CNPC_AttackHelicopter::CreateComponents() { if ( !BaseClass::CreateComponents() ) return false; m_Spotlight.Init( this, AI_SPOTLIGHT_NO_DLIGHTS, 45.0f, 500.0f ); return true; } //----------------------------------------------------------------------------- // Purpose : //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::Spawn( void ) { Precache( ); m_bIndestructible = false; m_bDeadlyShooting = false; m_bBombingSuppressed = false; m_bIgnorePathVisibilityTests = false; if ( !HasSpawnFlags(SF_HELICOPTER_ELECTRICAL_DRONE) ) { SetModel( CHOPPER_MODEL_NAME ); } else { SetModel( CHOPPER_DRONE_NAME ); } ExtractBbox( SelectHeaviestSequence( ACT_IDLE ), m_cullBoxMins, m_cullBoxMaxs ); GetEnemies()->SetFreeKnowledgeDuration( DEFAULT_FREE_KNOWLEDGE_DURATION ); float flLoadedSpeed = m_flMaxSpeed; BaseClass::Spawn(); float flChaseDist = HasSpawnFlags( SF_HELICOPTER_AGGRESSIVE ) ? CHOPPER_MIN_AGGRESSIVE_CHASE_DIST_DIFF : CHOPPER_MIN_CHASE_DIST_DIFF; InitPathingData( CHOPPER_ARRIVE_DIST, flChaseDist, CHOPPER_AVOID_DIST ); SetFarthestPathDist( GetMaxFiringDistance() ); m_flFrozenMax = 0.0f; m_takedamage = DAMAGE_YES; m_nGunState = GUN_STATE_IDLE; SetHullType( HULL_LARGE_CENTERED ); SetHullSizeNormal(); #ifdef HL2_EPISODIC CreateVPhysics(); #endif // HL2_EPISODIC SetPauseState( PAUSE_NO_PAUSE ); m_iMaxHealth = m_iHealth = sk_helicopter_health.GetInt(); m_flMaxSpeed = flLoadedSpeed; if ( m_flMaxSpeed <= 0 ) { m_flMaxSpeed = CHOPPER_MAX_SPEED; } m_flNextMegaBombHealth = m_iMaxHealth - m_iMaxHealth * g_helicopter_bullrush_mega_bomb_health.GetFloat(); m_nGrenadeCount = CHOPPER_BOMB_DROP_COUNT; m_flFieldOfView = -1.0; // 360 degrees m_flIdleTimeDelay = 0.0f; m_iAmmoType = GetAmmoDef()->Index("HelicopterGun"); InitBoneControllers(); m_fHelicopterFlags = BITS_HELICOPTER_GUN_ON; m_bSuppressSound = false; m_flAcrossTime = -1.0f; m_flPathOffset = 0.0f; m_flCurrPathOffset = 0.0f; m_nAttackMode = ATTACK_MODE_DEFAULT; m_flInputDropBombTime = gpGlobals->curtime; SetActivity( ACT_IDLE ); int nBombAttachment = LookupAttachment("bomb"); m_hSensor = static_cast(CreateEntityByName( "npc_helicoptersensor" )); m_hSensor->Spawn(); m_hSensor->SetParent( this, nBombAttachment ); m_hSensor->SetLocalOrigin( vec3_origin ); m_hSensor->SetLocalAngles( vec3_angle ); m_hSensor->SetOwnerEntity( this ); AddFlag( FL_AIMTARGET ); m_hCrashPoint.Set( NULL ); } #ifdef HL2_EPISODIC //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- bool CNPC_AttackHelicopter::CreateVPhysics( void ) { InitBoneFollowers(); return BaseClass::CreateVPhysics(); } #endif // HL2_EPISODIC //------------------------------------------------------------------------------ // Startup the chopper //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::Startup() { BaseClass::Startup(); if ( HasSpawnFlags( SF_HELICOPTER_LIGHTS ) ) { for ( int i = 0; i < MAX_HELICOPTER_LIGHTS; ++i ) { // See if there's an attachment for this smoke trail char buf[32]; Q_snprintf( buf, 32, "Light_Red%d", i ); int nAttachment = LookupAttachment( buf ); if ( nAttachment == 0 ) { m_hLights[i] = NULL; continue; } m_hLights[i] = CSprite::SpriteCreate( CHOPPER_RED_LIGHT_SPRITE, vec3_origin, false ); if ( !m_hLights[i] ) continue; m_hLights[i]->SetParent( this, nAttachment ); m_hLights[i]->SetLocalOrigin( vec3_origin ); m_hLights[i]->SetLocalVelocity( vec3_origin ); m_hLights[i]->SetMoveType( MOVETYPE_NONE ); m_hLights[i]->SetTransparency( kRenderTransAdd, 255, 255, 255, 200, kRenderFxNone ); m_hLights[i]->SetScale( 1.0f ); m_hLights[i]->TurnOn(); } SetContextThink( &CNPC_AttackHelicopter::BlinkLightsThink, gpGlobals->curtime + CHOPPER_LIGHT_BLINK_TIME_SHORT, s_pBlinkLightThinkContext ); } } //------------------------------------------------------------------------------ // Startup the chopper //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::BlinkLightsThink() { bool bIsOn = false; for ( int i = 0; i < MAX_HELICOPTER_LIGHTS; ++i ) { if ( !m_hLights[i] ) continue; if ( m_hLights[i]->GetScale() > 0.1f ) { m_hLights[i]->SetScale( 0.1f, CHOPPER_LIGHT_BLINK_TIME_SHORT ); } else { m_hLights[i]->SetScale( 0.5f, 0.0f ); bIsOn = true; } } float flTime; if ( bIsOn ) { flTime = CHOPPER_LIGHT_BLINK_TIME_SHORT; } else { flTime = m_bShortBlink ? CHOPPER_LIGHT_BLINK_TIME_SHORT : CHOPPER_LIGHT_BLINK_TIME; m_bShortBlink = !m_bShortBlink; } SetContextThink( &CNPC_AttackHelicopter::BlinkLightsThink, gpGlobals->curtime + flTime, s_pBlinkLightThinkContext ); } //------------------------------------------------------------------------------ // Start up spotlights //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::SpotlightStartup() { if ( !HasSpawnFlags( SF_HELICOPTER_LIGHTS ) ) return; Vector vecForward; Vector vecOrigin; GetAttachment( m_nSpotlightAttachment, vecOrigin, &vecForward ); m_Spotlight.SpotlightCreate( m_nSpotlightAttachment, vecForward ); SpotlightThink(); } //------------------------------------------------------------------------------ // Shutdown spotlights //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::SpotlightShutdown() { m_Spotlight.SpotlightDestroy(); SetContextThink( NULL, gpGlobals->curtime, s_pSpotlightThinkContext ); } //------------------------------------------------------------------------------ // Spotlights //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::SpotlightThink() { // NOTE: This function should deal with all deactivation cases if ( m_lifeState != LIFE_ALIVE ) { SpotlightShutdown(); return; } switch( m_nAttackMode ) { case ATTACK_MODE_BULLRUSH_VEHICLE: { switch ( m_nSecondaryMode ) { case BULLRUSH_MODE_SHOOT_GUN: { Vector vecForward; Vector vecOrigin; GetAttachment( m_nSpotlightAttachment, vecOrigin, &vecForward ); m_Spotlight.SetSpotlightTargetDirection( vecForward ); } break; case BULLRUSH_MODE_SHOOT_IDLE_PLAYER: if ( GetEnemy() ) { m_Spotlight.SetSpotlightTargetPos( GetEnemy()->WorldSpaceCenter() ); } break; default: SpotlightShutdown(); return; } } break; default: SpotlightShutdown(); return; } m_Spotlight.Update(); SetContextThink( &CNPC_AttackHelicopter::SpotlightThink, gpGlobals->curtime + TICK_INTERVAL, s_pSpotlightThinkContext ); } //----------------------------------------------------------------------------- // Purpose: Always transition along with the player //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::InputEnableAlwaysTransition( inputdata_t &inputdata ) { m_bAlwaysTransition = true; } //----------------------------------------------------------------------------- // Purpose: Stop always transitioning along with the player //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::InputDisableAlwaysTransition( inputdata_t &inputdata ) { m_bAlwaysTransition = false; } //------------------------------------------------------------------------------ // On Remove //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::UpdateOnRemove() { BaseClass::UpdateOnRemove(); StopLoopingSounds(); UTIL_Remove(m_hSensor); DestroySmokeTrails(); for ( int i = 0; i < MAX_HELICOPTER_LIGHTS; ++i ) { if ( m_hLights[i] ) { UTIL_Remove( m_hLights[i] ); m_hLights[i] = NULL; } } #ifdef HL2_EPISODIC m_BoneFollowerManager.DestroyBoneFollowers(); #endif // HL2_EPISODIC } //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::Activate( void ) { BaseClass::Activate(); m_nGunBaseAttachment = LookupAttachment("gun"); m_nGunTipAttachment = LookupAttachment("muzzle"); m_nBombAttachment = LookupAttachment("bomb"); m_nSpotlightAttachment = LookupAttachment("spotlight"); if ( HasSpawnFlags( SF_HELICOPTER_LONG_SHADOW ) ) { SetShadowCastDistance( 2048 ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- const char *CNPC_AttackHelicopter::GetTracerType( void ) { return "HelicopterTracer"; } //----------------------------------------------------------------------------- // Allows the shooter to change the impact effect of his bullets //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::DoImpactEffect( trace_t &tr, int nDamageType ) { UTIL_ImpactTrace( &tr, nDamageType, "HelicopterImpact" ); } //------------------------------------------------------------------------------ // Purpose : Create our rotor sound //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InitializeRotorSound( void ) { if ( !m_pRotorSound ) { CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); CPASAttenuationFilter filter( this ); if ( HasSpawnFlags( SF_HELICOPTER_LOUD_ROTOR_SOUND ) ) { m_pRotorSound = controller.SoundCreate( filter, entindex(), "NPC_AttackHelicopter.RotorsLoud" ); } else { m_pRotorSound = controller.SoundCreate( filter, entindex(), "NPC_AttackHelicopter.Rotors" ); } m_pRotorBlast = controller.SoundCreate( filter, entindex(), "NPC_AttackHelicopter.RotorBlast" ); m_pGunFiringSound = controller.SoundCreate( filter, entindex(), "NPC_AttackHelicopter.FireGun" ); controller.Play( m_pGunFiringSound, 0.0, 100 ); } else { Assert(m_pRotorSound); Assert(m_pRotorBlast); Assert(m_pGunFiringSound); } BaseClass::InitializeRotorSound(); } //------------------------------------------------------------------------------ // Gets the max speed of the helicopter //------------------------------------------------------------------------------ float CNPC_AttackHelicopter::GetMaxSpeed() { if ( HasSpawnFlags(SF_HELICOPTER_ELECTRICAL_DRONE) ) return DRONE_SPEED; if ( ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) && IsInSecondaryMode( BULLRUSH_MODE_DROP_BOMBS_FIXED_SPEED ) ) return CHOPPER_BULLRUSH_ENEMY_BOMB_SPEED; if ( !GetEnemyVehicle() ) return BaseClass::GetMaxSpeed(); return 3000.0f; } float CNPC_AttackHelicopter::GetMaxSpeedFiring() { if ( HasSpawnFlags(SF_HELICOPTER_ELECTRICAL_DRONE) ) return DRONE_SPEED; if ( ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) && IsInSecondaryMode( BULLRUSH_MODE_DROP_BOMBS_FIXED_SPEED ) ) return CHOPPER_BULLRUSH_ENEMY_BOMB_SPEED; if ( !GetEnemyVehicle() ) return BaseClass::GetMaxSpeedFiring(); return 3000.0f; } //------------------------------------------------------------------------------ // Returns the max firing distance //------------------------------------------------------------------------------ float CNPC_AttackHelicopter::GetMaxFiringDistance() { if ( !GetEnemyVehicle() ) return CHOPPER_GUN_MAX_FIRING_DIST; return 8000.0f; } //------------------------------------------------------------------------------ // Updates the enemy //------------------------------------------------------------------------------ float CNPC_AttackHelicopter::EnemySearchDistance( ) { return 6000.0f; } //------------------------------------------------------------------------------ // Leading behaviors //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InputStartBombingVehicle( inputdata_t &inputdata ) { m_nAttackMode = ATTACK_MODE_BOMB_VEHICLE; SetLeadingDistance( 1500.0f ); } void CNPC_AttackHelicopter::InputStartTrailingVehicle( inputdata_t &inputdata ) { m_nAttackMode = ATTACK_MODE_TRAIL_VEHICLE; SetLeadingDistance( -1500.0f ); } void CNPC_AttackHelicopter::InputStartDefaultBehavior( inputdata_t &inputdata ) { m_nAttackMode = ATTACK_MODE_DEFAULT; } void CNPC_AttackHelicopter::InputStartAlwaysLeadingVehicle( inputdata_t &inputdata ) { m_nAttackMode = ATTACK_MODE_ALWAYS_LEAD_VEHICLE; SetLeadingDistance( 0.0f ); } void CNPC_AttackHelicopter::InputStartBullrushBehavior( inputdata_t &inputdata ) { if ( m_nAttackMode != ATTACK_MODE_BULLRUSH_VEHICLE ) { m_nAttackMode = ATTACK_MODE_BULLRUSH_VEHICLE; SetSecondaryMode( BULLRUSH_MODE_WAIT_FOR_ENEMY ); SetLeadingDistance( 0.0f ); } } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InputStartCarpetBombing( inputdata_t &inputdata ) { m_bIsCarpetBombing = true; } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InputStopCarpetBombing( inputdata_t &inputdata ) { m_bIsCarpetBombing = false; } //------------------------------------------------------------------------------ // Become indestructible //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InputBecomeIndestructible( inputdata_t &inputdata ) { m_bIndestructible = true; } //------------------------------------------------------------------------------ // Deadly shooting, tex! //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InputEnableDeadlyShooting( inputdata_t &inputdata ) { m_bDeadlyShooting = true; } void CNPC_AttackHelicopter::InputDisableDeadlyShooting( inputdata_t &inputdata ) { m_bDeadlyShooting = false; } void CNPC_AttackHelicopter::InputStartNormalShooting( inputdata_t &inputdata ) { m_nShootingMode = SHOOT_MODE_DEFAULT; } void CNPC_AttackHelicopter::InputStartLongCycleShooting( inputdata_t &inputdata ) { m_nShootingMode = SHOOT_MODE_LONG_CYCLE; } void CNPC_AttackHelicopter::InputStartContinuousShooting( inputdata_t &inputdata ) { m_nShootingMode = SHOOT_MODE_CONTINUOUS; } void CNPC_AttackHelicopter::InputStartFastShooting( inputdata_t &inputdata ) { m_nShootingMode = SHOOT_MODE_FAST; } //------------------------------------------------------------------------------ // Deadly shooting, tex! //------------------------------------------------------------------------------ bool CNPC_AttackHelicopter::IsDeadlyShooting() { if ( m_bDeadlyShooting ) return true; if (( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) && IsInSecondaryMode( BULLRUSH_MODE_SHOOT_IDLE_PLAYER ) ) { return (!GetEnemyVehicle()) && GetEnemy() && GetEnemy()->IsPlayer(); } return false; } int CNPC_AttackHelicopter::GetShootingMode( ) { if ( IsDeadlyShooting() ) return SHOOT_MODE_LONG_CYCLE; if ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) return SHOOT_MODE_CONTINUOUS; return m_nShootingMode; } //----------------------------------------------------------------------------- // Bombing suppression //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::InputEnableBombing( inputdata_t &inputdata ) { m_bBombingSuppressed = false; } void CNPC_AttackHelicopter::InputDisableBombing( inputdata_t &inputdata ) { m_bBombingSuppressed = true; } //----------------------------------------------------------------------------- // Visibility tests //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::InputDisablePathVisibilityTests( inputdata_t &inputdata ) { m_bIgnorePathVisibilityTests = true; GetEnemies()->SetUnforgettable( GetEnemy(), true ); } void CNPC_AttackHelicopter::InputEnablePathVisibilityTests( inputdata_t &inputdata ) { m_bIgnorePathVisibilityTests = false; GetEnemies()->SetUnforgettable( GetEnemy(), false ); } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::InputSelfDestruct( inputdata_t &inputdata ) { m_lifeState = LIFE_ALIVE; // Force to die properly. CTakeDamageInfo info( this, this, Vector(0, 0, 1), WorldSpaceCenter(), GetMaxHealth(), CLASS_MISSILE ); TakeDamage( info ); } //----------------------------------------------------------------------------- // For scripted times where it *has* to shoot //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::InputSetHealthFraction( inputdata_t &inputdata ) { // Sets the health fraction, no damage effects if ( inputdata.value.Float() > 0 ) { SetHealth( GetMaxHealth() * inputdata.value.Float() * 0.01f ); } } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::InputStartBombExplodeOnContact( inputdata_t &inputdata ) { m_bBombsExplodeOnContact = true; } //----------------------------------------------------------------------------- // Purpose: // Input : &inputdata - //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::InputStopBombExplodeOnContact( inputdata_t &inputdata ) { m_bBombsExplodeOnContact = false; } //------------------------------------------------------------------------------ // For scripted times where it *has* to shoot //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InputResetIdleTime( inputdata_t &inputdata ) { if ( m_nGunState == GUN_STATE_IDLE ) { m_flNextAttack = gpGlobals->curtime; } } //----------------------------------------------------------------------------- // This trace filter ignores all breakables + physics props //----------------------------------------------------------------------------- class CTraceFilterChopper : public CTraceFilterSimple { DECLARE_CLASS( CTraceFilterChopper, CTraceFilterSimple ); public: CTraceFilterChopper( const IHandleEntity *passentity, int collisionGroup ); virtual bool ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ); private: const IHandleEntity *m_pPassEnt; int m_collisionGroup; }; CTraceFilterChopper::CTraceFilterChopper( const IHandleEntity *passentity, int collisionGroup ) : CTraceFilterSimple( passentity, collisionGroup ) { } bool CTraceFilterChopper::ShouldHitEntity( IHandleEntity *pServerEntity, int contentsMask ) { CBaseEntity *pEnt = static_cast(pServerEntity)->GetBaseEntity(); if ( pEnt ) { if ( FClassnameIs( pEnt, "func_breakable" ) || FClassnameIs( pEnt, "func_physbox" ) || FClassnameIs( pEnt, "prop_physics" ) || FClassnameIs( pEnt, "physics_prop" ) ) { return false; } } return BaseClass::ShouldHitEntity( pServerEntity, contentsMask ); } //----------------------------------------------------------------------------- // Enemy visibility check //----------------------------------------------------------------------------- CBaseEntity *CNPC_AttackHelicopter::FindTrackBlocker( const Vector &vecViewPoint, const Vector &vecTargetPos ) { if ( m_bIgnorePathVisibilityTests ) return NULL; CTraceFilterChopper chopperFilter( this, COLLISION_GROUP_NONE ); trace_t tr; AI_TraceHull( vecViewPoint, vecTargetPos, -Vector(4,4,4), Vector(4,4,4), MASK_SHOT, &chopperFilter, &tr ); if ( tr.fraction != 1.0f ) { Assert( tr.m_pEnt ); } return (tr.fraction != 1.0f) ? tr.m_pEnt : NULL; } //----------------------------------------------------------------------------- // More Enemy visibility check //----------------------------------------------------------------------------- bool CNPC_AttackHelicopter::FVisible( CBaseEntity *pEntity, int traceMask, CBaseEntity **ppBlocker ) { if ( pEntity->GetFlags() & FL_NOTARGET ) return false; #if 0 // FIXME: only block LOS through opaque water // don't look through water if ((m_nWaterLevel != 3 && pEntity->m_nWaterLevel == 3) || (m_nWaterLevel == 3 && pEntity->m_nWaterLevel == 0)) return false; #endif Vector vecLookerOrigin = EyePosition();//look through the caller's 'eyes' Vector vecTargetOrigin = pEntity->EyePosition(); CTraceFilterChopper chopperFilter( this, COLLISION_GROUP_NONE ); trace_t tr; UTIL_TraceLine(vecLookerOrigin, vecTargetOrigin, traceMask, &chopperFilter, &tr); if (tr.fraction != 1.0) { // Got line of sight! if ( tr.m_pEnt == pEntity ) return true; // Got line of sight on the vehicle the player is driving! if ( pEntity && pEntity->IsPlayer() ) { CBasePlayer *pPlayer = assert_cast( pEntity ); if ( tr.m_pEnt == pPlayer->GetVehicleEntity() ) return true; } if (ppBlocker) { *ppBlocker = tr.m_pEnt; } return false;// Line of sight is not established } return true;// line of sight is valid. } //------------------------------------------------------------------------------ // Shot spread //------------------------------------------------------------------------------ #define PLAYER_TIGHTEN_FACTOR 0.75f Vector CNPC_AttackHelicopter::GetAttackSpread( CBaseCombatWeapon *pWeapon, CBaseEntity *pTarget ) { float flSinConeDegrees = sin( sk_helicopter_firingcone.GetFloat() * PLAYER_TIGHTEN_FACTOR * 0.5f * (3.14f / 180.0f) ); Vector vecSpread( flSinConeDegrees, flSinConeDegrees, flSinConeDegrees ); return vecSpread; } //------------------------------------------------------------------------------ // Find interesting nearby things to shoot //------------------------------------------------------------------------------ int CNPC_AttackHelicopter::BuildMissTargetList( int nCount, CBaseEntity **ppMissCandidates ) { int numMissCandidates = 0; CBaseEntity *pEnts[256]; Vector radius( 150, 150, 150 ); const Vector &vecSource = GetEnemy()->WorldSpaceCenter(); int numEnts = UTIL_EntitiesInBox( pEnts, 256, vecSource - radius, vecSource+radius, 0 ); for ( int i = 0; i < numEnts; i++ ) { if ( pEnts[i] == NULL ) continue; if ( numMissCandidates >= nCount ) break; // Miss candidates cannot include the player or his vehicle if ( pEnts[i] == GetEnemyVehicle() || pEnts[i] == GetEnemy() ) continue; // See if it's a good target candidate if ( FClassnameIs( pEnts[i], "prop_dynamic" ) || FClassnameIs( pEnts[i], "prop_physics" ) || FClassnameIs( pEnts[i], "physics_prop" ) ) { ppMissCandidates[numMissCandidates++] = pEnts[i]; } } return numMissCandidates; } //------------------------------------------------------------------------------ // Gets a vehicle the enemy is in (if any) //------------------------------------------------------------------------------ CBaseEntity *CNPC_AttackHelicopter::GetEnemyVehicle() { if ( !GetEnemy() ) return NULL; if ( !GetEnemy()->IsPlayer() ) return NULL; return static_cast(GetEnemy())->GetVehicleEntity(); } //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::ShootAtPlayer( const Vector &vBasePos, const Vector &vGunDir ) { // Fire one shots per round right at the player, using usual rules FireBulletsInfo_t info; info.m_vecSrc = vBasePos; info.m_vecSpread = VECTOR_CONE_PRECALCULATED; info.m_flDistance = MAX_COORD_RANGE; info.m_iAmmoType = m_iAmmoType; info.m_iTracerFreq = 1; info.m_vecDirShooting = GetActualShootTrajectory( vBasePos ); info.m_nFlags = FIRE_BULLETS_TEMPORARY_DANGER_SOUND; DoMuzzleFlash(); QAngle vGunAng; VectorAngles( vGunDir, vGunAng ); FireBullets( info ); // Fire the rest of the bullets at objects around the player CBaseEntity *ppNearbyTargets[16]; int nActualTargets = BuildMissTargetList( 16, ppNearbyTargets ); // Randomly sort it... int i; for ( i = 0; i < nActualTargets; ++i ) { int nSwap = random->RandomInt( 0, nActualTargets - 1 ); V_swap( ppNearbyTargets[i], ppNearbyTargets[nSwap] ); } // Just shoot where we're facing float flSinConeDegrees = sin( sk_helicopter_firingcone.GetFloat() * 0.5f * (3.14f / 180.0f) ); Vector vecSpread( flSinConeDegrees, flSinConeDegrees, flSinConeDegrees ); // How many times should we hit the player this time? int nDesiredHitCount = (int)(((float)( m_nMaxBurstHits - m_nBurstHits ) / (float)m_nRemainingBursts) + 0.5f); int nNearbyTargetCount = 0; int nPlayerShotCount = 0; for ( i = sk_helicopter_roundsperburst.GetInt() - 1; --i >= 0; ) { // Find something interesting around the enemy to shoot instead of just missing. if ( nActualTargets > nNearbyTargetCount ) { // FIXME: Constrain to the firing cone? ppNearbyTargets[nNearbyTargetCount]->CollisionProp()->RandomPointInBounds( Vector(.25, .25, .25), Vector(.75, .75, .75), &info.m_vecDirShooting ); info.m_vecDirShooting -= vBasePos; VectorNormalize( info.m_vecDirShooting ); info.m_vecSpread = VECTOR_CONE_PRECALCULATED; info.m_flDistance = MAX_COORD_RANGE; info.m_nFlags = FIRE_BULLETS_TEMPORARY_DANGER_SOUND; FireBullets( info ); ++nNearbyTargetCount; continue; } if ( GetEnemy() && ( nPlayerShotCount < nDesiredHitCount )) { GetEnemy()->CollisionProp()->RandomPointInBounds( Vector(0, 0, 0), Vector(1, 1, 1), &info.m_vecDirShooting ); info.m_vecDirShooting -= vBasePos; VectorNormalize( info.m_vecDirShooting ); info.m_vecSpread = VECTOR_CONE_PRECALCULATED; info.m_flDistance = MAX_COORD_RANGE; info.m_nFlags = FIRE_BULLETS_TEMPORARY_DANGER_SOUND; FireBullets( info ); ++nPlayerShotCount; continue; } // Nothing nearby; just fire randomly... info.m_vecDirShooting = vGunDir; info.m_vecSpread = vecSpread; info.m_flDistance = 8192; info.m_nFlags = FIRE_BULLETS_TEMPORARY_DANGER_SOUND; FireBullets( info ); } } //----------------------------------------------------------------------------- // Chooses a point within the circle of death to fire in //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::PickDirectionToCircleOfDeath( const Vector &vBasePos, const Vector &vecFireAtPosition, Vector *pResult ) { *pResult = vecFireAtPosition; float x, y; do { x = random->RandomFloat( -1.0f, 1.0f ); y = random->RandomFloat( -1.0f, 1.0f ); } while ( (x * x + y * y) > 1.0f ); pResult->x += x * m_flCircleOfDeathRadius; pResult->y += y * m_flCircleOfDeathRadius; *pResult -= vBasePos; VectorNormalize( *pResult ); } //----------------------------------------------------------------------------- // Deliberately aims as close as possible w/o hitting //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::AimCloseToTargetButMiss( CBaseEntity *pTarget, float flMinDist, float flMaxDist, const Vector &shootOrigin, Vector *pResult ) { Vector vecDirection; VectorSubtract( pTarget->WorldSpaceCenter(), shootOrigin, vecDirection ); float flDist = VectorNormalize( vecDirection ); float flRadius = pTarget->BoundingRadius() + random->RandomFloat( flMinDist, flMaxDist ); float flMinRadius = flRadius; if ( flDist > flRadius ) { flMinRadius = flDist * flRadius / sqrt( flDist * flDist - flRadius * flRadius ); } // Choose random points in a plane perpendicular to the shoot origin. Vector vecRandomDir; vecRandomDir.Random( -1.0f, 1.0f ); VectorMA( vecRandomDir, -DotProduct( vecDirection, vecRandomDir ), vecDirection, vecRandomDir ); VectorNormalize( vecRandomDir ); vecRandomDir *= flMinRadius; vecRandomDir += pTarget->WorldSpaceCenter(); VectorSubtract( vecRandomDir, shootOrigin, *pResult ); VectorNormalize( *pResult ); } //----------------------------------------------------------------------------- // Make sure we don't hit too many times //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::FireBullets( const FireBulletsInfo_t &info ) { // Use this to count the number of hits in a burst bool bIsPlayer = GetEnemy() && GetEnemy()->IsPlayer(); if ( !bIsPlayer ) { BaseClass::FireBullets( info ); return; } if ( !GetEnemyVehicle() && !IsDeadlyShooting() ) { if ( m_nBurstHits >= m_nMaxBurstHits ) { FireBulletsInfo_t actualInfo = info; actualInfo.m_pAdditionalIgnoreEnt = GetEnemy(); BaseClass::FireBullets( actualInfo ); return; } } CBasePlayer *pPlayer = assert_cast(GetEnemy()); int nPrevHealth = pPlayer->GetHealth(); int nPrevArmor = pPlayer->ArmorValue(); BaseClass::FireBullets( info ); if (( pPlayer->GetHealth() < nPrevHealth ) || ( pPlayer->ArmorValue() < nPrevArmor )) { ++m_nBurstHits; } } //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::ShootInsideCircleOfDeath( const Vector &vBasePos, const Vector &vecFireAtPosition ) { Vector vecFireDirection; if ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) { PickDirectionToCircleOfDeath( vBasePos, vecFireAtPosition, &vecFireDirection ); } else if ( ( m_nNearShots < m_nMaxNearShots ) || !GetEnemyVehicle() ) { if ( ( m_nBurstHits < m_nMaxBurstHits ) || !GetEnemy() ) { ++m_nNearShots; PickDirectionToCircleOfDeath( vBasePos, vecFireAtPosition, &vecFireDirection ); } else { m_nNearShots += 6; AimCloseToTargetButMiss( GetEnemy(), 20.0f, 50.0f, vBasePos, &vecFireDirection ); } } else { AimCloseToTargetButMiss( GetEnemyVehicle(), 10.0f, 80.0f, vBasePos, &vecFireDirection ); } FireBulletsInfo_t info( 1, vBasePos, vecFireDirection, VECTOR_CONE_PRECALCULATED, MAX_COORD_RANGE, m_iAmmoType ); info.m_iTracerFreq = 1; info.m_nFlags = FIRE_BULLETS_TEMPORARY_DANGER_SOUND; FireBullets( info ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::DoMuzzleFlash( void ) { BaseClass::DoMuzzleFlash(); CEffectData data; data.m_nAttachmentIndex = LookupAttachment( "muzzle" ); data.m_nEntIndex = entindex(); DispatchEffect( "ChopperMuzzleFlash", data ); } //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ #define HIT_VEHICLE_SPEED_MIN 200.0f #define HIT_VEHICLE_SPEED_MAX 500.0f void CNPC_AttackHelicopter::ShootAtVehicle( const Vector &vBasePos, const Vector &vecFireAtPosition ) { int nShotsRemaining = sk_helicopter_roundsperburst.GetInt(); DoMuzzleFlash(); // Do special code against episodic drivers if ( hl2_episodic.GetBool() ) { Vector vecVelocity; GetEnemyVehicle()->GetVelocity( &vecVelocity, NULL ); float flSpeed = clamp( vecVelocity.Length(), 0.0f, 400.0f ); float flRange = RemapVal( flSpeed, 0.0f, 400.0f, 0.05f, 1.0f ); // Alter each shot's trajectory based on our speed for ( int i = 0; i < nShotsRemaining; i++ ) { Vector vecShotDir; // If they're at a dead stand-still, just hit them if ( flRange <= 0.1f ) { VectorSubtract( GetEnemy()->EyePosition(), vBasePos, vecShotDir ); Vector vecOffset; vecOffset.Random( -40.0f, 40.0f ); vecShotDir += vecOffset; VectorNormalize( vecShotDir ); } else { // Aim in a cone around them AimCloseToTargetButMiss( GetEnemy(), (3*12) * flRange, (10*12) * flRange, vBasePos, &vecShotDir ); } FireBulletsInfo_t info( 1, vBasePos, vecShotDir, VECTOR_CONE_PRECALCULATED, MAX_COORD_RANGE, m_iAmmoType ); info.m_iTracerFreq = 1; FireBullets( info ); } // We opt out of the rest of the function // FIXME: Should we emulate the below functionality and have half the bullets attempt to miss admirably? -- jdw return; } // Pop one at the player based on how fast he's going if ( m_nBurstHits < m_nMaxBurstHits ) { Vector vecDir; VectorSubtract( GetEnemy()->EyePosition(), vBasePos, vecDir ); Vector vecOffset; vecOffset.Random( -5.0f, 5.0f ); vecDir += vecOffset; VectorNormalize( vecDir ); FireBulletsInfo_t info( 1, vBasePos, vecDir, VECTOR_CONE_PRECALCULATED, MAX_COORD_RANGE, m_iAmmoType ); info.m_iTracerFreq = 1; FireBullets( info ); --nShotsRemaining; } // Fire half of the bullets within the circle of death, the other half at interesting things int i; int nFireInCircle = nShotsRemaining >> 1; nShotsRemaining -= nFireInCircle; for ( i = 0; i < nFireInCircle; ++i ) { ShootInsideCircleOfDeath( vBasePos, vecFireAtPosition ); } // Fire the rest of the bullets at objects around the enemy CBaseEntity *ppNearbyTargets[16]; int nActualTargets = BuildMissTargetList( 16, ppNearbyTargets ); // Randomly sort it... for ( i = 0; i < nActualTargets; ++i ) { int nSwap = random->RandomInt( 0, nActualTargets - 1 ); V_swap( ppNearbyTargets[i], ppNearbyTargets[nSwap] ); } // Just shoot where we're facing float flSinConeDegrees = sin( sk_helicopter_firingcone.GetFloat() * 0.5f * (3.14f / 180.0f) ); Vector vecSpread( flSinConeDegrees, flSinConeDegrees, flSinConeDegrees ); for ( i = nShotsRemaining; --i >= 0; ) { // Find something interesting around the enemy to shoot instead of just missing. if ( nActualTargets > i ) { Vector vecFireDirection; ppNearbyTargets[i]->CollisionProp()->RandomPointInBounds( Vector(.25, .25, .25), Vector(.75, .75, .75), &vecFireDirection ); vecFireDirection -= vBasePos; VectorNormalize( vecFireDirection ); // FIXME: Constrain to the firing cone? // I put in all the default arguments simply so I could guarantee the first shot of one of the bursts always hits FireBulletsInfo_t info( 1, vBasePos, vecFireDirection, VECTOR_CONE_PRECALCULATED, MAX_COORD_RANGE, m_iAmmoType ); info.m_iTracerFreq = 1; FireBullets( info ); } else { ShootInsideCircleOfDeath( vBasePos, vecFireAtPosition ); } } } //------------------------------------------------------------------------------ // Various states of the helicopter firing... //------------------------------------------------------------------------------ bool CNPC_AttackHelicopter::PoseGunTowardTargetDirection( const Vector &vTargetDir ) { Vector vecOut; VectorIRotate( vTargetDir, EntityToWorldTransform(), vecOut ); QAngle angles; VectorAngles(vecOut, angles); if (angles.y > 180) { angles.y = angles.y - 360; } else if (angles.y < -180) { angles.y = angles.y + 360; } if (angles.x > 180) { angles.x = angles.x - 360; } else if (angles.x < -180) { angles.x = angles.x + 360; } if ( ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) && !IsInSecondaryMode(BULLRUSH_MODE_SHOOT_IDLE_PLAYER) && GetEnemy()) { if ( GetEnemyVehicle() ) { angles.x = clamp( angles.x, -12.0f, 0.0f ); angles.y = clamp( angles.y, -10.0f, 10.0f ); } else { angles.x = clamp( angles.x, -10.0f, 10.0f ); angles.y = clamp( angles.y, -10.0f, 10.0f ); } } if (angles.x > m_angGun.x) { m_angGun.x = MIN( angles.x, m_angGun.x + 12 ); } if (angles.x < m_angGun.x) { m_angGun.x = MAX( angles.x, m_angGun.x - 12 ); } if (angles.y > m_angGun.y) { m_angGun.y = MIN( angles.y, m_angGun.y + 12 ); } if (angles.y < m_angGun.y) { m_angGun.y = MAX( angles.y, m_angGun.y - 12 ); } SetPoseParameter( m_poseWeapon_Pitch, -m_angGun.x ); SetPoseParameter( m_poseWeapon_Yaw, m_angGun.y ); return true; } //------------------------------------------------------------------------------ // Compute the enemy position (non-vehicle case) //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::ComputeFireAtPosition( Vector *pVecActualTargetPosition ) { // Deal with various leading behaviors... *pVecActualTargetPosition = m_vecTargetPosition; } //------------------------------------------------------------------------------ // Compute the enemy position (non-vehicle case) //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::ComputeVehicleFireAtPosition( Vector *pVecActualTargetPosition ) { CBaseEntity *pVehicle = GetEnemyVehicle(); // Make sure the circle of death doesn't move more than N units // This will cause the target to have to maintain a large enough speed *pVecActualTargetPosition = pVehicle->BodyTarget( GetAbsOrigin(), false ); // NDebugOverlay::Box( *pVecActualTargetPosition, // Vector(-m_flCircleOfDeathRadius, -m_flCircleOfDeathRadius, 0), // Vector(m_flCircleOfDeathRadius, m_flCircleOfDeathRadius, 0), // 0, 0, 255, false, 0.1f ); } //------------------------------------------------------------------------------ // Here's what we do when we're looking for a target //------------------------------------------------------------------------------ bool CNPC_AttackHelicopter::DoGunIdle( const Vector &vGunDir, const Vector &vTargetDir ) { // When bullrushing, skip the idle if ( ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) && ( IsInSecondaryMode( BULLRUSH_MODE_SHOOT_GUN ) || IsInSecondaryMode(BULLRUSH_MODE_SHOOT_IDLE_PLAYER) ) ) { EmitSound( "NPC_AttackHelicopter.ChargeGun" ); m_flChargeTime = gpGlobals->curtime + CHOPPER_GUN_CHARGE_TIME; m_nGunState = GUN_STATE_CHARGING; m_flCircleOfDeathRadius = CHOPPER_MAX_CIRCLE_OF_DEATH_RADIUS; return true; } // Can't continually fire.... if (m_flNextAttack > gpGlobals->curtime) return false; // Don't fire if we're too far away, or if the enemy isn't in front of us if (!GetEnemy()) return false; float flMaxDistSqr = GetMaxFiringDistance(); flMaxDistSqr *= flMaxDistSqr; float flDistSqr = WorldSpaceCenter().DistToSqr( GetEnemy()->WorldSpaceCenter() ); if (flDistSqr > flMaxDistSqr) return false; // If he's mostly within the cone, shoot away! float flChargeCone = sk_helicopter_firingcone.GetFloat() * 0.5f; if ( flChargeCone < 15.0f ) { flChargeCone = 15.0f; } float flCosConeDegrees = cos( flChargeCone * (3.14f / 180.0f) ); float fDotPr = DotProduct( vGunDir, vTargetDir ); if (fDotPr < flCosConeDegrees) return false; // Fast shooting doesn't charge up if( m_nShootingMode == SHOOT_MODE_FAST ) { m_flChargeTime = gpGlobals->curtime; m_nGunState = GUN_STATE_CHARGING; m_flAvoidMetric = 0.0f; m_vecLastAngVelocity.Init( 0, 0, 0 ); } else { EmitSound( "NPC_AttackHelicopter.ChargeGun" ); float flChargeTime = CHOPPER_GUN_CHARGE_TIME; float flVariance = flChargeTime * 0.1f; m_flChargeTime = gpGlobals->curtime + random->RandomFloat(flChargeTime - flVariance, flChargeTime + flVariance); m_nGunState = GUN_STATE_CHARGING; m_flAvoidMetric = 0.0f; m_vecLastAngVelocity.Init( 0, 0, 0 ); } return true; } //------------------------------------------------------------------------------ // How easy is the target to hit? //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::UpdateTargetHittability() { // This simply is a measure of how much juking is going on. // Along with how much steering is happening. if ( GetEnemyVehicle() ) { Vector vecVelocity; AngularImpulse vecAngVelocity; GetEnemyVehicle()->GetVelocity( &vecVelocity, &vecAngVelocity ); float flDist = fabs( vecAngVelocity.z - m_vecLastAngVelocity.z ); m_flAvoidMetric += flDist; m_vecLastAngVelocity = vecAngVelocity; } } //------------------------------------------------------------------------------ // Here's what we do when we're getting ready to fire //------------------------------------------------------------------------------ bool CNPC_AttackHelicopter::DoGunCharging( ) { // Update the target hittability, which will indicate how many hits we'll accept. UpdateTargetHittability(); if ( m_flChargeTime > gpGlobals->curtime ) return false; m_nGunState = GUN_STATE_FIRING; if ( HasSpawnFlags( SF_HELICOPTER_AGGRESSIVE ) ) { SetPauseState( PAUSE_AT_NEXT_LOS_POSITION ); } int nHitFactor = 1; switch( GetShootingMode() ) { case SHOOT_MODE_DEFAULT: case SHOOT_MODE_FAST: { int nBurstCount = sk_helicopter_burstcount.GetInt(); m_nRemainingBursts = random->RandomInt( nBurstCount, 2.0 * nBurstCount ); m_flIdleTimeDelay = 0.1f * ( m_nRemainingBursts - nBurstCount ); } break; case SHOOT_MODE_LONG_CYCLE: { m_nRemainingBursts = 60; m_flIdleTimeDelay = 0.0f; nHitFactor = 2; } break; case SHOOT_MODE_CONTINUOUS: if ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) { // We're relying on the special aiming behavior for bullrushing to just randomly deal damage m_nRemainingBursts = 1; m_flIdleTimeDelay = 0.0f; } else { m_nRemainingBursts = 0; m_flIdleTimeDelay = 0.0f; nHitFactor = 1000; } break; } if ( !GetEnemyVehicle() ) { m_nMaxBurstHits = !IsDeadlyShooting() ? random->RandomInt( 6, 9 ) : 200; m_nMaxNearShots = 10000; } else { Vector vecVelocity; GetEnemyVehicle()->GetVelocity( &vecVelocity, NULL ); float flSpeed = vecVelocity.Length(); flSpeed = clamp( flSpeed, 150.0f, 600.0f ); flSpeed = RemapVal( flSpeed, 150.0f, 600.0f, 0.0f, 1.0f ); float flAvoid = clamp( m_flAvoidMetric, 100.0f, 400.0f ); flAvoid = RemapVal( flAvoid, 100.0f, 400.0f, 0.0f, 1.0f ); float flTotal = 0.5f * ( flSpeed + flAvoid ); int nHitCount = (int)(RemapVal( flTotal, 0.0f, 1.0f, 7, -0.5 ) + 0.5f); int nMin = nHitCount >= 1 ? nHitCount - 1 : 0; m_nMaxBurstHits = random->RandomInt( nMin, nHitCount + 1 ); int nNearShots = (int)(RemapVal( flTotal, 0.0f, 1.0f, 70, 5 ) + 0.5f); int nMinNearShots = nNearShots >= 5 ? nNearShots - 5 : 0; m_nMaxNearShots = random->RandomInt( nMinNearShots, nNearShots + 5 ); // Set up the circle of death parameters at this point m_flCircleOfDeathRadius = SimpleSplineRemapVal( flTotal, 0.0f, 1.0f, CHOPPER_MIN_CIRCLE_OF_DEATH_RADIUS, CHOPPER_MAX_CIRCLE_OF_DEATH_RADIUS ); } m_nMaxBurstHits *= nHitFactor; m_nMaxNearShots *= nHitFactor; m_nBurstHits = 0; m_nNearShots = 0; return true; } //------------------------------------------------------------------------------ // Shoot where we're facing //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::ShootAtFacingDirection( const Vector &vBasePos, const Vector &vGunDir, bool bFirstShotAccurate ) { // Just shoot where we're facing float flSinConeDegrees = sin( sk_helicopter_firingcone.GetFloat() * 0.5f * (3.14f / 180.0f) ); Vector vecSpread( flSinConeDegrees, flSinConeDegrees, flSinConeDegrees ); int nShotCount = sk_helicopter_roundsperburst.GetInt(); if ( bFirstShotAccurate && GetEnemy() ) { // Check to see if the enemy is within his firing cone if ( GetEnemy() ) { // Find the closest point to the gunDir const Vector &vecCenter = GetEnemy()->WorldSpaceCenter(); float t; Vector vNearPoint; Vector vEndPoint; VectorMA( vBasePos, 1024.0f, vGunDir, vEndPoint ); CalcClosestPointOnLine( vecCenter, vBasePos, vEndPoint, vNearPoint, &t ); if ( t > 0.0f ) { Vector vecDelta; VectorSubtract( vecCenter, vBasePos, vecDelta ); float flDist = VectorNormalize( vecDelta ); float flPerpDist = vecCenter.DistTo( vNearPoint ); float flSinAngle = flPerpDist / flDist; if ( flSinAngle <= flSinConeDegrees ) { FireBulletsInfo_t info( 1, vBasePos, vecDelta, VECTOR_CONE_PRECALCULATED, 8192, m_iAmmoType ); info.m_iTracerFreq = 1; FireBullets( info ); --nShotCount; } } } } #ifdef HL2_EPISODIC if( GetEnemy() != NULL ) { CSoundEnt::InsertSound( SOUND_DANGER, GetEnemy()->WorldSpaceCenter(), 180.0f, 0.5f, this, SOUNDENT_CHANNEL_REPEATED_DANGER ); } #endif//HL2_EPISODIC DoMuzzleFlash(); FireBulletsInfo_t info( nShotCount, vBasePos, vGunDir, vecSpread, 8192, m_iAmmoType ); info.m_iTracerFreq = 1; FireBullets( info ); } //----------------------------------------------------------------------------- // Can we zap it? //----------------------------------------------------------------------------- bool CNPC_AttackHelicopter::IsValidZapTarget( CBaseEntity *pTarget ) { // Don't use the player or vehicle as a zap target, we'll do that ourselves. if ( pTarget->IsPlayer() || pTarget->GetServerVehicle() ) return false; if ( pTarget == this ) return false; if ( !pTarget->IsSolid() ) return false; Assert( pTarget ); IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT]; int count = pTarget->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) ); for ( int i = 0; i < count; i++ ) { int material = pList[i]->GetMaterialIndex(); const surfacedata_t *pSurfaceData = physprops->GetSurfaceData( material ); // Is flesh or metal? Go for it! if ( pSurfaceData->game.material == CHAR_TEX_METAL || pSurfaceData->game.material == CHAR_TEX_FLESH || pSurfaceData->game.material == CHAR_TEX_VENT || pSurfaceData->game.material == CHAR_TEX_GRATE || pSurfaceData->game.material == CHAR_TEX_COMPUTER || pSurfaceData->game.material == CHAR_TEX_BLOODYFLESH || pSurfaceData->game.material == CHAR_TEX_ALIENFLESH ) { return true; } } return false; } //------------------------------------------------------------------------------ // Effects //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::CreateZapBeam( const Vector &vecTargetPos ) { CEffectData data; data.m_nEntIndex = entindex(); data.m_nAttachmentIndex = 0; // m_nGunTipAttachment; data.m_vOrigin = vecTargetPos; data.m_flScale = 5; DispatchEffect( "TeslaZap", data ); } void CNPC_AttackHelicopter::CreateEntityZapEffect( CBaseEntity *pEnt ) { CEffectData data; data.m_nEntIndex = pEnt->entindex(); data.m_flMagnitude = 10; data.m_flScale = 1.0f; DispatchEffect( "TeslaHitboxes", data ); } //------------------------------------------------------------------------------ // Here's what we do when we *are* firing //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::FireElectricityGun( ) { if ( m_flNextAttack > gpGlobals->curtime ) return; EmitSound( "ReallyLoudSpark" ); CBaseEntity *ppEnts[256]; Vector vecCenter = WorldSpaceCenter(); float flRadius = 500.0f; vecCenter.z -= flRadius * 0.8f; int nEntCount = UTIL_EntitiesInSphere( ppEnts, 256, vecCenter, flRadius, 0 ); CBaseEntity *ppCandidates[256]; int nCandidateCount = 0; int i; for ( i = 0; i < nEntCount; i++ ) { if ( ppEnts[i] == NULL ) continue; // Zap metal or flesh things. if ( !IsValidZapTarget( ppEnts[i] ) ) continue; ppCandidates[ nCandidateCount++ ] = ppEnts[i]; } // First, put a bolt in front of the player, at random float flDist = 1024; if ( GetEnemy() ) { Vector vecDelta; Vector2DSubtract( GetEnemy()->WorldSpaceCenter().AsVector2D(), WorldSpaceCenter().AsVector2D(), vecDelta.AsVector2D() ); vecDelta.z = 0.0f; flDist = VectorNormalize( vecDelta ); Vector vecPerp( -vecDelta.y, vecDelta.x, 0.0f ); int nBoltCount = (int)(ClampSplineRemapVal( flDist, 256.0f, 1024.0f, 8, 0 ) + 0.5f); for ( i = 0; i < nBoltCount; ++i ) { Vector vecTargetPt = GetEnemy()->WorldSpaceCenter(); VectorMA( vecTargetPt, random->RandomFloat( flDist + 100, flDist + 500 ), vecDelta, vecTargetPt ); VectorMA( vecTargetPt, random->RandomFloat( -500, 500 ), vecPerp, vecTargetPt ); vecTargetPt.z += random->RandomFloat( -500, 500 ); CreateZapBeam( vecTargetPt ); } } // Next, choose the number of bolts... int nBoltCount = random->RandomInt( 8, 16 ); for ( i = 0; i < nBoltCount; ++i ) { if ( (nCandidateCount > 0) && random->RandomFloat( 0.0f, 1.0f ) < 0.6f ) { --nCandidateCount; Vector vecTarget; ppCandidates[nCandidateCount]->CollisionProp()->RandomPointInBounds( vec3_origin, Vector( 1, 1, 1 ), &vecTarget ); CreateZapBeam( vecTarget ); CreateEntityZapEffect( ppCandidates[nCandidateCount] ); } else { // Select random point *on* sphere Vector vecTargetPt; float flEffectRadius = random->RandomFloat( flRadius * 1.2, flRadius * 1.5f ); float flTheta = random->RandomFloat( 0.0f, 2.0f * M_PI ); float flPhi = random->RandomFloat( -0.5f * M_PI, 0.5f * M_PI ); vecTargetPt.x = cos(flTheta) * cos(flPhi); vecTargetPt.y = sin(flTheta) * cos(flPhi); vecTargetPt.z = sin(flPhi); vecTargetPt *= flEffectRadius; vecTargetPt += vecCenter; CreateZapBeam( vecTargetPt ); } } // Finally, put a bolt right at the player, at random float flHitRatio = ClampSplineRemapVal( flDist, 128.0f, 512.0f, 0.75f, 0.0f ); if ( random->RandomFloat( 0.0f, 1.0f ) < flHitRatio ) { if ( GetEnemyVehicle() ) { Vector vecTarget; GetEnemyVehicle()->CollisionProp()->RandomPointInBounds( vec3_origin, Vector( 1, 1, 1 ), &vecTarget ); CreateZapBeam( vecTarget ); CreateEntityZapEffect( GetEnemyVehicle() ); CTakeDamageInfo info( this, this, 5, DMG_SHOCK ); GetEnemy()->TakeDamage( info ); } else if ( GetEnemy() ) { Vector vecTarget; GetEnemy()->CollisionProp()->RandomPointInBounds( vec3_origin, Vector( 1, 1, 1 ), &vecTarget ); CreateZapBeam( vecTarget ); CTakeDamageInfo info( this, this, 5, DMG_SHOCK ); GetEnemy()->TakeDamage( info ); } } m_flNextAttack = gpGlobals->curtime + random->RandomFloat( 0.3f, 1.0f ); } //------------------------------------------------------------------------------ // Here's what we do when we *are* firing //------------------------------------------------------------------------------ #define INTERVAL_BETWEEN_HITS 4 bool CNPC_AttackHelicopter::DoGunFiring( const Vector &vBasePos, const Vector &vGunDir, const Vector &vecFireAtPosition ) { CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); float flVolume = controller.SoundGetVolume( m_pGunFiringSound ); if ( flVolume != 1.0f ) { controller.SoundChangeVolume( m_pGunFiringSound, 1.0, 0.01f ); } if ( ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) && ( IsInSecondaryMode( BULLRUSH_MODE_SHOOT_GUN ) ) ) { ShootAtFacingDirection( vBasePos, vGunDir, m_nRemainingBursts == 0 ); } else if ( GetEnemyVehicle() ) { ShootAtVehicle( vBasePos, vecFireAtPosition ); } else if ( GetEnemy() && GetEnemy()->IsPlayer() ) { if ( !IsDeadlyShooting() ) { ShootAtPlayer( vBasePos, vGunDir ); } else { ShootAtFacingDirection( vBasePos, vGunDir, true ); } } else { ShootAtFacingDirection( vBasePos, vGunDir, false ); } if ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) { if ( --m_nRemainingBursts < 0 ) { m_nRemainingBursts = INTERVAL_BETWEEN_HITS; } return true; } --m_nRemainingBursts; if ( m_nRemainingBursts > 0 ) return true; controller.SoundChangeVolume( m_pGunFiringSound, 0.0, 0.01f ); float flIdleTime = CHOPPER_GUN_IDLE_TIME; float flVariance = flIdleTime * 0.1f; m_flNextAttack = gpGlobals->curtime + m_flIdleTimeDelay + random->RandomFloat(flIdleTime - flVariance, flIdleTime + flVariance); m_nGunState = GUN_STATE_IDLE; SetPauseState( PAUSE_NO_PAUSE ); return true; } //------------------------------------------------------------------------------ // Is it "fair" to drop this bomb? //------------------------------------------------------------------------------ #define MIN_BOMB_DISTANCE_SQR ( 600.0f * 600.0f ) bool CNPC_AttackHelicopter::IsBombDropFair( const Vector &vecBombStartPos, const Vector &vecBombVelocity ) { if ( (m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE) && IsInSecondaryMode( BULLRUSH_MODE_SHOOT_IDLE_PLAYER ) ) return true; // Can happen if you're noclipping around if ( !GetEnemy() ) return false; // If the player is moving slowly, it's fair if ( GetEnemy()->GetSmoothedVelocity().LengthSqr() < ( CHOPPER_SLOW_BOMB_SPEED * CHOPPER_SLOW_BOMB_SPEED ) ) return true; // Skip out if we're right above or behind the player.. that's unfair if ( GetEnemy() && GetEnemy()->IsPlayer() ) { // How much time will it take to fall? // dx = 0.5 * a * t^2 Vector vecTarget = GetEnemy()->BodyTarget( GetAbsOrigin(), false ); float dz = vecBombStartPos.z - vecTarget.z; float dt = (dz > 0.0f) ? sqrt( 2 * dz / sv_gravity.GetFloat() ) : 0.0f; // Where will the enemy be in that time? Vector vecEnemyVel = GetEnemy()->GetSmoothedVelocity(); VectorMA( vecTarget, dt, vecEnemyVel, vecTarget ); // Where will the bomb be in that time? Vector vecBomb; VectorMA( vecBombStartPos, dt, vecBombVelocity, vecBomb ); float flEnemySpeed = vecEnemyVel.LengthSqr(); flEnemySpeed = clamp( flEnemySpeed, 200.0f, 500.0f ); float flDistFactorSq = RemapVal( flEnemySpeed, 200.0f, 500.0f, 0.3f, 1.0f ); flDistFactorSq *= flDistFactorSq; // If it's too close, then we're not doing it. if ( vecBomb.AsVector2D().DistToSqr( vecTarget.AsVector2D() ) < (flDistFactorSq * MIN_BOMB_DISTANCE_SQR) ) return false; } return true; } //----------------------------------------------------------------------------- // Purpose: Create the bomb entity and set it up // Input : &vecPos - Position to spawn at // &vecVelocity - velocity to spawn with //----------------------------------------------------------------------------- CGrenadeHelicopter *CNPC_AttackHelicopter::SpawnBombEntity( const Vector &vecPos, const Vector &vecVelocity ) { // Create the grenade and set it up CGrenadeHelicopter *pGrenade = static_cast(CreateEntityByName( "grenade_helicopter" )); pGrenade->SetAbsOrigin( vecPos ); pGrenade->SetOwnerEntity( this ); pGrenade->SetThrower( this ); pGrenade->SetAbsVelocity( vecVelocity ); DispatchSpawn( pGrenade ); pGrenade->SetExplodeOnContact( m_bBombsExplodeOnContact ); #ifdef HL2_EPISODIC // Disable collisions with the owner's bone followers while we drop physfollower_t *pFollower = m_BoneFollowerManager.GetBoneFollower( 0 ); if ( pFollower ) { CBaseEntity *pBoneFollower = pFollower->hFollower; PhysDisableEntityCollisions( pBoneFollower, pGrenade ); pGrenade->SetCollisionObject( pBoneFollower ); } #endif // HL2_EPISODIC return pGrenade; } //------------------------------------------------------------------------------ // Actually drops the bomb //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::CreateBomb( bool bCheckForFairness, Vector *pVecVelocity, bool bMegaBomb ) { if ( m_bBombingSuppressed ) return; Vector vTipPos; GetAttachment( m_nBombAttachment, vTipPos ); if ( !CBombSuppressor::CanBomb( vTipPos ) ) return; // Compute velocity Vector vecActualVelocity; if ( !pVecVelocity ) { Vector vecAcross; vecActualVelocity = GetAbsVelocity(); CrossProduct( vecActualVelocity, Vector( 0, 0, 1 ), vecAcross ); VectorNormalize( vecAcross ); vecAcross *= random->RandomFloat( 10.0f, 30.0f ); vecAcross *= random->RandomFloat( 0.0f, 1.0f ) < 0.5f ? 1.0f : -1.0f; // Blat out z component of velocity if it's moving upward.... if ( vecActualVelocity.z > 0 ) { vecActualVelocity.z = 0.0f; } vecActualVelocity += vecAcross; } else { vecActualVelocity = *pVecVelocity; } if ( bCheckForFairness ) { if ( !IsBombDropFair( vTipPos, vecActualVelocity ) ) return; } AddGesture( (Activity)ACT_HELICOPTER_DROP_BOMB ); EmitSound( "NPC_AttackHelicopter.DropMine" ); // Make the bomb and send it off CGrenadeHelicopter *pGrenade = SpawnBombEntity( vTipPos, vecActualVelocity ); if ( pGrenade && bMegaBomb ) { pGrenade->AddSpawnFlags( SF_GRENADE_HELICOPTER_MEGABOMB ); } } //------------------------------------------------------------------------------ // Drop a bomb at a particular location //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InputDropBomb( inputdata_t &inputdata ) { if ( m_flInputDropBombTime > gpGlobals->curtime ) return; // Prevent two triggers from being hit the same frame m_flInputDropBombTime = gpGlobals->curtime + 0.01f; CreateBomb( ); // If we're in the middle of a bomb dropping schedule, wait to drop another bomb. if ( ShouldDropBombs() ) { m_flNextAttack = gpGlobals->curtime + 0.5f + random->RandomFloat( 0.3f, 0.6f ); } } //------------------------------------------------------------------------------ // Drops a bomb straight downwards //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InputDropBombStraightDown( inputdata_t &inputdata ) { if ( m_flInputDropBombTime > gpGlobals->curtime ) return; // Prevent two triggers from being hit the same frame m_flInputDropBombTime = gpGlobals->curtime + 0.01f; Vector vTipPos; GetAttachment( m_nBombAttachment, vTipPos ); // Make the bomb drop straight down SpawnBombEntity( vTipPos, vec3_origin ); // If we're in the middle of a bomb dropping schedule, wait to drop another bomb. if ( ShouldDropBombs() ) { m_flNextAttack = gpGlobals->curtime + 0.5f + random->RandomFloat( 0.3f, 0.6f ); } } //------------------------------------------------------------------------------ // Drop a bomb at a particular location //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InputDropBombAtTargetInternal( inputdata_t &inputdata, bool bCheckFairness ) { if ( m_flInputDropBombTime > gpGlobals->curtime ) return; // Prevent two triggers from being hit the same frame m_flInputDropBombTime = gpGlobals->curtime + 0.01f; // Find our specified target string_t strBombTarget = MAKE_STRING( inputdata.value.String() ); CBaseEntity *pBombEnt = gEntList.FindEntityByName( NULL, strBombTarget ); if ( pBombEnt == NULL ) { Warning( "%s: Could not find bomb drop target '%s'!\n", GetClassname(), STRING( strBombTarget ) ); return; } Vector vTipPos; GetAttachment( m_nBombAttachment, vTipPos ); // Compute the time it would take to fall to the target Vector vecTarget = pBombEnt->BodyTarget( GetAbsOrigin(), false ); float dz = vTipPos.z - vecTarget.z; if ( dz <= 0.0f ) { Warning("Bomb target %s is above the chopper!\n", STRING( strBombTarget ) ); return; } float dt = sqrt( 2 * dz / sv_gravity.GetFloat() ); // Compute the velocity that would make it happen Vector vecVelocity; VectorSubtract( vecTarget, vTipPos, vecVelocity ); vecVelocity /= dt; vecVelocity.z = 0.0f; if ( bCheckFairness ) { if ( !IsBombDropFair( vTipPos, vecVelocity ) ) return; } // Make the bomb and send it off SpawnBombEntity( vTipPos, vecVelocity ); // If we're in the middle of a bomb dropping schedule, wait to drop another bomb. if ( ShouldDropBombs() ) { m_flNextAttack = gpGlobals->curtime + 1.5f + random->RandomFloat( 0.1f, 0.2f ); } } //------------------------------------------------------------------------------ // Drop a bomb at a particular location //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InputDropBombAtTargetAlways( inputdata_t &inputdata ) { InputDropBombAtTargetInternal( inputdata, false ); } //------------------------------------------------------------------------------ // Drop a bomb at a particular location //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InputDropBombAtTarget( inputdata_t &inputdata ) { InputDropBombAtTargetInternal( inputdata, true ); } //------------------------------------------------------------------------------ // Drop a bomb at a particular location //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::InputDropBombDelay( inputdata_t &inputdata ) { m_flInputDropBombTime = gpGlobals->curtime + inputdata.value.Float(); if ( ShouldDropBombs() ) { m_flNextAttack = m_flInputDropBombTime; } } //------------------------------------------------------------------------------ // Drop those bombs! //------------------------------------------------------------------------------ #define MAX_BULLRUSH_BOMB_DISTANCE_SQR ( 3072.0f * 3072.0f ) void CNPC_AttackHelicopter::DropBombs( ) { // Can't continually fire.... if (m_flNextAttack > gpGlobals->curtime) return; // Otherwise, behave as normal. if ( m_nAttackMode != ATTACK_MODE_BULLRUSH_VEHICLE ) { if ( GetEnemy() && GetEnemy()->IsPlayer() ) { if ( GetEnemy()->GetSmoothedVelocity().LengthSqr() > ( CHOPPER_SLOW_BOMB_SPEED * CHOPPER_SLOW_BOMB_SPEED ) ) { // Don't drop bombs if you are behind the player, unless the player is moving slowly float flLeadingDistSq = GetLeadingDistance() * 0.75f; flLeadingDistSq *= flLeadingDistSq; Vector vecPoint; ClosestPointToCurrentPath( &vecPoint ); if ( vecPoint.AsVector2D().DistToSqr( GetDesiredPosition().AsVector2D() ) > flLeadingDistSq ) return; } } } else { // Skip out if we're bullrushing but too far from the player if ( GetEnemy() ) { if ( GetEnemy()->GetAbsOrigin().AsVector2D().DistToSqr( GetAbsOrigin().AsVector2D() ) > MAX_BULLRUSH_BOMB_DISTANCE_SQR ) return; } } CreateBomb( ); m_flNextAttack = gpGlobals->curtime + 0.5f + random->RandomFloat( 0.3f, 0.6f ); if ( (m_nAttackMode != ATTACK_MODE_BULLRUSH_VEHICLE) ) { if ( --m_nGrenadeCount <= 0 ) { m_nGrenadeCount = CHOPPER_BOMB_DROP_COUNT; m_flNextAttack += random->RandomFloat( 1.5f, 3.0f ); } } } //------------------------------------------------------------------------------ // Should we drop those bombs? //------------------------------------------------------------------------------ #define BOMB_GRACE_PERIOD 1.5f #define BOMB_MIN_SPEED 150.0 bool CNPC_AttackHelicopter::ShouldDropBombs( void ) { if ( IsCarpetBombing() ) return true; if ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) { // Distance determines whether or not we should do this if ((m_nSecondaryMode == BULLRUSH_MODE_SHOOT_IDLE_PLAYER) && (SecondaryModeTime() >= BULLRUSH_IDLE_PLAYER_FIRE_TIME)) return ShouldBombIdlePlayer(); return (( m_nSecondaryMode == BULLRUSH_MODE_DROP_BOMBS_FIXED_SPEED ) || ( m_nSecondaryMode == BULLRUSH_MODE_DROP_BOMBS_FOLLOW_PLAYER )); } if (!IsLeading() || !GetEnemyVehicle()) return false; if (( m_nAttackMode != ATTACK_MODE_BOMB_VEHICLE ) && ( m_nAttackMode != ATTACK_MODE_ALWAYS_LEAD_VEHICLE )) return false; if ( m_nGunState != GUN_STATE_IDLE ) return false; // This is for bombing. If you get hit, give a grace period to get back to speed float flSpeedSqr = GetEnemy()->GetSmoothedVelocity().LengthSqr(); if ( flSpeedSqr >= BOMB_MIN_SPEED * BOMB_MIN_SPEED ) { m_flLastFastTime = gpGlobals->curtime; } else { if ( ( gpGlobals->curtime - m_flLastFastTime ) < BOMB_GRACE_PERIOD ) return false; } float flSpeedAlongPath = TargetSpeedAlongPath(); if ( m_nAttackMode == ATTACK_MODE_BOMB_VEHICLE ) return ( flSpeedAlongPath > -BOMB_MIN_SPEED ); // This is for ALWAYS_LEAD if ( fabs(flSpeedAlongPath) < 50.0f ) return false; float flLeadingDist = ComputeDistanceToLeadingPosition( ); flLeadingDist = GetLeadingDistance() - flLeadingDist; if ( flSpeedAlongPath < 0.0f ) { return flLeadingDist < 300.0f; } else { return flLeadingDist > -300.0f; } } //------------------------------------------------------------------------------ // Different bomb-dropping behavior //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::BullrushBombs( ) { if ( gpGlobals->curtime < m_flNextBullrushBombTime ) return; if ( m_nBullrushBombMode & 0x1 ) { CreateBomb( false, NULL, true ); } else { Vector vecAcross; Vector vecVelocity = GetAbsVelocity(); CrossProduct( vecVelocity, Vector( 0, 0, 1 ), vecAcross ); VectorNormalize( vecAcross ); vecAcross *= random->RandomFloat( 300.0f, 500.0f ); // Blat out z component of velocity if it's moving upward.... if ( vecVelocity.z > 0 ) { vecVelocity.z = 0.0f; } vecVelocity += vecAcross; CreateBomb( false, &vecVelocity, true ); VectorMA( vecVelocity, -2.0f, vecAcross, vecVelocity ); CreateBomb( false, &vecVelocity, true ); } m_nBullrushBombMode = !m_nBullrushBombMode; m_flNextBullrushBombTime = gpGlobals->curtime + 0.2f; } //----------------------------------------------------------------------------- // Purpose: Turn the gun off //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::InputGunOff( inputdata_t &inputdata ) { BaseClass::InputGunOff( inputdata ); if ( m_pGunFiringSound ) { CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); controller.SoundChangeVolume( m_pGunFiringSound, 0.0, 0.01f ); } } //------------------------------------------------------------------------------ // Fire that gun baby! //------------------------------------------------------------------------------ bool CNPC_AttackHelicopter::FireGun( void ) { // Do the test electricity gun if ( HasSpawnFlags(SF_HELICOPTER_ELECTRICAL_DRONE) ) { FireElectricityGun( ); return true; } // HACK: CBaseHelicopter ignores this, and fire forever at the last place it saw the player. Why? if (( m_nGunState == GUN_STATE_IDLE ) && ( m_nAttackMode != ATTACK_MODE_BULLRUSH_VEHICLE ) && !IsCarpetBombing() ) { if ( (m_flLastSeen + 1 <= gpGlobals->curtime) || (m_flPrevSeen + m_flGracePeriod > gpGlobals->curtime) ) return false; } if ( IsCarpetBombing() ) { BullrushBombs(); return false; } if ( ShouldDropBombs() ) { DropBombs( ); return false; } // Drop those bullrush bombs when shooting... if ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) { if ( IsInSecondaryMode( BULLRUSH_MODE_MEGA_BOMB ) ) { BullrushBombs( ); return false; } // Don't fire if we're bullrushing and we're getting distance if ( !IsInSecondaryMode( BULLRUSH_MODE_SHOOT_GUN ) && !IsInSecondaryMode(BULLRUSH_MODE_SHOOT_IDLE_PLAYER) ) return false; // If we're in the grace period on this mode, then don't fire if ( IsInSecondaryMode( BULLRUSH_MODE_SHOOT_IDLE_PLAYER ) && (SecondaryModeTime() < BULLRUSH_IDLE_PLAYER_FIRE_TIME) ) { // Stop our gun sound if ( m_nGunState != GUN_STATE_IDLE ) { ShutdownGunDuringBullrush(); } return false; } } // Get gun attachment points Vector vBasePos; GetAttachment( m_nGunBaseAttachment, vBasePos ); // Aim perfectly while idle, but after charging, the gun don't move so fast. Vector vecFireAtPosition; if ( !GetEnemyVehicle() || (m_nGunState == GUN_STATE_IDLE) ) { ComputeFireAtPosition( &vecFireAtPosition ); } else { ComputeVehicleFireAtPosition( &vecFireAtPosition ); } Vector vTargetDir = vecFireAtPosition - vBasePos; VectorNormalize( vTargetDir ); // Makes the model of the gun point to where we're aiming. if ( !PoseGunTowardTargetDirection( vTargetDir ) ) return false; // Are we charging? if ( m_nGunState == GUN_STATE_CHARGING ) { if ( !DoGunCharging( ) ) return false; } Vector vTipPos; GetAttachment( m_nGunTipAttachment, vTipPos ); Vector vGunDir = vTipPos - vBasePos; VectorNormalize( vGunDir ); // Are we firing? if ( m_nGunState == GUN_STATE_FIRING ) { return DoGunFiring( vTipPos, vGunDir, vecFireAtPosition ); } return DoGunIdle( vGunDir, vTargetDir ); } //----------------------------------------------------------------------------- // Should we trigger a damage effect? //----------------------------------------------------------------------------- inline bool CNPC_AttackHelicopter::ShouldTriggerDamageEffect( int nPrevHealth, int nEffectCount ) const { int nPrevRange = (int)( ((float)nPrevHealth / (float)GetMaxHealth()) * nEffectCount ); int nRange = (int)( ((float)GetHealth() / (float)GetMaxHealth()) * nEffectCount ); return ( nRange != nPrevRange ); } //----------------------------------------------------------------------------- // Add a smoke trail since we've taken more damage //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::AddSmokeTrail( const Vector &vecPos ) { if ( m_nSmokeTrailCount == MAX_SMOKE_TRAILS ) return; // See if there's an attachment for this smoke trail int nAttachment = LookupAttachment( UTIL_VarArgs( "damage%d", m_nSmokeTrailCount ) ); if ( nAttachment == 0 ) return; // The final smoke trail is a flaming engine if ( m_nSmokeTrailCount == 0 || m_nSmokeTrailCount % 2 ) { CFireTrail *pFireTrail = CFireTrail::CreateFireTrail(); if ( pFireTrail == NULL ) return; m_hSmokeTrail[m_nSmokeTrailCount] = pFireTrail; pFireTrail->FollowEntity( this, UTIL_VarArgs( "damage%d", m_nSmokeTrailCount ) ); pFireTrail->SetParent( this, nAttachment ); pFireTrail->SetLocalOrigin( vec3_origin ); pFireTrail->SetMoveType( MOVETYPE_NONE ); pFireTrail->SetLifetime( -1 ); } else { SmokeTrail *pSmokeTrail = SmokeTrail::CreateSmokeTrail(); if( !pSmokeTrail ) return; m_hSmokeTrail[m_nSmokeTrailCount] = pSmokeTrail; pSmokeTrail->m_SpawnRate = 48; pSmokeTrail->m_ParticleLifetime = 0.5f; pSmokeTrail->m_StartColor.Init(0.15, 0.15, 0.15); pSmokeTrail->m_EndColor.Init(0.0, 0.0, 0.0); pSmokeTrail->m_StartSize = 24; pSmokeTrail->m_EndSize = 80; pSmokeTrail->m_SpawnRadius = 8; pSmokeTrail->m_Opacity = 0.2; pSmokeTrail->m_MinSpeed = 16; pSmokeTrail->m_MaxSpeed = 64; pSmokeTrail->SetLifetime(-1); pSmokeTrail->SetParent( this, nAttachment ); pSmokeTrail->SetLocalOrigin( vec3_origin ); pSmokeTrail->SetMoveType( MOVETYPE_NONE ); } m_nSmokeTrailCount++; } //----------------------------------------------------------------------------- // Destroy all smoke trails //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::DestroySmokeTrails() { for ( int i = m_nSmokeTrailCount; --i >= 0; ) { UTIL_Remove( m_hSmokeTrail[i] ); m_hSmokeTrail[i] = NULL; } } //----------------------------------------------------------------------------- // Purpose: // Input : &vecChunkPos - //----------------------------------------------------------------------------- void Chopper_CreateChunk( CBaseEntity *pChopper, const Vector &vecChunkPos, const QAngle &vecChunkAngles, const char *pszChunkName, bool bSmall ) { // Drop a flaming, smoking chunk. CGib *pChunk = CREATE_ENTITY( CGib, "gib" ); pChunk->Spawn( pszChunkName ); pChunk->SetBloodColor( DONT_BLEED ); pChunk->SetAbsOrigin( vecChunkPos ); pChunk->SetAbsAngles( vecChunkAngles ); pChunk->SetOwnerEntity( pChopper ); if ( bSmall ) { pChunk->m_lifeTime = random->RandomFloat( 0.5f, 1.0f ); pChunk->SetSolidFlags( FSOLID_NOT_SOLID ); pChunk->SetSolid( SOLID_BBOX ); pChunk->AddEffects( EF_NODRAW ); pChunk->SetGravity( UTIL_ScaleForGravity( 400 ) ); } else { pChunk->m_lifeTime = 5.0f; } pChunk->SetCollisionGroup( COLLISION_GROUP_DEBRIS ); // Set the velocity Vector vecVelocity; AngularImpulse angImpulse; QAngle angles; angles.x = random->RandomFloat( -70, 20 ); angles.y = random->RandomFloat( 0, 360 ); angles.z = 0.0f; AngleVectors( angles, &vecVelocity ); vecVelocity *= random->RandomFloat( 550, 800 ); vecVelocity += pChopper->GetAbsVelocity(); angImpulse = RandomAngularImpulse( -180, 180 ); pChunk->SetAbsVelocity( vecVelocity ); if ( bSmall == false ) { IPhysicsObject *pPhysicsObject = pChunk->VPhysicsInitNormal( SOLID_VPHYSICS, pChunk->GetSolidFlags(), false ); if ( pPhysicsObject ) { pPhysicsObject->EnableMotion( true ); pPhysicsObject->SetVelocity(&vecVelocity, &angImpulse ); } } CFireTrail *pFireTrail = CFireTrail::CreateFireTrail(); if ( pFireTrail == NULL ) return; pFireTrail->FollowEntity( pChunk, "" ); pFireTrail->SetParent( pChunk, 0 ); pFireTrail->SetLocalOrigin( vec3_origin ); pFireTrail->SetMoveType( MOVETYPE_NONE ); pFireTrail->SetLifetime( pChunk->m_lifeTime ); } //------------------------------------------------------------------------------ // Pow! //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::ExplodeAndThrowChunk( const Vector &vecExplosionPos ) { CEffectData data; data.m_vOrigin = vecExplosionPos; DispatchEffect( "HelicopterMegaBomb", data ); EmitSound( "BaseExplosionEffect.Sound" ); UTIL_ScreenShake( vecExplosionPos, 25.0, 150.0, 1.0, 750.0f, SHAKE_START ); if(GetCrashPoint() != NULL) { // Make it clear that I'm done for. ExplosionCreate( vecExplosionPos, QAngle(0,0,1), this, 100, 128, false ); } if ( random->RandomInt( 0, 4 ) ) { for ( int i = 0; i < 2; i++ ) { Chopper_CreateChunk( this, vecExplosionPos, RandomAngle(0, 360), g_PropDataSystem.GetRandomChunkModel( "MetalChunks" ), true ); } } else { Chopper_CreateChunk( this, vecExplosionPos, RandomAngle(0, 360), s_pChunkModelName[random->RandomInt( 0, CHOPPER_MAX_SMALL_CHUNKS - 1 )], false ); } } //----------------------------------------------------------------------------- // Drop a corpse! //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::DropCorpse( int nDamage ) { // Don't drop another corpse if the next guy's not out on the gun yet if ( m_flLastCorpseFall > gpGlobals->curtime ) return; // Clamp damage to prevent ridiculous ragdoll velocity if( nDamage > 250.0f ) nDamage = 250.0f; m_flLastCorpseFall = gpGlobals->curtime + 3.0; // Spawn a ragdoll combine guard float forceScale = nDamage * 75 * 4; Vector vecForceVector = RandomVector(-1,1); vecForceVector.z = 0.5; vecForceVector *= forceScale; CBaseEntity *pGib = CreateRagGib( "models/combine_soldier.mdl", GetAbsOrigin(), GetAbsAngles(), vecForceVector ); if ( pGib ) { pGib->SetOwnerEntity( this ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr ) { // Take no damage from trace attacks unless it's blast damage. RadiusDamage() sometimes calls // TraceAttack() as a means for delivering blast damage. Usually when the explosive penetrates // the target. (RPG missiles do this sometimes). if ( ( info.GetDamageType() & DMG_AIRBOAT ) || ( info.GetInflictor()->Classify() == CLASS_MISSILE ) || ( info.GetAttacker()->Classify() == CLASS_MISSILE ) ) { BaseClass::BaseClass::TraceAttack( info, vecDir, ptr ); } } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- int CNPC_AttackHelicopter::OnTakeDamage( const CTakeDamageInfo &info ) { // We don't take blast damage from anything but the airboat or missiles (or myself!) if( info.GetInflictor() != this ) { if ( ( ( info.GetDamageType() & DMG_AIRBOAT ) == 0 ) && ( info.GetInflictor()->Classify() != CLASS_MISSILE ) && ( info.GetAttacker()->Classify() != CLASS_MISSILE ) ) return 0; } if ( m_bIndestructible ) { if ( GetHealth() < info.GetDamage() ) return 0; } // helicopter takes extra damage from its own grenades CGrenadeHelicopter *pGren = dynamic_cast(info.GetInflictor()); if ( pGren && info.GetAttacker() && info.GetAttacker()->IsPlayer() ) { CTakeDamageInfo fudgedInfo = info; float damage; if( g_pGameRules->IsSkillLevel(SKILL_EASY) ) { damage = GetMaxHealth() / sk_helicopter_num_bombs1.GetFloat(); } else if( g_pGameRules->IsSkillLevel(SKILL_HARD) ) { damage = GetMaxHealth() / sk_helicopter_num_bombs3.GetFloat(); } else // Medium, or unspecified { damage = GetMaxHealth() / sk_helicopter_num_bombs2.GetFloat(); } damage = ceilf( damage ); fudgedInfo.SetDamage( damage ); fudgedInfo.SetMaxDamage( damage ); return BaseClass::OnTakeDamage( fudgedInfo ); } return BaseClass::OnTakeDamage( info ); } //----------------------------------------------------------------------------- // Purpose: Take damage from trace attacks if they hit the gunner //----------------------------------------------------------------------------- int CNPC_AttackHelicopter::OnTakeDamage_Alive( const CTakeDamageInfo &info ) { int nPrevHealth = GetHealth(); if ( ( info.GetInflictor() != NULL ) && ( info.GetInflictor()->GetOwnerEntity() != NULL ) && ( info.GetInflictor()->GetOwnerEntity() == this ) ) { // Don't take damage from my own bombs. (Unless the player grabbed them and threw them back) return 0; } // Chain int nRetVal = BaseClass::OnTakeDamage_Alive( info ); if( info.GetDamageType() & DMG_BLAST ) { // Apply a force push that makes us look like we're reacting to the damage Vector damageDir = info.GetDamageForce(); VectorNormalize( damageDir ); ApplyAbsVelocityImpulse( damageDir * 500.0f ); // Knock the helicopter off of the level, too. Vector vecRight, vecForce; float flDot; GetVectors( NULL, &vecRight, NULL ); vecForce = info.GetDamageForce(); VectorNormalize( vecForce ); flDot = DotProduct( vecForce, vecRight ); m_flGoalRollDmg = random->RandomFloat( 10, 30 ); if( flDot <= 0.0f ) { // Missile hit the right side. m_flGoalRollDmg *= -1; } } // Spawn damage effects if ( nPrevHealth != GetHealth() ) { // Give the badly damaged call to say we're going to mega bomb soon if ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) { if (( nPrevHealth > m_flNextMegaBombHealth ) && (GetHealth() <= m_flNextMegaBombHealth) ) { EmitSound( "NPC_AttackHelicopter.BadlyDamagedAlert" ); } } if ( ShouldTriggerDamageEffect( nPrevHealth, MAX_SMOKE_TRAILS ) ) { AddSmokeTrail( info.GetDamagePosition() ); } if ( ShouldTriggerDamageEffect( nPrevHealth, MAX_CORPSES ) ) { if ( nPrevHealth != GetMaxHealth() ) { DropCorpse( info.GetDamage() ); } } if ( ShouldTriggerDamageEffect( nPrevHealth, MAX_EXPLOSIONS ) ) { ExplodeAndThrowChunk( info.GetDamagePosition() ); } int nPrevPercent = (int)(100.0f * nPrevHealth / GetMaxHealth()); int nCurrPercent = (int)(100.0f * GetHealth() / GetMaxHealth()); if (( (nPrevPercent + 9) / 10 ) != ( (nCurrPercent + 9) / 10 )) { m_OnHealthChanged.Set( nCurrPercent, this, this ); } } return nRetVal; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void Chopper_BecomeChunks( CBaseEntity *pChopper ) { QAngle vecChunkAngles = pChopper->GetAbsAngles(); Vector vecForward, vecUp; pChopper->GetVectors( &vecForward, NULL, &vecUp ); #ifdef HL2_EPISODIC CNPC_AttackHelicopter *pAttackHelicopter; pAttackHelicopter = dynamic_cast(pChopper); if( pAttackHelicopter != NULL ) { // New for EP2, we may be tailspinning, (crashing) and playing an animation that is spinning // our root bone, which means our model is not facing the way our entity is facing. So we have // to do some attachment point math to get the proper angles to use for computing the relative // positions of the gibs. The attachment points called DAMAGE0 is properly oriented and attached // to the chopper body so we can use its angles. int iAttach = pAttackHelicopter->LookupAttachment( "damage0" ); Vector vecAttachPos; if( iAttach > -1 ) { pAttackHelicopter->GetAttachment(iAttach, vecAttachPos, vecChunkAngles ); AngleVectors( vecChunkAngles, &vecForward, NULL, &vecUp ); } } #endif//HL2_EPISODIC Vector vecChunkPos = pChopper->GetAbsOrigin(); Vector vecRight(0,0,0); if( hl2_episodic.GetBool() ) { // We need to get a right hand vector to toss the cockpit and tail pieces // so their motion looks like a continuation of the tailspin animation // that the chopper plays before crashing. pChopper->GetVectors( NULL, &vecRight, NULL ); } // Body CHelicopterChunk *pBodyChunk = CHelicopterChunk::CreateHelicopterChunk( vecChunkPos, vecChunkAngles, pChopper->GetAbsVelocity(), HELICOPTER_CHUNK_BODY, CHUNK_BODY ); Chopper_CreateChunk( pChopper, vecChunkPos, RandomAngle( 0, 360 ), s_pChunkModelName[random->RandomInt( 0, CHOPPER_MAX_CHUNKS - 1 )], false ); vecChunkPos = pChopper->GetAbsOrigin() + ( vecForward * 100.0f ) + ( vecUp * -38.0f ); // Cockpit CHelicopterChunk *pCockpitChunk = CHelicopterChunk::CreateHelicopterChunk( vecChunkPos, vecChunkAngles, pChopper->GetAbsVelocity() + vecRight * -800.0f, HELICOPTER_CHUNK_COCKPIT, CHUNK_COCKPIT ); Chopper_CreateChunk( pChopper, vecChunkPos, RandomAngle( 0, 360 ), s_pChunkModelName[random->RandomInt( 0, CHOPPER_MAX_CHUNKS - 1 )], false ); pCockpitChunk->m_hMaster = pBodyChunk; vecChunkPos = pChopper->GetAbsOrigin() + ( vecForward * -175.0f ); // Tail CHelicopterChunk *pTailChunk = CHelicopterChunk::CreateHelicopterChunk( vecChunkPos, vecChunkAngles, pChopper->GetAbsVelocity() + vecRight * 800.0f, HELICOPTER_CHUNK_TAIL, CHUNK_TAIL ); Chopper_CreateChunk( pChopper, vecChunkPos, RandomAngle( 0, 360 ), s_pChunkModelName[random->RandomInt( 0, CHOPPER_MAX_CHUNKS - 1 )], false ); pTailChunk->m_hMaster = pBodyChunk; // Constrain all the pieces together loosely IPhysicsObject *pBodyObject = pBodyChunk->VPhysicsGetObject(); Assert( pBodyObject ); IPhysicsObject *pCockpitObject = pCockpitChunk->VPhysicsGetObject(); Assert( pCockpitObject ); IPhysicsObject *pTailObject = pTailChunk->VPhysicsGetObject(); Assert( pTailObject ); IPhysicsConstraintGroup *pGroup = NULL; // Create the constraint constraint_fixedparams_t fixed; fixed.Defaults(); fixed.InitWithCurrentObjectState( pBodyObject, pTailObject ); fixed.constraint.Defaults(); pBodyChunk->m_pTailConstraint = physenv->CreateFixedConstraint( pBodyObject, pTailObject, pGroup, fixed ); fixed.Defaults(); fixed.InitWithCurrentObjectState( pBodyObject, pCockpitObject ); fixed.constraint.Defaults(); pBodyChunk->m_pCockpitConstraint = physenv->CreateFixedConstraint( pBodyObject, pCockpitObject, pGroup, fixed ); } //----------------------------------------------------------------------------- // Purpose: Start us crashing //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::Event_Killed( const CTakeDamageInfo &info ) { if( m_lifeState == LIFE_ALIVE ) { m_OnShotDown.FireOutput( this, this ); } m_lifeState = LIFE_DYING; CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); controller.SoundChangeVolume( m_pGunFiringSound, 0.0, 0.1f ); if( GetCrashPoint() == NULL ) { CBaseEntity *pCrashPoint = gEntList.FindEntityByClassname( NULL, "info_target_helicopter_crash" ); if( pCrashPoint != NULL ) { m_hCrashPoint.Set( pCrashPoint ); SetDesiredPosition( pCrashPoint->GetAbsOrigin() ); // Start the failing engine sound CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); controller.SoundDestroy( m_pRotorSound ); CPASAttenuationFilter filter( this ); m_pRotorSound = controller.SoundCreate( filter, entindex(), "NPC_AttackHelicopter.EngineFailure" ); controller.Play( m_pRotorSound, 1.0, 100 ); // Tailspin!! SetActivity( ACT_HELICOPTER_CRASHING ); // Intentionally returning with m_lifeState set to LIFE_DYING return; } } Chopper_BecomeChunks( this ); StopLoopingSounds(); m_lifeState = LIFE_DEAD; EmitSound( "NPC_CombineGunship.Explode" ); SetThink( &CNPC_AttackHelicopter::SUB_Remove ); SetNextThink( gpGlobals->curtime + 0.1f ); AddEffects( EF_NODRAW ); // Makes the slower rotors fade back in SetStartupTime( gpGlobals->curtime + 99.0f ); m_iHealth = 0; m_takedamage = DAMAGE_NO; m_OnDeath.FireOutput( info.GetAttacker(), this ); } //------------------------------------------------------------------------------ // Creates the breakable husk of an attack chopper //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::CreateChopperHusk() { // We're embedded into the ground CBaseEntity *pCorpse = CreateEntityByName( "prop_physics" ); pCorpse->SetAbsOrigin( GetAbsOrigin() ); pCorpse->SetAbsAngles( GetAbsAngles() ); pCorpse->SetModel( CHOPPER_MODEL_CORPSE_NAME ); pCorpse->AddSpawnFlags( SF_PHYSPROP_MOTIONDISABLED ); pCorpse->Spawn(); pCorpse->SetMoveType( MOVETYPE_NONE ); } //----------------------------------------------------------------------------- // Think! //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::PrescheduleThink( void ) { if ( m_flGoalRollDmg != 0.0f ) { m_flGoalRollDmg = UTIL_Approach( 0, m_flGoalRollDmg, 2.0f ); } switch( m_lifeState ) { case LIFE_DYING: { if( GetCrashPoint() != NULL ) { // Stay on this, no matter what. SetDesiredPosition( GetCrashPoint()->WorldSpaceCenter() ); } if ( random->RandomInt( 0, 4 ) == 0 ) { Vector explodePoint; CollisionProp()->RandomPointInBounds( Vector(0.25,0.25,0.25), Vector(0.75,0.75,0.75), &explodePoint ); ExplodeAndThrowChunk( explodePoint ); } } break; } BaseClass::PrescheduleThink(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- float CNPC_AttackHelicopter::UpdatePerpPathDistance( float flMaxPathOffset ) { if ( !IsLeading() || !GetEnemy() ) { m_flCurrPathOffset = 0.0f; return 0.0f; } float flNewPathOffset = TargetDistanceToPath(); // Make bomb dropping more interesting if ( ShouldDropBombs() ) { float flSpeedAlongPath = TargetSpeedAlongPath(); if ( flSpeedAlongPath > 10.0f ) { float flLeadTime = GetLeadingDistance() / flSpeedAlongPath; flLeadTime = clamp( flLeadTime, 0.0f, 2.0f ); flNewPathOffset += 0.25 * flLeadTime * TargetSpeedAcrossPath(); } flSpeedAlongPath = clamp( flSpeedAlongPath, 100.0f, 500.0f ); float flSinHeight = SimpleSplineRemapVal( flSpeedAlongPath, 100.0f, 500.0f, 0.0f, 200.0f ); flNewPathOffset += flSinHeight * sin( 2.0f * M_PI * (gpGlobals->curtime / 6.0f) ); } if ( (flMaxPathOffset != 0.0f) && (flNewPathOffset > flMaxPathOffset) ) { flNewPathOffset = flMaxPathOffset; } float flMaxChange = 1000.0f * (gpGlobals->curtime - GetLastThink()); if ( fabs( flNewPathOffset - m_flCurrPathOffset ) < flMaxChange ) { m_flCurrPathOffset = flNewPathOffset; } else { float flSign = (m_flCurrPathOffset < flNewPathOffset) ? 1.0f : -1.0f; m_flCurrPathOffset += flSign * flMaxChange; } return m_flCurrPathOffset; } //----------------------------------------------------------------------------- // Computes the max speed + acceleration: //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::GetMaxSpeedAndAccel( float *pMaxSpeed, float *pAccelRate ) { *pAccelRate = CHOPPER_ACCEL_RATE; *pMaxSpeed = GetMaxSpeed(); if ( GetEnemyVehicle() ) { *pAccelRate *= 9.0f; } } //----------------------------------------------------------------------------- // Computes the acceleration: //----------------------------------------------------------------------------- #define HELICOPTER_GRAVITY 384 #define HELICOPTER_DT 0.1f #define HELICOPTER_MIN_DZ_DAMP -500.0f #define HELICOPTER_MAX_DZ_DAMP -1000.0f #define HELICOPTER_FORCE_BLEND 0.8f #define HELICOPTER_FORCE_BLEND_VEHICLE 0.2f void CNPC_AttackHelicopter::ComputeVelocity( const Vector &vecTargetPosition, float flAdditionalHeight, float flMinDistFromSegment, float flMaxDistFromSegment, Vector *pVecAccel ) { Vector deltaPos; VectorSubtract( vecTargetPosition, GetAbsOrigin(), deltaPos ); // calc goal linear accel to hit deltaPos in dt time. // This is solving the equation xf = 0.5 * a * dt^2 + vo * dt + xo float dt = 1.0f; pVecAccel->x = 2.0f * (deltaPos.x - GetAbsVelocity().x * dt) / (dt * dt); pVecAccel->y = 2.0f * (deltaPos.y - GetAbsVelocity().y * dt) / (dt * dt); pVecAccel->z = 2.0f * (deltaPos.z - GetAbsVelocity().z * dt) / (dt * dt) + HELICOPTER_GRAVITY; float flDistFromPath = 0.0f; Vector vecPoint, vecDelta; if ( flMaxDistFromSegment != 0.0f ) { // Also, add in a little force to get us closer to our current line segment if we can ClosestPointToCurrentPath( &vecPoint ); if ( flAdditionalHeight != 0.0f ) { Vector vecEndPoint, vecClosest; vecEndPoint = vecPoint; vecEndPoint.z += flAdditionalHeight; CalcClosestPointOnLineSegment( GetAbsOrigin(), vecPoint, vecEndPoint, vecClosest ); vecPoint = vecClosest; } VectorSubtract( vecPoint, GetAbsOrigin(), vecDelta ); flDistFromPath = VectorNormalize( vecDelta ); if ( flDistFromPath > flMaxDistFromSegment ) { // Strongly constrain to an n unit pipe around the current path // by damping out all impulse forces that would push us further from the pipe float flAmount = (flDistFromPath - flMaxDistFromSegment) / 200.0f; flAmount = clamp( flAmount, 0, 1 ); VectorMA( *pVecAccel, flAmount * 200.0f, vecDelta, *pVecAccel ); } } // Apply avoidance forces if ( !HasSpawnFlags( SF_HELICOPTER_IGNORE_AVOID_FORCES ) ) { Vector vecAvoidForce; CAvoidSphere::ComputeAvoidanceForces( this, 350.0f, 2.0f, &vecAvoidForce ); *pVecAccel += vecAvoidForce; CAvoidBox::ComputeAvoidanceForces( this, 350.0f, 2.0f, &vecAvoidForce ); *pVecAccel += vecAvoidForce; } // don't fall faster than 0.2G or climb faster than 2G pVecAccel->z = clamp( pVecAccel->z, HELICOPTER_GRAVITY * 0.2f, HELICOPTER_GRAVITY * 2.0f ); // The lift factor owing to horizontal movement float flHorizLiftFactor = fabs( pVecAccel->x ) * 0.10f + fabs( pVecAccel->y ) * 0.10f; // If we're way above the path, dampen horizontal lift factor float flNewHorizLiftFactor = clamp( deltaPos.z, HELICOPTER_MAX_DZ_DAMP, HELICOPTER_MIN_DZ_DAMP ); flNewHorizLiftFactor = SimpleSplineRemapVal( flNewHorizLiftFactor, HELICOPTER_MIN_DZ_DAMP, HELICOPTER_MAX_DZ_DAMP, flHorizLiftFactor, 2.5f * (HELICOPTER_GRAVITY * 0.2) ); float flDampening = (flNewHorizLiftFactor != 0.0f) ? (flNewHorizLiftFactor / flHorizLiftFactor) : 1.0f; if ( flDampening < 1.0f ) { pVecAccel->x *= flDampening; pVecAccel->y *= flDampening; flHorizLiftFactor = flNewHorizLiftFactor; } Vector forward, right, up; GetVectors( &forward, &right, &up ); // First, attenuate the current force float flForceBlend = GetEnemyVehicle() ? HELICOPTER_FORCE_BLEND_VEHICLE : HELICOPTER_FORCE_BLEND; m_flForce *= flForceBlend; // Now add force based on our acceleration factors m_flForce += ( pVecAccel->z + flHorizLiftFactor ) * HELICOPTER_DT * (1.0f - flForceBlend); // The force is always *locally* upward based; we pitch + roll the chopper to get movement Vector vecImpulse; VectorMultiply( up, m_flForce, vecImpulse ); // NOTE: These have to be done *before* the additional path distance drag forces are applied below ApplySidewaysDrag( right ); ApplyGeneralDrag(); // If LIFE_DYING, maintain control as long as we're flying to a crash point. if ( m_lifeState != LIFE_DYING || (m_lifeState == LIFE_DYING && GetCrashPoint() != NULL) ) { vecImpulse.z += -HELICOPTER_GRAVITY * HELICOPTER_DT; if ( flMinDistFromSegment != 0.0f && ( flDistFromPath > flMinDistFromSegment ) ) { Vector vecVelDir = GetAbsVelocity(); // Strongly constrain to an n unit pipe around the current path // by damping out all impulse forces that would push us further from the pipe float flDot = DotProduct( vecImpulse, vecDelta ); if ( flDot < 0.0f ) { VectorMA( vecImpulse, -flDot * 0.1f, vecDelta, vecImpulse ); } // Also apply an extra impulse to compensate for the current velocity flDot = DotProduct( vecVelDir, vecDelta ); if ( flDot < 0.0f ) { VectorMA( vecImpulse, -flDot * 0.1f, vecDelta, vecImpulse ); } } } else { // No more upward lift... vecImpulse.z = -HELICOPTER_GRAVITY * HELICOPTER_DT; // Damp the horizontal impulses; we should pretty much be falling ballistically vecImpulse.x *= 0.1f; vecImpulse.y *= 0.1f; } // Add in our velocity pulse for this frame ApplyAbsVelocityImpulse( vecImpulse ); } //----------------------------------------------------------------------------- // Computes the max speed + acceleration: //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::ComputeAngularVelocity( const Vector &vecGoalUp, const Vector &vecFacingDirection ) { QAngle goalAngAccel; if ( m_lifeState != LIFE_DYING || (m_lifeState == LIFE_DYING && GetCrashPoint() != NULL) ) { Vector forward, right, up; GetVectors( &forward, &right, &up ); Vector goalUp = vecGoalUp; VectorNormalize( goalUp ); // calc goal orientation to hit linear accel forces float goalPitch = RAD2DEG( asin( DotProduct( forward, goalUp ) ) ); float goalYaw = UTIL_VecToYaw( vecFacingDirection ); float goalRoll = RAD2DEG( asin( DotProduct( right, goalUp ) ) + m_flGoalRollDmg ); goalPitch *= 0.75f; // clamp goal orientations goalPitch = clamp( goalPitch, -30, 45 ); goalRoll = clamp( goalRoll, -45, 45 ); // calc angular accel needed to hit goal pitch in dt time. float dt = 0.6; goalAngAccel.x = 2.0 * (AngleDiff( goalPitch, AngleNormalize( GetAbsAngles().x ) ) - GetLocalAngularVelocity().x * dt) / (dt * dt); goalAngAccel.y = 2.0 * (AngleDiff( goalYaw, AngleNormalize( GetAbsAngles().y ) ) - GetLocalAngularVelocity().y * dt) / (dt * dt); goalAngAccel.z = 2.0 * (AngleDiff( goalRoll, AngleNormalize( GetAbsAngles().z ) ) - GetLocalAngularVelocity().z * dt) / (dt * dt); goalAngAccel.x = clamp( goalAngAccel.x, -300, 300 ); //goalAngAccel.y = clamp( goalAngAccel.y, -60, 60 ); goalAngAccel.y = clamp( goalAngAccel.y, -120, 120 ); goalAngAccel.z = clamp( goalAngAccel.z, -300, 300 ); } else { goalAngAccel.x = 0; goalAngAccel.y = random->RandomFloat( 50, 120 ); goalAngAccel.z = 0; } // limit angular accel changes to similate mechanical response times QAngle angAccelAccel; float dt = 0.1; angAccelAccel.x = (goalAngAccel.x - m_vecAngAcceleration.x) / dt; angAccelAccel.y = (goalAngAccel.y - m_vecAngAcceleration.y) / dt; angAccelAccel.z = (goalAngAccel.z - m_vecAngAcceleration.z) / dt; angAccelAccel.x = clamp( angAccelAccel.x, -1000, 1000 ); angAccelAccel.y = clamp( angAccelAccel.y, -1000, 1000 ); angAccelAccel.z = clamp( angAccelAccel.z, -1000, 1000 ); // DevMsg( "pitch %6.1f (%6.1f:%6.1f) ", goalPitch, GetLocalAngles().x, m_vecAngVelocity.x ); // DevMsg( "roll %6.1f (%6.1f:%6.1f) : ", goalRoll, GetLocalAngles().z, m_vecAngVelocity.z ); // DevMsg( "%6.1f %6.1f %6.1f : ", goalAngAccel.x, goalAngAccel.y, goalAngAccel.z ); // DevMsg( "%6.0f %6.0f %6.0f\n", angAccelAccel.x, angAccelAccel.y, angAccelAccel.z ); m_vecAngAcceleration += angAccelAccel * 0.1; QAngle angVel = GetLocalAngularVelocity(); angVel += m_vecAngAcceleration * 0.1; angVel.y = clamp( angVel.y, -120, 120 ); // Fix up pitch and yaw to tend toward small values if ( m_lifeState == LIFE_DYING && GetCrashPoint() == NULL ) { float flPitchDiff = random->RandomFloat( -5, 5 ) - GetAbsAngles().x; angVel.x = flPitchDiff * 0.1f; float flRollDiff = random->RandomFloat( -5, 5 ) - GetAbsAngles().z; angVel.z = flRollDiff * 0.1f; } SetLocalAngularVelocity( angVel ); float flAmt = clamp( angVel.y, -30, 30 ); float flRudderPose = RemapVal( flAmt, -30, 30, 45, -45 ); SetPoseParameter( "rudder", flRudderPose ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::FlightDirectlyOverhead( void ) { Vector vecTargetPosition = m_vecTargetPosition; CBaseEntity *pEnemy = GetEnemy(); if ( HasEnemy() && FVisible( pEnemy ) ) { if ( GetEnemy()->IsPlayer() ) { CBaseEntity *pEnemyVehicle = assert_cast(GetEnemy())->GetVehicleEntity(); if ( pEnemyVehicle ) { Vector vecEnemyVel = pEnemyVehicle->GetSmoothedVelocity(); Vector vecRelativePosition; VectorSubtract( GetAbsOrigin(), pEnemyVehicle->GetAbsOrigin(), vecRelativePosition ); float flDist = VectorNormalize( vecRelativePosition ); float flEnemySpeed = VectorNormalize( vecEnemyVel ); float flDot = DotProduct( vecRelativePosition, vecEnemyVel ); float flSpeed = GetMaxSpeed() * 0.3f; //GetAbsVelocity().Length(); float a = flSpeed * flSpeed - flEnemySpeed * flEnemySpeed; float b = 2.0f * flEnemySpeed * flDist * flDot; float c = - flDist * flDist; float flDiscrim = b * b - 4 * a * c; if ( flDiscrim >= 0 ) { float t = ( -b + sqrt( flDiscrim ) ) / (2 * a); t = clamp( t, 0.0f, 4.0f ); VectorMA( pEnemyVehicle->GetAbsOrigin(), t * flEnemySpeed, vecEnemyVel, vecTargetPosition ); } } } } // if ( GetCurrentPathTargetPosition() ) // { // vecTargetPosition.z = GetCurrentPathTargetPosition()->z; // } NDebugOverlay::Cross3D( vecTargetPosition, -Vector(32,32,32), Vector(32,32,32), 0, 0, 255, true, 0.1f ); UpdateFacingDirection( vecTargetPosition ); Vector accel; ComputeVelocity( vecTargetPosition, 0.0f, 0.0f, 0.0f, &accel ); ComputeAngularVelocity( accel, m_vecDesiredFaceDir ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::Flight( void ) { if( GetFlags() & FL_ONGROUND ) { // This would be really bad. SetGroundEntity( NULL ); } // Determine the distances we must lie from the path float flMaxPathOffset = MaxDistanceFromCurrentPath(); float flPerpDist = UpdatePerpPathDistance( flMaxPathOffset ); float flMinDistFromSegment, flMaxDistFromSegment; if ( !IsLeading() ) { flMinDistFromSegment = 0.0f; flMaxDistFromSegment = 0.0f; } else { flMinDistFromSegment = fabs(flPerpDist) + 100.0f; flMaxDistFromSegment = fabs(flPerpDist) + 200.0f; if ( flMaxPathOffset != 0.0 ) { if ( flMaxDistFromSegment > flMaxPathOffset - 100.0f ) { flMaxDistFromSegment = flMaxPathOffset - 100.0f; } if ( flMinDistFromSegment > flMaxPathOffset - 200.0f ) { flMinDistFromSegment = flMaxPathOffset - 200.0f; } } } float maxSpeed, accelRate; GetMaxSpeedAndAccel( &maxSpeed, &accelRate ); Vector vecTargetPosition; float flCurrentSpeed = GetAbsVelocity().Length(); float flDist = MIN( flCurrentSpeed + accelRate, maxSpeed ); float dt = 1.0f; ComputeActualTargetPosition( flDist, dt, flPerpDist, &vecTargetPosition ); // Raise high in the air when doing the shooting attack float flAdditionalHeight = 0.0f; if ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) { flAdditionalHeight = clamp( m_flBullrushAdditionalHeight, 0.0f, flMaxPathOffset ); vecTargetPosition.z += flAdditionalHeight; } Vector accel; UpdateFacingDirection( vecTargetPosition ); ComputeVelocity( vecTargetPosition, flAdditionalHeight, flMinDistFromSegment, flMaxDistFromSegment, &accel ); ComputeAngularVelocity( accel, m_vecDesiredFaceDir ); } //------------------------------------------------------------------------------ // Updates the facing direction //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::UpdateFacingDirection( const Vector &vecActualDesiredPosition ) { bool bIsBullrushing = ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ); bool bSeenTargetRecently = HasSpawnFlags( SF_HELICOPTER_AGGRESSIVE ) || ( m_flLastSeen + 5 > gpGlobals->curtime ); if ( GetEnemy() && !bIsBullrushing ) { if ( !IsLeading() ) { if( IsCarpetBombing() && hl2_episodic.GetBool() ) { m_vecDesiredFaceDir = vecActualDesiredPosition - GetAbsOrigin(); } else if ( !IsCrashing() && bSeenTargetRecently ) { // If we've seen the target recently, face the target. m_vecDesiredFaceDir = m_vecTargetPosition - GetAbsOrigin(); } else { // Remain facing the way you were facing... } } else { if ( ShouldDropBombs() || IsCarpetBombing() ) { m_vecDesiredFaceDir = vecActualDesiredPosition - GetAbsOrigin(); } else { m_vecDesiredFaceDir = m_vecTargetPosition - GetAbsOrigin(); } } } else { // Face our desired position float flDistSqr = vecActualDesiredPosition.AsVector2D().DistToSqr( GetAbsOrigin().AsVector2D() ); if ( flDistSqr <= 50 * 50 ) { if (( flDistSqr > 1 * 1 ) && bSeenTargetRecently && IsInSecondaryMode( BULLRUSH_MODE_SHOOT_IDLE_PLAYER ) ) { m_vecDesiredFaceDir = m_vecTargetPosition - GetAbsOrigin(); m_vecDesiredFaceDir.z = 0.0f; } else { GetVectors( &m_vecDesiredFaceDir, NULL, NULL ); } } else { m_vecDesiredFaceDir = vecActualDesiredPosition - GetAbsOrigin(); } } VectorNormalize( m_vecDesiredFaceDir ); } //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ #define ENEMY_CREEP_RATE 400 float CNPC_AttackHelicopter::CreepTowardEnemy( float flSpeed, float flMinSpeed, float flMaxSpeed, float flMinDist, float flMaxDist ) { float dt = gpGlobals->curtime - GetLastThink(); float flEnemyCreepDist = ENEMY_CREEP_RATE * dt; // When the player is slow, creep toward him within a second or two float flLeadingDist = ClampSplineRemapVal( flSpeed, flMinSpeed, flMaxSpeed, flMinDist, flMaxDist ); float flCurrentDist = GetLeadingDistance( ); if ( fabs(flLeadingDist - flCurrentDist) > flEnemyCreepDist ) { float flSign = ( flLeadingDist < flCurrentDist ) ? -1.0f : 1.0f; flLeadingDist = flCurrentDist + flSign * flEnemyCreepDist; } return flLeadingDist; } #define MIN_ENEMY_SPEED 300 //------------------------------------------------------------------------------ // Computes how far to lead the player when bombing //------------------------------------------------------------------------------ float CNPC_AttackHelicopter::ComputeBombingLeadingDistance( float flSpeed, float flSpeedAlongPath, bool bEnemyInVehicle ) { if ( ( flSpeed <= MIN_ENEMY_SPEED ) && bEnemyInVehicle ) { return CreepTowardEnemy( flSpeed, 0.0f, MIN_ENEMY_SPEED, 0.0f, 1000.0f ); } return ClampSplineRemapVal( flSpeedAlongPath, 200.0f, 600.0f, 1000.0f, 2000.0f ); } //------------------------------------------------------------------------------ // Computes how far to lead the player when bullrushing //------------------------------------------------------------------------------ float CNPC_AttackHelicopter::ComputeBullrushLeadingDistance( float flSpeed, float flSpeedAlongPath, bool bEnemyInVehicle ) { switch ( m_nSecondaryMode ) { case BULLRUSH_MODE_WAIT_FOR_ENEMY: return 0.0f; case BULLRUSH_MODE_GET_DISTANCE: return m_bRushForward ? -CHOPPER_BULLRUSH_MODE_DISTANCE : CHOPPER_BULLRUSH_MODE_DISTANCE; case BULLRUSH_MODE_DROP_BOMBS_FOLLOW_PLAYER: // return m_bRushForward ? 1500.0f : -1500.0f; return ComputeBombingLeadingDistance( flSpeed, flSpeedAlongPath, bEnemyInVehicle ); case BULLRUSH_MODE_SHOOT_IDLE_PLAYER: return 0.0f; case BULLRUSH_MODE_DROP_BOMBS_FIXED_SPEED: return m_bRushForward ? 7000 : -7000; case BULLRUSH_MODE_MEGA_BOMB: return m_bRushForward ? CHOPPER_BULLRUSH_MODE_DISTANCE : -CHOPPER_BULLRUSH_MODE_DISTANCE; case BULLRUSH_MODE_SHOOT_GUN: { float flLeadDistance = 1000.f - CHOPPER_BULLRUSH_ENEMY_BOMB_DISTANCE; return m_bRushForward ? flLeadDistance : -flLeadDistance; } } Assert(0); return 0.0f; } //------------------------------------------------------------------------------ // Secondary mode //------------------------------------------------------------------------------ inline void CNPC_AttackHelicopter::SetSecondaryMode( int nMode, bool bRetainTime ) { m_nSecondaryMode = nMode; if (!bRetainTime) { m_flSecondaryModeStartTime = gpGlobals->curtime; } } inline bool CNPC_AttackHelicopter::IsInSecondaryMode( int nMode ) { return m_nSecondaryMode == nMode; } inline float CNPC_AttackHelicopter::SecondaryModeTime( ) const { return gpGlobals->curtime - m_flSecondaryModeStartTime; } //------------------------------------------------------------------------------ // Switch to idle //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::SwitchToBullrushIdle( void ) { // Put us directly into idle gun state (we're in firing state) m_flNextAttack = gpGlobals->curtime; m_nGunState = GUN_STATE_IDLE; m_nRemainingBursts = 0; m_flBullrushAdditionalHeight = 0.0f; SetPauseState( PAUSE_NO_PAUSE ); CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); controller.SoundChangeVolume( m_pGunFiringSound, 0.0, 0.1f ); } //------------------------------------------------------------------------------ // Should the chopper shoot the idle player? //------------------------------------------------------------------------------ bool CNPC_AttackHelicopter::ShouldShootIdlePlayerInBullrush() { // Once he starts shooting, then don't stop until the player is moving pretty fast float flSpeedSqr = IsInSecondaryMode( BULLRUSH_MODE_SHOOT_IDLE_PLAYER ) ? CHOPPER_BULLRUSH_SLOW_SHOOT_SPEED_2_SQ : CHOPPER_BULLRUSH_SLOW_SHOOT_SPEED_SQ; return ( GetEnemy() && GetEnemy()->GetSmoothedVelocity().LengthSqr() <= flSpeedSqr ); } //------------------------------------------------------------------------------ // Shutdown shooting during bullrush //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::ShutdownGunDuringBullrush( ) { // Put us directly into idle gun state (we're in firing state) m_flNextAttack = gpGlobals->curtime; m_nGunState = GUN_STATE_IDLE; m_nRemainingBursts = 0; SetPauseState( PAUSE_NO_PAUSE ); CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); controller.SoundChangeVolume( m_pGunFiringSound, 0.0, 0.1f ); } #define HELICOPTER_MIN_IDLE_BOMBING_DIST 350.0f #define HELICOPTER_MIN_IDLE_BOMBING_SPEED 350.0f //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CNPC_AttackHelicopter::ShouldBombIdlePlayer( void ) { // Must be settled over a position and not moving too quickly to do this if ( GetAbsVelocity().LengthSqr() > Square(HELICOPTER_MIN_IDLE_BOMBING_SPEED) ) return false; // Must be within a certain range of the target float flDistToTargetSqr = (GetEnemy()->WorldSpaceCenter() - GetAbsOrigin()).Length2DSqr(); if ( flDistToTargetSqr < Square(HELICOPTER_MIN_IDLE_BOMBING_DIST) ) return true; // Can't bomb this return false; } //------------------------------------------------------------------------------ // Update the bullrush state //------------------------------------------------------------------------------ #define BULLRUSH_GOAL_TOLERANCE 200 #define BULLRUSH_BOMB_MAX_DISTANCE 3500 void CNPC_AttackHelicopter::UpdateBullrushState( void ) { if ( !GetEnemy() || IsInForcedMove() ) { if ( !IsInSecondaryMode( BULLRUSH_MODE_WAIT_FOR_ENEMY ) ) { SwitchToBullrushIdle(); SetSecondaryMode( BULLRUSH_MODE_WAIT_FOR_ENEMY ); } } switch( m_nSecondaryMode ) { case BULLRUSH_MODE_WAIT_FOR_ENEMY: { m_flBullrushAdditionalHeight = CHOPPER_BULLRUSH_SHOOTING_VERTICAL_OFFSET; if ( GetEnemy() && !IsInForcedMove() ) { // This forces us to not start trying checking positions // until we have been on the path for a little while if ( SecondaryModeTime() > 0.3f ) { float flDistanceToGoal = ComputeDistanceToTargetPosition(); Vector vecPathDir; CurrentPathDirection( &vecPathDir ); bool bMovingForward = DotProduct2D( GetAbsVelocity().AsVector2D(), vecPathDir.AsVector2D() ) >= 0.0f; if ( flDistanceToGoal * (bMovingForward ? 1.0f : -1.0f) > 1000 ) { m_bRushForward = bMovingForward; SetSecondaryMode( BULLRUSH_MODE_SHOOT_GUN ); SpotlightStartup(); } else { m_bRushForward = !bMovingForward; SetSecondaryMode( BULLRUSH_MODE_GET_DISTANCE ); } } } else { m_flSecondaryModeStartTime = gpGlobals->curtime; } } break; case BULLRUSH_MODE_GET_DISTANCE: { m_flBullrushAdditionalHeight = CHOPPER_BULLRUSH_SHOOTING_VERTICAL_OFFSET; float flDistanceToGoal = ComputeDistanceToTargetPosition(); if ( m_bRushForward ) { if ( flDistanceToGoal < (CHOPPER_BULLRUSH_MODE_DISTANCE - 1000) ) break; } else { if ( flDistanceToGoal > -(CHOPPER_BULLRUSH_MODE_DISTANCE - 1000) ) break; } if ( GetHealth() <= m_flNextMegaBombHealth ) { m_flNextMegaBombHealth -= GetMaxHealth() * g_helicopter_bullrush_mega_bomb_health.GetFloat(); m_flNextBullrushBombTime = gpGlobals->curtime; SetSecondaryMode( BULLRUSH_MODE_MEGA_BOMB ); EmitSound( "NPC_AttackHelicopter.MegabombAlert" ); } else { SetSecondaryMode( BULLRUSH_MODE_SHOOT_GUN ); SpotlightStartup(); } } break; case BULLRUSH_MODE_MEGA_BOMB: { m_flBullrushAdditionalHeight = CHOPPER_BULLRUSH_SHOOTING_VERTICAL_OFFSET; float flDistanceToGoal = ComputeDistanceToTargetPosition(); if ( m_bRushForward ) { if ( flDistanceToGoal > -(CHOPPER_BULLRUSH_MODE_DISTANCE - 1000) ) break; } else { if ( flDistanceToGoal < (CHOPPER_BULLRUSH_MODE_DISTANCE - 1000) ) break; } m_bRushForward = !m_bRushForward; SetSecondaryMode( BULLRUSH_MODE_GET_DISTANCE ); } break; case BULLRUSH_MODE_SHOOT_GUN: { // When shooting, stop when we cross the player's position // Then start bombing. Use the fixed speed version if we're too far // from the enemy or if he's travelling in the opposite direction. // Otherwise, do the standard bombing behavior for a while. float flDistanceToGoal = ComputeDistanceToTargetPosition(); float flShootingHeight = CHOPPER_BULLRUSH_SHOOTING_VERTICAL_OFFSET; float flSwitchToBombDist = CHOPPER_BULLRUSH_ENEMY_BOMB_DISTANCE; float flDropDownDist = 2000.0f; if ( m_bRushForward ) { m_flBullrushAdditionalHeight = ClampSplineRemapVal( flDistanceToGoal, flSwitchToBombDist, flSwitchToBombDist + flDropDownDist, 0.0f, flShootingHeight ); if ( flDistanceToGoal > flSwitchToBombDist ) break; } else { m_flBullrushAdditionalHeight = ClampSplineRemapVal( flDistanceToGoal, -flSwitchToBombDist - flDropDownDist, -flSwitchToBombDist, flShootingHeight, 0.0f ); if ( flDistanceToGoal < -flSwitchToBombDist ) break; } if ( ShouldShootIdlePlayerInBullrush() ) { SetSecondaryMode( BULLRUSH_MODE_SHOOT_IDLE_PLAYER ); } else { ShutdownGunDuringBullrush( ); SetSecondaryMode( BULLRUSH_MODE_DROP_BOMBS_FIXED_SPEED ); } } break; case BULLRUSH_MODE_SHOOT_IDLE_PLAYER: { // Shut down our gun if we're switching to bombing if ( ShouldBombIdlePlayer() ) { // Must not already be shutdown if (( m_nGunState != GUN_STATE_IDLE ) && (SecondaryModeTime() >= BULLRUSH_IDLE_PLAYER_FIRE_TIME)) { ShutdownGunDuringBullrush( ); } } // m_nBurstHits = 0; m_flCircleOfDeathRadius = ClampSplineRemapVal( SecondaryModeTime(), BULLRUSH_IDLE_PLAYER_FIRE_TIME, BULLRUSH_IDLE_PLAYER_FIRE_TIME + 5.0f, 256.0f, 64.0f ); m_flBullrushAdditionalHeight = CHOPPER_BULLRUSH_SHOOTING_VERTICAL_OFFSET; if ( !ShouldShootIdlePlayerInBullrush() ) { ShutdownGunDuringBullrush( ); SetSecondaryMode( BULLRUSH_MODE_DROP_BOMBS_FIXED_SPEED ); } } break; case BULLRUSH_MODE_DROP_BOMBS_FOLLOW_PLAYER: { m_flBullrushAdditionalHeight = 0.0f; float flDistanceToGoal = ComputeDistanceToTargetPosition(); if ( fabs( flDistanceToGoal ) > 2000.0f ) { SetSecondaryMode( BULLRUSH_MODE_DROP_BOMBS_FIXED_SPEED, true ); break; } } // FALL THROUGH!! case BULLRUSH_MODE_DROP_BOMBS_FIXED_SPEED: { float flDistanceToGoal = ComputeDistanceToTargetPosition(); m_flBullrushAdditionalHeight = 0.0f; if (( SecondaryModeTime() >= CHOPPER_BULLRUSH_ENEMY_BOMB_TIME ) || ( flDistanceToGoal > BULLRUSH_BOMB_MAX_DISTANCE )) { m_bRushForward = !m_bRushForward; SetSecondaryMode( BULLRUSH_MODE_GET_DISTANCE ); } } break; } } //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::UpdateEnemyLeading( void ) { bool bEnemyInVehicle = true; CBaseEntity *pTarget = GetEnemyVehicle(); if ( !pTarget ) { bEnemyInVehicle = false; if ( (m_nAttackMode == ATTACK_MODE_DEFAULT) || !GetEnemy() ) { EnableLeading( false ); return; } pTarget = GetEnemy(); } EnableLeading( true ); float flLeadingDist = 0.0f; float flSpeedAlongPath = TargetSpeedAlongPath(); float flSpeed = pTarget->GetSmoothedVelocity().Length(); // Do the test electricity gun if ( HasSpawnFlags(SF_HELICOPTER_ELECTRICAL_DRONE) ) { if ( flSpeedAlongPath < 200.0f ) { flLeadingDist = ClampSplineRemapVal( flSpeedAlongPath, 0.0f, 200.0f, 100.0f, -200.0f ); } else { flLeadingDist = ClampSplineRemapVal( flSpeedAlongPath, 200.0f, 600.0f, -200.0f, -500.0f ); } SetLeadingDistance( flLeadingDist ); return; } switch( m_nAttackMode ) { case ATTACK_MODE_BULLRUSH_VEHICLE: flLeadingDist = ComputeBullrushLeadingDistance( flSpeed, flSpeedAlongPath, bEnemyInVehicle ); break; case ATTACK_MODE_ALWAYS_LEAD_VEHICLE: if (( flSpeed <= MIN_ENEMY_SPEED ) && (bEnemyInVehicle) ) { flLeadingDist = CreepTowardEnemy( flSpeed, 0.0f, MIN_ENEMY_SPEED, 0.0f, 1000.0f ); } else { if ( flSpeedAlongPath > 0.0f ) { flLeadingDist = ClampSplineRemapVal( flSpeedAlongPath, 200.0f, 600.0f, 1000.0f, 2000.0f ); } else { flLeadingDist = ClampSplineRemapVal( flSpeedAlongPath, -600.0f, -200.0f, -2000.0f, -1000.0f ); } } break; case ATTACK_MODE_BOMB_VEHICLE: flLeadingDist = ComputeBombingLeadingDistance( flSpeed, flSpeedAlongPath, bEnemyInVehicle ); break; case ATTACK_MODE_DEFAULT: case ATTACK_MODE_TRAIL_VEHICLE: if (( flSpeed <= MIN_ENEMY_SPEED ) && (bEnemyInVehicle)) { flLeadingDist = CreepTowardEnemy( flSpeed, 150.0f, MIN_ENEMY_SPEED, 500.0f, -1000.0f ); } else { flLeadingDist = ClampSplineRemapVal( flSpeedAlongPath, -600.0f, -200.0f, -2500.0f, -1000.0f ); } break; } SetLeadingDistance( flLeadingDist ); } //----------------------------------------------------------------------------- // Purpose: // Input : *pInfo - // bAlways - //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::SetTransmit( CCheckTransmitInfo *pInfo, bool bAlways ) { // Are we already marked for transmission? if ( pInfo->m_pTransmitEdict->Get( entindex() ) ) return; BaseClass::SetTransmit( pInfo, bAlways ); // Make our smoke trails always come with us for ( int i = 0; i < m_nSmokeTrailCount; i++ ) { m_hSmokeTrail[i]->SetTransmit( pInfo, bAlways ); } } //------------------------------------------------------------------------------ // Purpose : //------------------------------------------------------------------------------ void CNPC_AttackHelicopter::Hunt( void ) { if ( m_lifeState == LIFE_DEAD ) { return; } if ( m_lifeState == LIFE_DYING ) { Flight(); UpdatePlayerDopplerShift( ); return; } // FIXME: Hack to allow us to change the firing distance SetFarthestPathDist( GetMaxFiringDistance() ); UpdateEnemy(); // Give free knowledge of the enemy position if the chopper is "aggressive" if ( HasSpawnFlags( SF_HELICOPTER_AGGRESSIVE ) && GetEnemy() ) { m_vecTargetPosition = GetEnemy()->WorldSpaceCenter(); } // Test for state transitions when in bullrush mode if ( m_nAttackMode == ATTACK_MODE_BULLRUSH_VEHICLE ) { UpdateBullrushState(); } UpdateEnemyLeading(); UpdateTrackNavigation( ); Flight(); UpdatePlayerDopplerShift( ); FireWeapons(); if ( !(m_fHelicopterFlags & BITS_HELICOPTER_GUN_ON) ) { // !!!HACKHACK This is a fairly unsavoury hack that allows the attack // chopper to continue to carpet bomb even with the gun turned off // (Normally the chopper will carpet bomb inside FireGun(), but FireGun() // doesn't get called by the above call to FireWeapons() if the gun is turned off) // Adding this little exception here lets me avoid going into the CBaseHelicopter and // making some functions virtual that don't want to be virtual. if ( IsCarpetBombing() ) { BullrushBombs(); } } #ifdef HL2_EPISODIC // Update our bone followers m_BoneFollowerManager.UpdateBoneFollowers(this); #endif // HL2_EPISODIC } //----------------------------------------------------------------------------- // Purpose: Cache whatever pose parameters we intend to use //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::PopulatePoseParameters( void ) { m_poseWeapon_Pitch = LookupPoseParameter("weapon_pitch"); m_poseWeapon_Yaw = LookupPoseParameter("weapon_yaw"); m_poseRudder = LookupPoseParameter("rudder"); BaseClass::PopulatePoseParameters(); } #ifdef HL2_EPISODIC //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_AttackHelicopter::InitBoneFollowers( void ) { // Don't do this if we're already loaded if ( m_BoneFollowerManager.GetNumBoneFollowers() != 0 ) return; // Init our followers m_BoneFollowerManager.InitBoneFollowers( this, ARRAYSIZE(pFollowerBoneNames), pFollowerBoneNames ); } #endif // HL2_EPISODIC //----------------------------------------------------------------------------- // Where are how should we avoid? //----------------------------------------------------------------------------- AI_BEGIN_CUSTOM_NPC( npc_helicopter, CNPC_AttackHelicopter ) // DECLARE_TASK( ) DECLARE_ACTIVITY( ACT_HELICOPTER_DROP_BOMB ); DECLARE_ACTIVITY( ACT_HELICOPTER_CRASHING ); // DECLARE_CONDITION( COND_ ) //========================================================= // DEFINE_SCHEDULE // ( // SCHED_DUMMY, // // " Tasks" // " TASK_FACE_ENEMY 0" // " " // " Interrupts" // ) AI_END_CUSTOM_NPC() //------------------------------------------------------------------------------ // // A sensor used to drop bombs only in the correct points // //------------------------------------------------------------------------------ LINK_ENTITY_TO_CLASS( npc_helicoptersensor, CBombDropSensor ); BEGIN_DATADESC( CBombDropSensor ) DEFINE_INPUTFUNC( FIELD_VOID, "DropBomb", InputDropBomb ), DEFINE_INPUTFUNC( FIELD_VOID, "DropBombStraightDown", InputDropBombStraightDown ), DEFINE_INPUTFUNC( FIELD_STRING, "DropBombAtTargetAlways", InputDropBombAtTargetAlways ), DEFINE_INPUTFUNC( FIELD_STRING, "DropBombAtTarget", InputDropBombAtTarget ), DEFINE_INPUTFUNC( FIELD_FLOAT, "DropBombDelay", InputDropBombDelay ), END_DATADESC() void CBombDropSensor::Spawn() { BaseClass::Spawn(); UTIL_SetSize(this, Vector(-30,-30,-30), Vector(30,30,30) ); SetSolid(SOLID_BBOX); // Shots pass through SetCollisionGroup( COLLISION_GROUP_PROJECTILE ); } // Drop a bomb at a particular location void CBombDropSensor::InputDropBomb( inputdata_t &inputdata ) { inputdata_t myVersion = inputdata; myVersion.pActivator = this; assert_cast(GetOwnerEntity())->InputDropBomb( myVersion ); } void CBombDropSensor::InputDropBombStraightDown( inputdata_t &inputdata ) { inputdata_t myVersion = inputdata; myVersion.pActivator = this; assert_cast(GetOwnerEntity())->InputDropBombStraightDown( myVersion ); } void CBombDropSensor::InputDropBombAtTarget( inputdata_t &inputdata ) { inputdata_t myVersion = inputdata; myVersion.pActivator = this; assert_cast(GetOwnerEntity())->InputDropBombAtTarget( myVersion ); } void CBombDropSensor::InputDropBombAtTargetAlways( inputdata_t &inputdata ) { inputdata_t myVersion = inputdata; myVersion.pActivator = this; assert_cast(GetOwnerEntity())->InputDropBombAtTargetAlways( myVersion ); } void CBombDropSensor::InputDropBombDelay( inputdata_t &inputdata ) { inputdata_t myVersion = inputdata; myVersion.pActivator = this; assert_cast(GetOwnerEntity())->InputDropBombDelay( myVersion ); } //------------------------------------------------------------------------------ // // The bombs the helicopter drops on the player // //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ // Save/load //------------------------------------------------------------------------------ LINK_ENTITY_TO_CLASS( grenade_helicopter, CGrenadeHelicopter ); BEGIN_DATADESC( CGrenadeHelicopter ) DEFINE_FIELD( m_bActivated, FIELD_BOOLEAN ), DEFINE_FIELD( m_bExplodeOnContact, FIELD_BOOLEAN ), DEFINE_SOUNDPATCH( m_pWarnSound ), DEFINE_FIELD( m_hWarningSprite, FIELD_EHANDLE ), DEFINE_FIELD( m_bBlinkerAtTop, FIELD_BOOLEAN ), #ifdef HL2_EPISODIC DEFINE_FIELD( m_flLifetime, FIELD_FLOAT ), DEFINE_FIELD( m_hCollisionObject, FIELD_EHANDLE ), DEFINE_FIELD( m_bPickedUp, FIELD_BOOLEAN ), DEFINE_FIELD( m_flBlinkFastTime, FIELD_TIME ), DEFINE_INPUTFUNC( FIELD_FLOAT, "ExplodeIn", InputExplodeIn ), DEFINE_OUTPUT( m_OnPhysGunOnlyPickup, "OnPhysGunOnlyPickup" ), #endif // HL2_EPISODIC DEFINE_THINKFUNC( ExplodeThink ), DEFINE_THINKFUNC( AnimateThink ), DEFINE_THINKFUNC( RampSoundThink ), DEFINE_THINKFUNC( WarningBlinkerThink ), DEFINE_ENTITYFUNC( ExplodeConcussion ), END_DATADESC() #define SF_HELICOPTER_GRENADE_DUD (1<<16) // Will not become active on impact, only when launched via physcannon //------------------------------------------------------------------------------ // Precache //------------------------------------------------------------------------------ void CGrenadeHelicopter::Precache( void ) { BaseClass::Precache( ); PrecacheModel( GRENADE_HELICOPTER_MODEL ); PrecacheScriptSound( "ReallyLoudSpark" ); PrecacheScriptSound( "NPC_AttackHelicopterGrenade.Ping" ); PrecacheScriptSound( "NPC_AttackHelicopterGrenade.PingCaptured" ); PrecacheScriptSound( "NPC_AttackHelicopterGrenade.HardImpact" ); PrecacheEffect( "HelicopterImpact" ); PrecacheEffect( "WaterSurfaceExplosion" ); PrecacheEffect( "HelicopterMegaBomb" ); UTIL_PrecacheOther( "env_explosion" ); } //------------------------------------------------------------------------------ // Spawn //------------------------------------------------------------------------------ void CGrenadeHelicopter::Spawn( void ) { Precache(); // point sized, solid, bouncing SetCollisionGroup( COLLISION_GROUP_PROJECTILE ); SetModel( GRENADE_HELICOPTER_MODEL ); if ( HasSpawnFlags( SF_HELICOPTER_GRENADE_DUD ) ) { m_nSkin = (int)SKIN_DUD; } if ( !HasSpawnFlags( SF_GRENADE_HELICOPTER_MEGABOMB ) ) { IPhysicsObject *pPhysicsObject = VPhysicsInitNormal( SOLID_VPHYSICS, GetSolidFlags(), false ); SetMoveType( MOVETYPE_VPHYSICS ); Vector vecAbsVelocity = GetAbsVelocity(); pPhysicsObject->AddVelocity( &vecAbsVelocity, NULL ); } else { SetSolid( SOLID_BBOX ); SetCollisionBounds( Vector( -12.5, -12.5, -12.5 ), Vector( 12.5, 12.5, 12.5 ) ); VPhysicsInitShadow( false, false ); SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_CUSTOM ); SetElasticity( 0.5f ); AddEffects( EF_NOSHADOW ); } // We're always being dropped beneath the helicopter; need to not // be affected by the rotor wash AddEFlags( EFL_NO_ROTORWASH_PUSH ); // contact grenades arc lower QAngle angles; VectorAngles(GetAbsVelocity(), angles ); SetLocalAngles( angles ); SetThink( NULL ); // Tumble in air QAngle vecAngVel( random->RandomFloat ( -100, -500 ), 0, 0 ); SetLocalAngularVelocity( vecAngVel ); // Explode on contact SetTouch( &CGrenadeHelicopter::ExplodeConcussion ); // use a lower gravity for grenades to make them easier to see SetGravity( UTIL_ScaleForGravity( 400 ) ); #ifdef HL2_EPISODIC m_bPickedUp = false; m_flLifetime = BOMB_LIFETIME * 2.0; #endif // HL2_EPISODIC if ( hl2_episodic.GetBool() ) { // Disallow this, we'd rather deal with them as physobjects m_takedamage = DAMAGE_NO; } else { // Allow player to blow this puppy up in the air m_takedamage = DAMAGE_YES; } m_bActivated = false; m_pWarnSound = NULL; m_bExplodeOnContact = false; m_flDamage = sk_helicopter_grenadedamage.GetFloat(); g_pNotify->AddEntity( this, this ); if( hl2_episodic.GetBool() ) { SetContextThink( &CGrenadeHelicopter::AnimateThink, gpGlobals->curtime, s_pAnimateThinkContext ); } } //------------------------------------------------------------------------------ // On Remve //------------------------------------------------------------------------------ void CGrenadeHelicopter::UpdateOnRemove() { if( m_pWarnSound ) { CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); controller.SoundDestroy( m_pWarnSound ); } g_pNotify->ClearEntity( this ); BaseClass::UpdateOnRemove(); } #ifdef HL2_EPISODIC //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CGrenadeHelicopter::InputExplodeIn( inputdata_t &inputdata ) { m_flLifetime = inputdata.value.Float(); if ( HasSpawnFlags( SF_HELICOPTER_GRENADE_DUD ) ) { // We are a dud no more! RemoveSpawnFlags( SF_HELICOPTER_GRENADE_DUD ); m_nSkin = (int)SKIN_REGULAR; } m_bActivated = false; BecomeActive(); } #endif //------------------------------------------------------------------------------ // Activate! //------------------------------------------------------------------------------ void CGrenadeHelicopter::BecomeActive() { if ( m_bActivated ) return; if ( IsMarkedForDeletion() ) return; m_bActivated = true; bool bMegaBomb = HasSpawnFlags(SF_GRENADE_HELICOPTER_MEGABOMB); SetThink( &CGrenadeHelicopter::ExplodeThink ); if ( hl2_episodic.GetBool() ) { if ( HasSpawnFlags( SF_HELICOPTER_GRENADE_DUD ) == false ) { SetNextThink( gpGlobals->curtime + GetBombLifetime() ); } else { // NOTE: A dud will not explode after a set time, only when launched! SetThink( NULL ); return; } } else { SetNextThink( gpGlobals->curtime + GetBombLifetime() ); } if ( !bMegaBomb ) { SetContextThink( &CGrenadeHelicopter::RampSoundThink, gpGlobals->curtime + GetBombLifetime() - BOMB_RAMP_SOUND_TIME, s_pRampSoundContext ); CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); CReliableBroadcastRecipientFilter filter; m_pWarnSound = controller.SoundCreate( filter, entindex(), "NPC_AttackHelicopterGrenade.Ping" ); controller.Play( m_pWarnSound, 1.0, PITCH_NORM ); } SetContextThink( &CGrenadeHelicopter::WarningBlinkerThink, gpGlobals->curtime + (GetBombLifetime() - 2.0f), s_pWarningBlinkerContext ); #ifdef HL2_EPISODIC m_flBlinkFastTime = gpGlobals->curtime + GetBombLifetime() - 1.0f; #endif//HL2_EPISODIC } //------------------------------------------------------------------------------ // Pow! //------------------------------------------------------------------------------ void CGrenadeHelicopter::RampSoundThink( ) { if ( m_pWarnSound ) { CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); controller.SoundChangePitch( m_pWarnSound, 140, BOMB_RAMP_SOUND_TIME ); } SetContextThink( NULL, gpGlobals->curtime, s_pRampSoundContext ); } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CGrenadeHelicopter::WarningBlinkerThink() { #ifndef HL2_EPISODIC return; #endif /* if( !m_hWarningSprite.Get() ) { Vector up; GetVectors( NULL, NULL, &up ); // Light isn't on, so create the sprite. m_hWarningSprite = CSprite::SpriteCreate( "sprites/redglow1.vmt", GetAbsOrigin() + up * 10.0f, false ); CSprite *pSprite = (CSprite *)m_hWarningSprite.Get(); if( pSprite != NULL ) { pSprite->SetParent( this, LookupAttachment("top") ); pSprite->SetTransparency( kRenderTransAdd, 255, 255, 255, 255, kRenderFxNone ); pSprite->SetScale( 0.35, 0.0 ); } m_bBlinkerAtTop = true; ResetSequence( LookupActivity( "ACT_ARM" ) ); } else */ { // Just flip it to the other attachment. if( m_bBlinkerAtTop ) { //m_hWarningSprite->SetParentAttachment( "SetParentAttachment", "bottom", false ); m_nSkin = (int)SKIN_REGULAR; m_bBlinkerAtTop = false; } else { //m_hWarningSprite->SetParentAttachment( "SetParentAttachment", "top", false ); m_nSkin = (int)SKIN_DUD; m_bBlinkerAtTop = true; } } // Frighten people CSoundEnt::InsertSound ( SOUND_DANGER, WorldSpaceCenter(), g_helicopter_bomb_danger_radius.GetFloat(), 0.2f, this, SOUNDENT_CHANNEL_REPEATED_DANGER ); #ifdef HL2_EPISODIC if( gpGlobals->curtime >= m_flBlinkFastTime ) { SetContextThink( &CGrenadeHelicopter::WarningBlinkerThink, gpGlobals->curtime + 0.1f, s_pWarningBlinkerContext ); } else { SetContextThink( &CGrenadeHelicopter::WarningBlinkerThink, gpGlobals->curtime + 0.2f, s_pWarningBlinkerContext ); } #endif//HL2_EPISODIC } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CGrenadeHelicopter::StopWarningBlinker() { if( m_hWarningSprite.Get() ) { UTIL_Remove( m_hWarningSprite.Get() ); m_hWarningSprite.Set( NULL ); } } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ void CGrenadeHelicopter::AnimateThink() { StudioFrameAdvance(); SetContextThink( &CGrenadeHelicopter::AnimateThink, gpGlobals->curtime + 0.1f, s_pAnimateThinkContext ); } //------------------------------------------------------------------------------ // Entity events... these are events targetted to a particular entity //------------------------------------------------------------------------------ void CGrenadeHelicopter::OnEntityEvent( EntityEvent_t event, void *pEventData ) { BaseClass::OnEntityEvent( event, pEventData ); if ( event == ENTITY_EVENT_WATER_TOUCH ) { BecomeActive(); } } //------------------------------------------------------------------------------ // If we hit water, then stop //------------------------------------------------------------------------------ void CGrenadeHelicopter::PhysicsSimulate( void ) { Vector vecPrevPosition = GetAbsOrigin(); BaseClass::PhysicsSimulate(); if (!m_bActivated && (GetMoveType() != MOVETYPE_VPHYSICS)) { if ( GetWaterLevel() > 1 ) { SetAbsVelocity( vec3_origin ); SetMoveType( MOVETYPE_NONE ); BecomeActive(); } // Stuck condition, can happen pretty often if ( vecPrevPosition == GetAbsOrigin() ) { SetAbsVelocity( vec3_origin ); SetMoveType( MOVETYPE_NONE ); BecomeActive(); } } } //------------------------------------------------------------------------------ // If we hit something, start the timer //------------------------------------------------------------------------------ void CGrenadeHelicopter::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) { BaseClass::VPhysicsCollision( index, pEvent ); BecomeActive(); #ifndef HL2_EPISODIC // in ep2, don't do this here, do it in Touch() if ( m_bExplodeOnContact ) { Vector vecVelocity; GetVelocity( &vecVelocity, NULL ); DoExplosion( GetAbsOrigin(), vecVelocity ); } #endif if( hl2_episodic.GetBool() ) { float flImpactSpeed = pEvent->preVelocity->Length(); if( flImpactSpeed > 400.0f && pEvent->pEntities[ 1 ]->IsWorld() ) { EmitSound( "NPC_AttackHelicopterGrenade.HardImpact" ); } } } #if HL2_EPISODIC //------------------------------------------------------------------------------ // double launch velocity for ep2_outland_08 //------------------------------------------------------------------------------ Vector CGrenadeHelicopter::PhysGunLaunchVelocity( const Vector &forward, float flMass ) { // return ( striderbuster_shot_velocity.GetFloat() * forward ); return BaseClass::PhysGunLaunchVelocity(forward,flMass) * sk_helicopter_grenaderadius.GetFloat(); } #endif //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ float CGrenadeHelicopter::GetBombLifetime() { #if HL2_EPISODIC return m_flLifetime; #else return BOMB_LIFETIME; #endif } //------------------------------------------------------------------------------ // Pow! //------------------------------------------------------------------------------ int CGrenadeHelicopter::OnTakeDamage( const CTakeDamageInfo &info ) { // We don't take blast damage if ( info.GetDamageType() & DMG_BLAST ) return 0; return BaseClass::OnTakeDamage( info ); } //------------------------------------------------------------------------------ // Pow! //------------------------------------------------------------------------------ void CGrenadeHelicopter::DoExplosion( const Vector &vecOrigin, const Vector &vecVelocity ) { ExplosionCreate( GetAbsOrigin(), GetAbsAngles(), GetOwnerEntity() ? GetOwnerEntity() : this, sk_helicopter_grenadedamage.GetFloat(), sk_helicopter_grenaderadius.GetFloat(), (SF_ENVEXPLOSION_NOSPARKS|SF_ENVEXPLOSION_NODLIGHTS|SF_ENVEXPLOSION_NODECAL|SF_ENVEXPLOSION_NOFIREBALL|SF_ENVEXPLOSION_NOPARTICLES), sk_helicopter_grenadeforce.GetFloat(), this ); if ( GetShakeAmplitude() ) { UTIL_ScreenShake( GetAbsOrigin(), GetShakeAmplitude(), 150.0, 1.0, GetShakeRadius(), SHAKE_START ); } CEffectData data; // If we're under water do a water explosion if ( GetWaterLevel() != 0 && (GetWaterType() & CONTENTS_WATER) ) { data.m_vOrigin = WorldSpaceCenter(); data.m_flMagnitude = 128; data.m_flScale = 128; data.m_fFlags = 0; DispatchEffect( "WaterSurfaceExplosion", data ); } else { // Otherwise do a normal explosion data.m_vOrigin = GetAbsOrigin(); DispatchEffect( "HelicopterMegaBomb", data ); } UTIL_Remove( this ); } //------------------------------------------------------------------------------ // I think I Pow! //------------------------------------------------------------------------------ void CGrenadeHelicopter::ExplodeThink(void) { #ifdef HL2_EPISODIC // remember if we were thrown by player, we can only determine this prior to explosion bool bIsThrownByPlayer = IsThrownByPlayer(); int iHealthBefore = 0; // get the health of the helicopter we came from prior to our explosion CNPC_AttackHelicopter *pOwner = dynamic_cast( GetOriginalThrower() ); if ( pOwner ) { iHealthBefore = pOwner->GetHealth(); } #endif // HL2_EPISODIC Vector vecVelocity; GetVelocity( &vecVelocity, NULL ); DoExplosion( GetAbsOrigin(), vecVelocity ); #ifdef HL2_EPISODIC // if we were thrown by player, look at health of helicopter after explosion and determine if we damaged it if ( bIsThrownByPlayer && pOwner && ( iHealthBefore > 0 ) ) { int iHealthAfter = pOwner->GetHealth(); if ( iHealthAfter == iHealthBefore ) { // The player threw us, we exploded due to timer, and we did not damage the helicopter that fired us. Send a miss event SendMissEvent(); } } #endif // HL2_EPISODIC } //------------------------------------------------------------------------------ // I think I Pow! //------------------------------------------------------------------------------ void CGrenadeHelicopter::ResolveFlyCollisionCustom( trace_t &trace, Vector &vecVelocity ) { ResolveFlyCollisionBounce( trace, vecVelocity, 0.1f ); } //------------------------------------------------------------------------------ // Contact grenade, explode when it touches something //------------------------------------------------------------------------------ void CGrenadeHelicopter::ExplodeConcussion( CBaseEntity *pOther ) { if ( !pOther->IsSolid() ) return; if ( !m_bExplodeOnContact ) { if ( pOther->IsWorld() ) return; if ( hl2_episodic.GetBool() ) { // Don't hit anything other than vehicles if ( pOther->GetCollisionGroup() != COLLISION_GROUP_VEHICLE ) return; } } #ifdef HL2_EPISODIC CBaseEntity *pEntityHit = pOther; if ( pEntityHit->ClassMatches( "phys_bone_follower" ) && pEntityHit->GetOwnerEntity() ) { pEntityHit = pEntityHit->GetOwnerEntity(); } if ( ( CLASS_COMBINE_GUNSHIP != pEntityHit->Classify() ) || !pEntityHit->ClassMatches( "npc_helicopter" ) ) { // We hit something other than a helicopter. If the player threw us, send a miss event if ( IsThrownByPlayer() ) { SendMissEvent(); } } #endif // HL2_EPISODIC Vector vecVelocity; GetVelocity( &vecVelocity, NULL ); DoExplosion( GetAbsOrigin(), vecVelocity ); } #ifdef HL2_EPISODIC //----------------------------------------------------------------------------- // Purpose: The bomb will act differently when picked up by the player //----------------------------------------------------------------------------- void CGrenadeHelicopter::OnPhysGunPickup(CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) { if ( reason == PICKED_UP_BY_CANNON ) { if ( !m_bPickedUp ) { if( m_hWarningSprite.Get() != NULL ) { UTIL_Remove( m_hWarningSprite ); m_hWarningSprite.Set(NULL); } // Turn on BecomeActive(); // Change the warning sound to a captured sound. SetContextThink( &CGrenadeHelicopter::RampSoundThink, gpGlobals->curtime + GetBombLifetime() - BOMB_RAMP_SOUND_TIME, s_pRampSoundContext ); CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); controller.SoundDestroy( m_pWarnSound ); CReliableBroadcastRecipientFilter filter; m_pWarnSound = controller.SoundCreate( filter, entindex(), "NPC_AttackHelicopterGrenade.PingCaptured" ); controller.Play( m_pWarnSound, 1.0, PITCH_NORM ); // Reset our counter so the player has more time SetThink( &CGrenadeHelicopter::ExplodeThink ); SetNextThink( gpGlobals->curtime + GetBombLifetime() ); SetContextThink( &CGrenadeHelicopter::WarningBlinkerThink, gpGlobals->curtime + GetBombLifetime() - 2.0f, s_pWarningBlinkerContext ); #ifdef HL2_EPISODIC m_nSkin = (int)SKIN_REGULAR; m_flBlinkFastTime = gpGlobals->curtime + GetBombLifetime() - 1.0f; #endif//HL2_EPISODIC // Stop us from sparing damage to the helicopter that dropped us SetOwnerEntity( pPhysGunUser ); PhysEnableEntityCollisions( this, m_hCollisionObject ); // Don't do this again! m_bPickedUp = true; m_OnPhysGunOnlyPickup.FireOutput( pPhysGunUser, this ); } } BaseClass::OnPhysGunPickup( pPhysGunUser, reason ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CGrenadeHelicopter::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t reason ) { if ( reason == LAUNCHED_BY_CANNON ) { // Enable world touches. unsigned int flags = VPhysicsGetObject()->GetCallbackFlags(); VPhysicsGetObject()->SetCallbackFlags( flags | CALLBACK_GLOBAL_TOUCH_STATIC ); // Explode on contact SetTouch( &CGrenadeHelicopter::ExplodeConcussion ); m_bExplodeOnContact = true; } BaseClass::OnPhysGunDrop( pPhysGunUser, reason ); } //----------------------------------------------------------------------------- // Purpose: Returns if the player threw this grenade w/phys gun //----------------------------------------------------------------------------- bool CGrenadeHelicopter::IsThrownByPlayer() { // if player is the owner and we're set to explode on contact, then the player threw this grenade. return ( ( GetOwnerEntity() == UTIL_GetLocalPlayer() ) && m_bExplodeOnContact ); } //----------------------------------------------------------------------------- // Purpose: If player threw this grenade, sends a miss event //----------------------------------------------------------------------------- void CGrenadeHelicopter::SendMissEvent() { // send a miss event IGameEvent *event = gameeventmanager->CreateEvent( "helicopter_grenade_punt_miss" ); if ( event ) { gameeventmanager->FireEvent( event ); } } #endif // HL2_EPISODIC //----------------------------------------------------------------------------- // // This entity is used to create little force spheres that the helicopters should avoid. // //----------------------------------------------------------------------------- CUtlVector< CAvoidSphere::AvoidSphereHandle_t > CAvoidSphere::s_AvoidSpheres; #define SF_AVOIDSPHERE_AVOID_BELOW 0x00010000 LINK_ENTITY_TO_CLASS( npc_heli_avoidsphere, CAvoidSphere ); BEGIN_DATADESC( CAvoidSphere ) DEFINE_KEYFIELD( m_flRadius, FIELD_FLOAT, "radius" ), END_DATADESC() //----------------------------------------------------------------------------- // Creates an avoidance sphere //----------------------------------------------------------------------------- CBaseEntity *CreateHelicopterAvoidanceSphere( CBaseEntity *pParent, int nAttachment, float flRadius, bool bAvoidBelow ) { CAvoidSphere *pSphere = static_cast(CreateEntityByName( "npc_heli_avoidsphere" )); pSphere->Init( flRadius ); if ( bAvoidBelow ) { pSphere->AddSpawnFlags( SF_AVOIDSPHERE_AVOID_BELOW ); } pSphere->Spawn(); pSphere->SetParent( pParent, nAttachment ); pSphere->SetLocalOrigin( vec3_origin ); pSphere->SetLocalAngles( vec3_angle ); pSphere->SetOwnerEntity( pParent ); return pSphere; } //----------------------------------------------------------------------------- // Init //----------------------------------------------------------------------------- void CAvoidSphere::Init( float flRadius ) { m_flRadius = flRadius; } //----------------------------------------------------------------------------- // Spawn, remove //----------------------------------------------------------------------------- void CAvoidSphere::Activate( ) { BaseClass::Activate(); s_AvoidSpheres.AddToTail( this ); } void CAvoidSphere::UpdateOnRemove( ) { s_AvoidSpheres.FindAndRemove( this ); BaseClass::UpdateOnRemove(); } //----------------------------------------------------------------------------- // Where are how should we avoid? //----------------------------------------------------------------------------- void CAvoidSphere::ComputeAvoidanceForces( CBaseEntity *pEntity, float flEntityRadius, float flAvoidTime, Vector *pVecAvoidForce ) { pVecAvoidForce->Init( ); Vector vecEntityDelta; VectorMultiply( pEntity->GetAbsVelocity(), flAvoidTime, vecEntityDelta ); Vector vecEntityCenter = pEntity->WorldSpaceCenter(); for ( int i = s_AvoidSpheres.Count(); --i >= 0; ) { CAvoidSphere *pSphere = s_AvoidSpheres[i].Get(); const Vector &vecAvoidCenter = pSphere->WorldSpaceCenter(); // NOTE: This test can be thought of sweeping a sphere through space // and seeing if it intersects the avoidance sphere float flTotalRadius = flEntityRadius + pSphere->m_flRadius; float t1, t2; if ( !IntersectRayWithSphere( vecEntityCenter, vecEntityDelta, vecAvoidCenter, flTotalRadius, &t1, &t2 ) ) { continue; } // NOTE: The point of closest approach is at the average t value Vector vecClosestApproach; float flAverageT = (t1 + t2) * 0.5f; VectorMA( vecEntityCenter, flAverageT, vecEntityDelta, vecClosestApproach ); // Add velocity to make it be pushed out away from the sphere center // without totally counteracting its velocity. Vector vecDir; VectorSubtract( vecClosestApproach, vecAvoidCenter, vecDir ); float flZDist = vecDir.z; float flDist = VectorNormalize( vecDir ); float flDistToTravel; if ( flDist < 0.01f ) { flDist = 0.01f; vecDir.Init( 0, 0, 1 ); flDistToTravel = flTotalRadius; } else { // make the chopper always avoid *above* // That means if a force would be applied to push the chopper down, // figure out a new distance to travel that would push the chopper up. if ( flZDist < 0.0f && !pSphere->HasSpawnFlags(SF_AVOIDSPHERE_AVOID_BELOW) ) { Vector vecExitPoint; vecDir.z = -vecDir.z; VectorMA( vecAvoidCenter, flTotalRadius, vecDir, vecExitPoint ); VectorSubtract( vecExitPoint, vecClosestApproach, vecDir ); flDistToTravel = VectorNormalize( vecDir ); } else { Assert( flDist <= flTotalRadius ); flDistToTravel = flTotalRadius - flDist; } } // The actual force amount is easy to think about: // We need to change the position by dx over a time dt, so dv = dx/dt // But so it doesn't explode, lets clamp t1 to a not-unreasonable time if ( t1 < 0.25f ) { t1 = 0.25f; } float flForce = 1.25f * flDistToTravel / t1; vecDir *= flForce; *pVecAvoidForce += vecDir; } } //----------------------------------------------------------------------------- // // This entity is used to create little force boxes that the helicopters should avoid. // //----------------------------------------------------------------------------- CUtlVector< CAvoidBox::AvoidBoxHandle_t > CAvoidBox::s_AvoidBoxes; #define SF_AVOIDBOX_AVOID_BELOW 0x00010000 LINK_ENTITY_TO_CLASS( npc_heli_avoidbox, CAvoidBox ); BEGIN_DATADESC( CAvoidBox ) END_DATADESC() //----------------------------------------------------------------------------- // Spawn, remove //----------------------------------------------------------------------------- void CAvoidBox::Spawn( ) { SetModel( STRING( GetModelName() ) ); SetSolid( SOLID_BSP ); AddSolidFlags( FSOLID_NOT_SOLID ); AddEffects( EF_NODRAW ); } void CAvoidBox::Activate( ) { BaseClass::Activate(); s_AvoidBoxes.AddToTail( this ); } void CAvoidBox::UpdateOnRemove( ) { s_AvoidBoxes.FindAndRemove( this ); BaseClass::UpdateOnRemove(); } //----------------------------------------------------------------------------- // Where are how should we avoid? //----------------------------------------------------------------------------- void CAvoidBox::ComputeAvoidanceForces( CBaseEntity *pEntity, float flEntityRadius, float flAvoidTime, Vector *pVecAvoidForce ) { pVecAvoidForce->Init( ); Vector vecEntityDelta, vecEntityEnd; VectorMultiply( pEntity->GetAbsVelocity(), flAvoidTime, vecEntityDelta ); Vector vecEntityCenter = pEntity->WorldSpaceCenter(); VectorAdd( vecEntityCenter, vecEntityDelta, vecEntityEnd ); Vector vecVelDir = pEntity->GetAbsVelocity(); VectorNormalize( vecVelDir ); for ( int i = s_AvoidBoxes.Count(); --i >= 0; ) { CAvoidBox *pBox = s_AvoidBoxes[i].Get(); const Vector &vecAvoidCenter = pBox->WorldSpaceCenter(); // NOTE: This test can be thought of sweeping a sphere through space // and seeing if it intersects the avoidance box float flTotalRadius = flEntityRadius + pBox->BoundingRadius(); float t1, t2; if ( !IntersectInfiniteRayWithSphere( vecEntityCenter, vecEntityDelta, vecAvoidCenter, flTotalRadius, &t1, &t2 ) ) { continue; } if (( t2 < 0.0f ) || ( t1 > 1.0f )) continue; // Unlike the avoid spheres, we also need to make sure the ray intersects the box Vector vecLocalCenter, vecLocalDelta; pBox->CollisionProp()->WorldToCollisionSpace( vecEntityCenter, &vecLocalCenter ); pBox->CollisionProp()->WorldDirectionToCollisionSpace( vecEntityDelta, &vecLocalDelta ); Vector vecBoxMin( -flEntityRadius, -flEntityRadius, -flEntityRadius ); Vector vecBoxMax( flEntityRadius, flEntityRadius, flEntityRadius ); vecBoxMin += pBox->CollisionProp()->OBBMins(); vecBoxMax += pBox->CollisionProp()->OBBMaxs(); trace_t tr; if ( !IntersectRayWithBox( vecLocalCenter, vecLocalDelta, vecBoxMin, vecBoxMax, 0.0f, &tr ) ) continue; // NOTE: The point of closest approach is at the average t value Vector vecClosestApproach; float flAverageT = (t1 + t2) * 0.5f; VectorMA( vecEntityCenter, flAverageT, vecEntityDelta, vecClosestApproach ); // Add velocity to make it be pushed out away from the sphere center // without totally counteracting its velocity. Vector vecDir; VectorSubtract( vecClosestApproach, vecAvoidCenter, vecDir ); // Limit unnecessary sideways motion if ( ( tr.plane.type != 3 ) || ( tr.plane.normal[2] > 0.0f ) ) { vecDir.x *= 0.1f; vecDir.y *= 0.1f; } float flZDist = vecDir.z; float flDist = VectorNormalize( vecDir ); float flDistToTravel; if ( flDist < 10.0f ) { flDist = 10.0f; vecDir.Init( 0, 0, 1 ); flDistToTravel = flTotalRadius; } else { // make the chopper always avoid *above* // That means if a force would be applied to push the chopper down, // figure out a new distance to travel that would push the chopper up. if ( flZDist < 0.0f && !pBox->HasSpawnFlags(SF_AVOIDSPHERE_AVOID_BELOW) ) { Vector vecExitPoint; vecDir.z = -vecDir.z; VectorMA( vecAvoidCenter, flTotalRadius, vecDir, vecExitPoint ); VectorSubtract( vecExitPoint, vecClosestApproach, vecDir ); flDistToTravel = VectorNormalize( vecDir ); } else { Assert( flDist <= flTotalRadius ); flDistToTravel = flTotalRadius - flDist; } } // The actual force amount is easy to think about: // We need to change the position by dx over a time dt, so dv = dx/dt // But so it doesn't explode, lets clamp t1 to a not-unreasonable time if ( t1 < 0.25f ) { t1 = 0.25f; } float flForce = 1.5f * flDistToTravel / t1; vecDir *= flForce; *pVecAvoidForce += vecDir; } } //----------------------------------------------------------------------------- // // This entity is used to create little force boxes that the helicopters should avoid. // //----------------------------------------------------------------------------- CUtlVector< CBombSuppressor::BombSuppressorHandle_t > CBombSuppressor::s_BombSuppressors; LINK_ENTITY_TO_CLASS( npc_heli_nobomb, CBombSuppressor ); BEGIN_DATADESC( CBombSuppressor ) END_DATADESC() //----------------------------------------------------------------------------- // Spawn, remove //----------------------------------------------------------------------------- void CBombSuppressor::Spawn( ) { SetModel( STRING( GetModelName() ) ); SetSolid( SOLID_BSP ); AddSolidFlags( FSOLID_NOT_SOLID ); AddEffects( EF_NODRAW ); } void CBombSuppressor::Activate( ) { BaseClass::Activate(); s_BombSuppressors.AddToTail( this ); } void CBombSuppressor::UpdateOnRemove( ) { s_BombSuppressors.FindAndRemove( this ); BaseClass::UpdateOnRemove(); } //----------------------------------------------------------------------------- // Where are how should we avoid? //----------------------------------------------------------------------------- bool CBombSuppressor::CanBomb( const Vector &vecPosition ) { for ( int i = s_BombSuppressors.Count(); --i >= 0; ) { CBombSuppressor *pBox = s_BombSuppressors[i].Get(); if ( pBox->CollisionProp()->IsPointInBounds( vecPosition ) ) return false; } return true; } LINK_ENTITY_TO_CLASS( helicopter_chunk, CHelicopterChunk ); BEGIN_DATADESC( CHelicopterChunk ) DEFINE_THINKFUNC( FallThink ), DEFINE_FIELD( m_bLanded, FIELD_BOOLEAN ), DEFINE_FIELD( m_hMaster, FIELD_EHANDLE ), DEFINE_FIELD( m_nChunkID, FIELD_INTEGER ), DEFINE_PHYSPTR( m_pTailConstraint ), DEFINE_PHYSPTR( m_pCockpitConstraint ), END_DATADESC() //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CHelicopterChunk::Spawn( void ) { } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CHelicopterChunk::FallThink( void ) { if ( m_bLanded ) { SetThink( NULL ); return; } if ( random->RandomInt( 0, 8 ) == 0 ) { CEffectData data; data.m_vOrigin = GetAbsOrigin() + RandomVector( -64, 64 ); DispatchEffect( "HelicopterMegaBomb", data ); EmitSound( "BaseExplosionEffect.Sound" ); } SetNextThink( gpGlobals->curtime + 0.1f ); } //----------------------------------------------------------------------------- // Purpose: // Input : index - // *pEvent - //----------------------------------------------------------------------------- void CHelicopterChunk::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) { BaseClass::VPhysicsCollision( index, pEvent ); if ( m_bLanded == false ) { int otherIndex = !index; CBaseEntity *pOther = pEvent->pEntities[otherIndex]; if ( !pOther ) return; if ( pOther->IsWorld() ) { CollisionCallback( this ); m_bLanded = true; SetThink( NULL ); } } } //----------------------------------------------------------------------------- // Purpose: // Input : *pCaller - //----------------------------------------------------------------------------- void CHelicopterChunk::CollisionCallback( CHelicopterChunk *pCaller ) { if ( m_bLanded ) return; if ( m_hMaster != NULL ) { m_hMaster->CollisionCallback( this ); } else { // Break our other constraints if ( m_pTailConstraint ) { physenv->DestroyConstraint( m_pTailConstraint ); m_pTailConstraint = NULL; } if ( m_pCockpitConstraint ) { physenv->DestroyConstraint( m_pCockpitConstraint ); m_pCockpitConstraint = NULL; } // Add a dust cloud AR2Explosion *pExplosion = AR2Explosion::CreateAR2Explosion( GetAbsOrigin() ); if ( pExplosion != NULL ) { pExplosion->SetLifetime( 10 ); } // Make a loud noise EmitSound( "NPC_AttackHelicopter.Crash" ); m_bLanded = true; } } //----------------------------------------------------------------------------- // Purpose: // Input : &vecPos - // &vecAngles - // &vecVelocity - // *pszModelName - // Output : CHelicopterChunk //----------------------------------------------------------------------------- CHelicopterChunk *CHelicopterChunk::CreateHelicopterChunk( const Vector &vecPos, const QAngle &vecAngles, const Vector &vecVelocity, const char *pszModelName, int chunkID ) { // Drop a flaming, smoking chunk. CHelicopterChunk *pChunk = CREATE_ENTITY( CHelicopterChunk, "helicopter_chunk" ); if ( pChunk == NULL ) return NULL; pChunk->Spawn(); pChunk->SetAbsOrigin( vecPos ); pChunk->SetAbsAngles( vecAngles ); pChunk->SetModel( pszModelName ); pChunk->m_nChunkID = chunkID; pChunk->SetCollisionGroup( COLLISION_GROUP_INTERACTIVE ); IPhysicsObject *pPhysicsObject = pChunk->VPhysicsInitNormal( SOLID_VPHYSICS, pChunk->GetSolidFlags(), false ); // Set the velocity if ( pPhysicsObject ) { pPhysicsObject->EnableMotion( true ); Vector vecChunkVelocity; AngularImpulse angImpulse; vecChunkVelocity = vecVelocity; angImpulse = vec3_origin; pPhysicsObject->SetVelocity(&vecChunkVelocity, &angImpulse ); } pChunk->SetThink( &CHelicopterChunk::FallThink ); pChunk->SetNextThink( gpGlobals->curtime + 0.1f ); pChunk->m_bLanded = false; SmokeTrail *pSmokeTrail = SmokeTrail::CreateSmokeTrail(); pSmokeTrail->FollowEntity( pChunk, "damage" ); pSmokeTrail->m_SpawnRate = 4; pSmokeTrail->m_ParticleLifetime = 2.0f; pSmokeTrail->m_StartColor.Init( 0.7f, 0.7f, 0.7f ); pSmokeTrail->m_EndColor.Init( 0.6, 0.6, 0.6 ); pSmokeTrail->m_StartSize = 32; pSmokeTrail->m_EndSize = 64; pSmokeTrail->m_SpawnRadius= 8; pSmokeTrail->m_MinSpeed = 0; pSmokeTrail->m_MaxSpeed = 8; pSmokeTrail->m_Opacity = 0.35f; CFireTrail *pFireTrail = CFireTrail::CreateFireTrail(); if ( pFireTrail == NULL ) return pChunk; pFireTrail->FollowEntity( pChunk, "damage" ); pFireTrail->SetParent( pChunk, 1 ); pFireTrail->SetLocalOrigin( vec3_origin ); pFireTrail->SetMoveType( MOVETYPE_NONE ); pFireTrail->SetLifetime( 10.0f ); return pChunk; }