//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "ai_basenpc.h" #include "ammodef.h" #include "ai_memory.h" #include "weapon_rpg.h" #include "effect_color_tables.h" #include "te_effect_dispatch.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" extern const char* g_pModelNameLaser; // No model, impervious to damage. #define SF_STARTDISABLED (1 << 19) #define CANNON_PAINT_ENEMY_TIME 1.0f #define CANNON_SUBSEQUENT_PAINT_TIME 0.4f #define CANNON_PAINT_NPC_TIME_NOISE 1.0f #define NUM_ANCILLARY_BEAMS 4 int gHaloTexture = 0; //----------------------------------------------------------------------------- // // Combine Cannon // //----------------------------------------------------------------------------- class CNPC_Combine_Cannon : public CAI_BaseNPC { DECLARE_CLASS( CNPC_Combine_Cannon, CAI_BaseNPC ); public: CNPC_Combine_Cannon( void ); virtual void Precache( void ); virtual void Spawn( void ); virtual Class_T Classify( void ); virtual float MaxYawSpeed( void ); virtual Vector EyePosition( void ); virtual void UpdateOnRemove( void ); virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); virtual bool QuerySeeEntity( CBaseEntity *pEntity, bool bOnlyHateOrFearIfNPC = false ); virtual void StartTask( const Task_t *pTask ); virtual void RunTask( const Task_t *pTask ); virtual int RangeAttack1Conditions( float flDot, float flDist ); virtual int SelectSchedule( void ); virtual int TranslateSchedule( int scheduleType ); virtual void PrescheduleThink( void ); virtual bool FCanCheckAttacks ( void ); virtual int Restore( IRestore &restore ); virtual void OnScheduleChange( void ); virtual bool FVisible( CBaseEntity *pEntity, int traceMask = MASK_BLOCKLOS, CBaseEntity **ppBlocker = NULL ); virtual bool WeaponLOSCondition( const Vector &ownerPos, const Vector &targetPos, bool bSetConditions) { return true; } virtual int GetSoundInterests( void ) { return (SOUND_PLAYER|SOUND_COMBAT|SOUND_DANGER); } virtual bool ShouldNotDistanceCull( void ) { return true; } virtual void UpdateEfficiency( bool bInPVS ) { SetEfficiency( ( GetSleepState() != AISS_AWAKE ) ? AIE_DORMANT : AIE_NORMAL ); SetMoveEfficiency( AIME_NORMAL ); } virtual const char *GetTracerType( void ) { return "HelicopterTracer"; } private: void ScopeGlint( void ); void AdjustShotPosition( CBaseEntity *pTarget, Vector *vecIn ); float GetRefireTime( void ) { return 0.1f; } bool IsLaserOn( void ) { return m_pBeam != NULL; } bool FireBullet( const Vector &vecTarget, bool bDirectShot ); Vector DesiredBodyTarget( CBaseEntity *pTarget ); Vector LeadTarget( CBaseEntity *pTarget ); Vector GetBulletOrigin( void ); static const char *pAttackSounds[]; void ClearTargetGroup( void ); float GetWaitTimePercentage( float flTime, bool fLinear ); void GetPaintAim( const Vector &vecStart, const Vector &vecGoal, float flParameter, Vector *pProgress ); bool VerifyShot( CBaseEntity *pTarget ); void SetSweepTarget( const char *pszTarget ); // Inputs void InputEnableSniper( inputdata_t &inputdata ); void InputDisableSniper( inputdata_t &inputdata ); void LaserOff( void ); void LaserOn( const Vector &vecTarget, const Vector &vecDeviance ); void PaintTarget( const Vector &vecTarget, float flPaintTime ); private: void CreateLaser( void ); void CreateAncillaryBeams( void ); void UpdateAncillaryBeams( float flConvergencePerc, const Vector &vecOrigin, const Vector &vecBasis ); int m_iAmmoType; float m_flBarrageDuration; Vector m_vecPaintCursor; float m_flPaintTime; CHandle m_pBeam; CHandle m_pAncillaryBeams[NUM_ANCILLARY_BEAMS]; EHANDLE m_hBarrageTarget; bool m_fEnabled; Vector m_vecPaintStart; // used to track where a sweep starts for the purpose of interpolating. float m_flTimeLastAttackedPlayer; float m_flTimeLastShotMissed; float m_flSightDist; DEFINE_CUSTOM_AI; DECLARE_DATADESC(); }; LINK_ENTITY_TO_CLASS( npc_combine_cannon, CNPC_Combine_Cannon ); //========================================================= //========================================================= BEGIN_DATADESC( CNPC_Combine_Cannon ) DEFINE_FIELD( m_fEnabled, FIELD_BOOLEAN ), DEFINE_FIELD( m_vecPaintStart, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_flPaintTime, FIELD_TIME ), DEFINE_FIELD( m_vecPaintCursor, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_pBeam, FIELD_EHANDLE ), DEFINE_FIELD( m_flTimeLastAttackedPlayer, FIELD_TIME ), DEFINE_FIELD( m_flTimeLastShotMissed, FIELD_TIME ), DEFINE_FIELD( m_iAmmoType, FIELD_INTEGER ), DEFINE_FIELD( m_flBarrageDuration, FIELD_TIME ), DEFINE_FIELD( m_hBarrageTarget, FIELD_EHANDLE ), DEFINE_ARRAY( m_pAncillaryBeams, FIELD_EHANDLE, NUM_ANCILLARY_BEAMS ), DEFINE_KEYFIELD( m_flSightDist, FIELD_FLOAT, "sightdist" ), // Inputs DEFINE_INPUTFUNC( FIELD_VOID, "EnableSniper", InputEnableSniper ), DEFINE_INPUTFUNC( FIELD_VOID, "DisableSniper", InputDisableSniper ), END_DATADESC() //========================================================= // Private conditions //========================================================= enum Sniper_Conds { COND_CANNON_ENABLED = LAST_SHARED_CONDITION, COND_CANNON_DISABLED, COND_CANNON_NO_SHOT, }; //========================================================= // schedules //========================================================= enum { SCHED_CANNON_CAMP = LAST_SHARED_SCHEDULE, SCHED_CANNON_ATTACK, SCHED_CANNON_DISABLEDWAIT, SCHED_CANNON_SNAPATTACK, }; //========================================================= // tasks //========================================================= enum { TASK_CANNON_PAINT_ENEMY = LAST_SHARED_TASK, TASK_CANNON_PAINT_DECOY, TASK_CANNON_ATTACK_CURSOR, }; //----------------------------------------------------------------------------- // Constructor //----------------------------------------------------------------------------- CNPC_Combine_Cannon::CNPC_Combine_Cannon( void ) : m_pBeam( NULL ), m_hBarrageTarget( NULL ) { #ifdef _DEBUG m_vecPaintCursor.Init(); m_vecPaintStart.Init(); #endif } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- bool CNPC_Combine_Cannon::QuerySeeEntity( CBaseEntity *pEntity, bool bOnlyHateOrFearIfNPC ) { Disposition_t disp = IRelationType(pEntity); if ( disp != D_HT ) { // Don't bother with anything I wouldn't shoot. return false; } if ( !FInViewCone(pEntity) ) { // Yes, this does call FInViewCone twice a frame for all entities checked for // visibility, but doing this allows us to cut out a bunch of traces that would // be done by VerifyShot for entities that aren't even in our viewcone. return false; } if ( VerifyShot( pEntity ) ) return BaseClass::QuerySeeEntity( pEntity, bOnlyHateOrFearIfNPC ); return false; } //----------------------------------------------------------------------------- // Purpose: Hide the beams //----------------------------------------------------------------------------- void CNPC_Combine_Cannon::LaserOff( void ) { if ( m_pBeam != NULL ) { m_pBeam->TurnOn(); } for ( int i = 0; i < NUM_ANCILLARY_BEAMS; i++ ) { if ( m_pAncillaryBeams[i] == NULL ) continue; m_pAncillaryBeams[i]->TurnOn(); } SetNextThink( gpGlobals->curtime + 0.1f ); } //----------------------------------------------------------------------------- // Purpose: Switch on the laser and point it at a direction //----------------------------------------------------------------------------- void CNPC_Combine_Cannon::LaserOn( const Vector &vecTarget, const Vector &vecDeviance ) { if ( m_pBeam != NULL ) { m_pBeam->TurnOff(); // Don't aim right at the guy right now. Vector vecInitialAim; if( vecDeviance == vec3_origin ) { // Start the aim where it last left off! vecInitialAim = m_vecPaintCursor; } else { vecInitialAim = vecTarget; } vecInitialAim.x += random->RandomFloat( -vecDeviance.x, vecDeviance.x ); vecInitialAim.y += random->RandomFloat( -vecDeviance.y, vecDeviance.y ); vecInitialAim.z += random->RandomFloat( -vecDeviance.z, vecDeviance.z ); m_pBeam->SetStartPos( GetBulletOrigin() ); m_pBeam->SetEndPos( vecInitialAim ); m_vecPaintStart = vecInitialAim; } for ( int i = 0; i < NUM_ANCILLARY_BEAMS; i++ ) { if ( m_pAncillaryBeams[i] == NULL ) continue; m_pAncillaryBeams[i]->TurnOff(); } } //----------------------------------------------------------------------------- // Crikey! //----------------------------------------------------------------------------- float CNPC_Combine_Cannon::GetWaitTimePercentage( float flTime, bool fLinear ) { float flElapsedTime; float flTimeParameter; flElapsedTime = flTime - (GetWaitFinishTime() - gpGlobals->curtime); flTimeParameter = ( flElapsedTime / flTime ); if( fLinear ) { return flTimeParameter; } else { return (1 + sin( (M_PI * flTimeParameter) - (M_PI / 2) ) ) / 2; } } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- void CNPC_Combine_Cannon::GetPaintAim( const Vector &vecStart, const Vector &vecGoal, float flParameter, Vector *pProgress ) { // Quaternions Vector vecIdealDir; QAngle vecIdealAngles; QAngle vecCurrentAngles; Vector vecCurrentDir; Vector vecBulletOrigin = GetBulletOrigin(); // vecIdealDir is where the gun should be aimed when the painting // time is up. This can be approximate. This is only for drawing the // laser, not actually aiming the weapon. A large discrepancy will look // bad, though. vecIdealDir = vecGoal - vecBulletOrigin; VectorNormalize(vecIdealDir); // Now turn vecIdealDir into angles! VectorAngles( vecIdealDir, vecIdealAngles ); // This is the vector of the beam's current aim. vecCurrentDir = m_vecPaintStart - vecBulletOrigin; VectorNormalize(vecCurrentDir); // Turn this to angles, too. VectorAngles( vecCurrentDir, vecCurrentAngles ); Quaternion idealQuat; Quaternion currentQuat; Quaternion aimQuat; AngleQuaternion( vecIdealAngles, idealQuat ); AngleQuaternion( vecCurrentAngles, currentQuat ); QuaternionSlerp( currentQuat, idealQuat, flParameter, aimQuat ); QuaternionAngles( aimQuat, vecCurrentAngles ); // Rebuild the current aim vector. AngleVectors( vecCurrentAngles, &vecCurrentDir ); *pProgress = vecCurrentDir; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_Combine_Cannon::CreateLaser( void ) { if ( m_pBeam != NULL ) return; m_pBeam = CBeam::BeamCreate( g_pModelNameLaser, 2.0f ); m_pBeam->SetColor( 0, 100, 255 ); m_pBeam->PointsInit( vec3_origin, GetBulletOrigin() ); m_pBeam->SetBrightness( 255 ); m_pBeam->SetNoise( 0 ); m_pBeam->SetWidth( 1.0f ); m_pBeam->SetEndWidth( 0 ); m_pBeam->SetScrollRate( 0 ); m_pBeam->SetFadeLength( 0 ); m_pBeam->SetHaloTexture( gHaloTexture ); m_pBeam->SetHaloScale( 16.0f ); // Think faster while painting SetNextThink( gpGlobals->curtime + 0.02f ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_Combine_Cannon::CreateAncillaryBeams( void ) { for ( int i = 0; i < NUM_ANCILLARY_BEAMS; i++ ) { if ( m_pAncillaryBeams[i] != NULL ) continue; m_pAncillaryBeams[i] = CBeam::BeamCreate( g_pModelNameLaser, 2.0f ); m_pAncillaryBeams[i]->SetColor( 0, 100, 255 ); m_pAncillaryBeams[i]->PointsInit( vec3_origin, GetBulletOrigin() ); m_pAncillaryBeams[i]->SetBrightness( 255 ); m_pAncillaryBeams[i]->SetNoise( 0 ); m_pAncillaryBeams[i]->SetWidth( 1.0f ); m_pAncillaryBeams[i]->SetEndWidth( 0 ); m_pAncillaryBeams[i]->SetScrollRate( 0 ); m_pAncillaryBeams[i]->SetFadeLength( 0 ); m_pAncillaryBeams[i]->SetHaloTexture( gHaloTexture ); m_pAncillaryBeams[i]->SetHaloScale( 16.0f ); m_pAncillaryBeams[i]->TurnOff(); } } #define LINE_LENGTH 1600.0f //----------------------------------------------------------------------------- // Purpose: // Input : flConvergencePerc - // vecBasis - //----------------------------------------------------------------------------- void CNPC_Combine_Cannon::UpdateAncillaryBeams( float flConvergencePerc, const Vector &vecOrigin, const Vector &vecBasis ) { // Multiple beams deviate from the basis direction by a certain number of degrees and "converge" // at the basis vector over a duration of time, the position in that duration expressed by // flConvergencePerc. The beams are most deviated at 0 and fully converged at 1. float flRotationOffset = (2*M_PI)/(float)NUM_ANCILLARY_BEAMS; // Degrees separating each beam, in radians float flDeviation = DEG2RAD(90) * ( 1.0f - flConvergencePerc ); float flOffset; Vector vecFinal; Vector vecOffset; matrix3x4_t matRotate; QAngle vecAngles; VectorAngles( vecBasis, vecAngles ); vecAngles[PITCH] += 90.0f; AngleMatrix( vecAngles, vecOrigin, matRotate ); trace_t tr; float flScale = LINE_LENGTH * flDeviation; // For each beam, find its offset and trace outwards to place its endpoint for ( int i = 0; i < NUM_ANCILLARY_BEAMS; i++ ) { if ( flConvergencePerc >= 0.99f ) { m_pAncillaryBeams[i]->TurnOn(); continue; } m_pAncillaryBeams[i]->TurnOff(); // Find the number of radians offset we are flOffset = (float) i * flRotationOffset + DEG2RAD( 30.0f ); flOffset += (M_PI/8.0f) * sin( gpGlobals->curtime * 3.0f ); // Construct a circle that's also offset by the line's length vecOffset.x = cos( flOffset ) * flScale; vecOffset.y = sin( flOffset ) * flScale; vecOffset.z = LINE_LENGTH; // Rotate this whole thing into the space of the basis vector VectorRotate( vecOffset, matRotate, vecFinal ); VectorNormalize( vecFinal ); // Trace a line down that vector to find where we'll eventually stop our line UTIL_TraceLine( vecOrigin, vecOrigin + ( vecFinal * LINE_LENGTH ), MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); // Move the beam to that position m_pAncillaryBeams[i]->SetBrightness( 255.0f * flConvergencePerc ); m_pAncillaryBeams[i]->SetEndPos( tr.startpos ); m_pAncillaryBeams[i]->SetStartPos( tr.endpos ); } } //----------------------------------------------------------------------------- // Sweep the laser sight towards the point where the gun should be aimed //----------------------------------------------------------------------------- void CNPC_Combine_Cannon::PaintTarget( const Vector &vecTarget, float flPaintTime ) { // vecStart is the barrel of the gun (or the laser sight) Vector vecStart = GetBulletOrigin(); // keep painttime from hitting 0 exactly. flPaintTime = MAX( flPaintTime, 0.000001f ); // Find out where we are in the arc of the paint duration float flPaintPerc = GetWaitTimePercentage( flPaintTime, false ); ScopeGlint(); // Find out where along our line we're painting Vector vecCurrentDir; float flInterp = RemapValClamped( flPaintPerc, 0.0f, 0.5f, 0.0f, 1.0f ); flInterp = clamp( flInterp, 0.0f, 1.0f ); GetPaintAim( m_vecPaintStart, vecTarget, flInterp, &vecCurrentDir ); #define THRESHOLD 0.9f float flNoiseScale; if ( flPaintPerc >= THRESHOLD ) { flNoiseScale = 1 - (1 / (1 - THRESHOLD)) * ( flPaintPerc - THRESHOLD ); } else if ( flPaintPerc <= 1 - THRESHOLD ) { flNoiseScale = flPaintPerc / (1 - THRESHOLD); } else { flNoiseScale = 1; } // mult by P vecCurrentDir.x += flNoiseScale * ( sin( 3 * M_PI * gpGlobals->curtime ) * 0.0006 ); vecCurrentDir.y += flNoiseScale * ( sin( 2 * M_PI * gpGlobals->curtime + 0.5 * M_PI ) * 0.0006 ); vecCurrentDir.z += flNoiseScale * ( sin( 1.5 * M_PI * gpGlobals->curtime + M_PI ) * 0.0006 ); // Find where our center is trace_t tr; UTIL_TraceLine( vecStart, vecStart + vecCurrentDir * 8192, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); m_vecPaintCursor = tr.endpos; // Update our beam position m_pBeam->SetEndPos( tr.startpos ); m_pBeam->SetStartPos( tr.endpos ); m_pBeam->SetBrightness( 255.0f * flPaintPerc ); m_pBeam->RelinkBeam(); // Find points around that center point and make our designators converge at that point over time UpdateAncillaryBeams( flPaintPerc, vecStart, vecCurrentDir ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_Combine_Cannon::OnScheduleChange( void ) { LaserOff(); m_hBarrageTarget = NULL; BaseClass::OnScheduleChange(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_Combine_Cannon::Precache( void ) { PrecacheModel("models/combine_soldier.mdl"); PrecacheModel("effects/bluelaser1.vmt"); gHaloTexture = PrecacheModel("sprites/light_glow03.vmt"); PrecacheScriptSound( "NPC_Combine_Cannon.FireBullet" ); BaseClass::Precache(); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CNPC_Combine_Cannon::Spawn( void ) { Precache(); /// HACK: SetModel( "models/combine_soldier.mdl" ); // Setup our ancillary beams but keep them hidden for now CreateLaser(); CreateAncillaryBeams(); m_iAmmoType = GetAmmoDef()->Index( "CombineHeavyCannon" ); SetHullType( HULL_HUMAN ); SetHullSizeNormal(); UTIL_SetSize( this, Vector( -16, -16 , 0 ), Vector( 16, 16, 64 ) ); SetSolid( SOLID_BBOX ); AddSolidFlags( FSOLID_NOT_STANDABLE ); SetMoveType( MOVETYPE_FLY ); m_bloodColor = DONT_BLEED; m_iHealth = 10; m_flFieldOfView = DOT_45DEGREE; m_NPCState = NPC_STATE_NONE; if( HasSpawnFlags( SF_STARTDISABLED ) ) { m_fEnabled = false; } else { m_fEnabled = true; } CapabilitiesClear(); CapabilitiesAdd( bits_CAP_INNATE_RANGE_ATTACK1 | bits_CAP_SIMPLE_RADIUS_DAMAGE ); m_HackedGunPos = Vector ( 0, 0, 0 ); AddSpawnFlags( SF_NPC_LONG_RANGE | SF_NPC_ALWAYSTHINK ); NPCInit(); // Limit our look distance SetDistLook( m_flSightDist ); AddEffects( EF_NODRAW ); AddSolidFlags( FSOLID_NOT_SOLID ); // Point the cursor straight ahead so that the sniper's // first sweep of the laser doesn't look weird. Vector vecForward; AngleVectors( GetLocalAngles(), &vecForward ); m_vecPaintCursor = GetBulletOrigin() + vecForward * 1024; // none! GetEnemies()->SetFreeKnowledgeDuration( 0.0f ); GetEnemies()->SetEnemyDiscardTime( 2.0f ); m_flTimeLastAttackedPlayer = 0.0f; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- Class_T CNPC_Combine_Cannon::Classify( void ) { if ( m_fEnabled ) return CLASS_COMBINE; return CLASS_NONE; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- Vector CNPC_Combine_Cannon::GetBulletOrigin( void ) { return GetAbsOrigin(); } //----------------------------------------------------------------------------- // Purpose: Nothing kills the cannon but entity I/O //----------------------------------------------------------------------------- int CNPC_Combine_Cannon::OnTakeDamage_Alive( const CTakeDamageInfo &info ) { // We are invulnerable to normal attacks for the moment return 0; } //--------------------------------------------------------- // Purpose: //--------------------------------------------------------- void CNPC_Combine_Cannon::UpdateOnRemove( void ) { // Remove the main laser if ( m_pBeam != NULL ) { UTIL_Remove( m_pBeam); m_pBeam = NULL; } // Remove our ancillary beams for ( int i = 0; i < NUM_ANCILLARY_BEAMS; i++ ) { if ( m_pAncillaryBeams[i] == NULL ) continue; UTIL_Remove( m_pAncillaryBeams[i] ); m_pAncillaryBeams[i] = NULL; } BaseClass::UpdateOnRemove(); } //--------------------------------------------------------- // Purpose: //--------------------------------------------------------- int CNPC_Combine_Cannon::SelectSchedule ( void ) { // Fire at our target if( GetEnemy() && HasCondition( COND_CAN_RANGE_ATTACK1 ) ) return SCHED_RANGE_ATTACK1; // Wait for a target // TODO: Sweep like a sniper? return SCHED_COMBAT_STAND; } //--------------------------------------------------------- // Purpose: //--------------------------------------------------------- bool CNPC_Combine_Cannon::FCanCheckAttacks ( void ) { return true; } //--------------------------------------------------------- //--------------------------------------------------------- bool CNPC_Combine_Cannon::VerifyShot( CBaseEntity *pTarget ) { trace_t tr; Vector vecTarget = DesiredBodyTarget( pTarget ); UTIL_TraceLine( GetBulletOrigin(), vecTarget, MASK_SHOT, pTarget, COLLISION_GROUP_NONE, &tr ); if( tr.fraction != 1.0 ) { if( pTarget->IsPlayer() ) { // if the target is the player, do another trace to see if we can shoot his eyeposition. This should help // improve sniper responsiveness in cases where the player is hiding his chest from the sniper with his // head in full view. UTIL_TraceLine( GetBulletOrigin(), pTarget->EyePosition(), MASK_SHOT, pTarget, COLLISION_GROUP_NONE, &tr ); if( tr.fraction == 1.0 ) { return true; } } // Trace hit something. if( tr.m_pEnt ) { if( tr.m_pEnt->m_takedamage == DAMAGE_YES ) { // Just shoot it if I can hurt it. Probably a breakable or glass pane. return true; } } return false; } else { return true; } } //--------------------------------------------------------- //--------------------------------------------------------- int CNPC_Combine_Cannon::RangeAttack1Conditions( float flDot, float flDist ) { if ( GetNextAttack() > gpGlobals->curtime ) return COND_NONE; if ( HasCondition( COND_SEE_ENEMY ) && !HasCondition( COND_ENEMY_OCCLUDED ) ) { if ( VerifyShot( GetEnemy() ) ) { // Can see the enemy, have a clear shot to his midsection ClearCondition( COND_CANNON_NO_SHOT ); return COND_CAN_RANGE_ATTACK1; } else { // Can see the enemy, but can't take a shot at his midsection SetCondition( COND_CANNON_NO_SHOT ); return COND_NONE; } } return COND_NONE; } //--------------------------------------------------------- //--------------------------------------------------------- int CNPC_Combine_Cannon::TranslateSchedule( int scheduleType ) { switch( scheduleType ) { case SCHED_RANGE_ATTACK1: return SCHED_CANNON_ATTACK; break; } return BaseClass::TranslateSchedule( scheduleType ); } //--------------------------------------------------------- //--------------------------------------------------------- void CNPC_Combine_Cannon::ScopeGlint( void ) { CEffectData data; data.m_vOrigin = GetAbsOrigin(); data.m_vNormal = vec3_origin; data.m_vAngles = vec3_angle; data.m_nColor = COMMAND_POINT_BLUE; DispatchEffect( "CommandPointer", data ); } //----------------------------------------------------------------------------- // Purpose: // Input : *vecIn - //----------------------------------------------------------------------------- void CNPC_Combine_Cannon::AdjustShotPosition( CBaseEntity *pTarget, Vector *vecIn ) { if ( pTarget == NULL || vecIn == NULL ) return; Vector low = pTarget->WorldSpaceCenter() - ( pTarget->WorldSpaceCenter() - pTarget->GetAbsOrigin() ) * .25; Vector high = pTarget->EyePosition(); Vector delta = high - low; Vector result = low + delta * 0.5; // Only take the height (*vecIn)[2] = result[2]; } //--------------------------------------------------------- // This starts the bullet state machine. The actual effects // of the bullet will happen later. This function schedules // those effects. // // fDirectShot indicates whether the bullet is a "direct shot" // that is - fired with the intent that it will strike the // enemy. Otherwise, the bullet is intended to strike a // decoy object or nothing at all in particular. //--------------------------------------------------------- bool CNPC_Combine_Cannon::FireBullet( const Vector &vecTarget, bool bDirectShot ) { Vector vecBulletOrigin = GetBulletOrigin(); Vector vecDir = ( vecTarget - vecBulletOrigin ); VectorNormalize( vecDir ); FireBulletsInfo_t info; info.m_iShots = 1; info.m_iTracerFreq = 1.0f; info.m_vecDirShooting = vecDir; info.m_vecSrc = vecBulletOrigin; info.m_flDistance = MAX_TRACE_LENGTH; info.m_pAttacker = this; info.m_iAmmoType = m_iAmmoType; info.m_iPlayerDamage = 20.0f; info.m_vecSpread = Vector( 0.015f, 0.015f, 0.015f ); // medium cone FireBullets( info ); EmitSound( "NPC_Combine_Cannon.FireBullet" ); // Don't attack for a certain amount of time SetNextAttack( gpGlobals->curtime + GetRefireTime() ); // Sniper had to be aiming here to fire here, so make it the cursor m_vecPaintCursor = vecTarget; LaserOff(); return true; } //--------------------------------------------------------- //--------------------------------------------------------- void CNPC_Combine_Cannon::StartTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_CANNON_ATTACK_CURSOR: break; case TASK_RANGE_ATTACK1: // Setup the information for this barrage m_flBarrageDuration = gpGlobals->curtime + random->RandomFloat( 0.25f, 0.5f ); m_hBarrageTarget = GetEnemy(); break; case TASK_CANNON_PAINT_ENEMY: { if ( GetEnemy()->IsPlayer() ) { float delay = random->RandomFloat( 0.0f, 0.3f ); if ( ( gpGlobals->curtime - m_flTimeLastAttackedPlayer ) < 1.0f ) { SetWait( CANNON_SUBSEQUENT_PAINT_TIME ); m_flPaintTime = CANNON_SUBSEQUENT_PAINT_TIME; } else { SetWait( CANNON_PAINT_ENEMY_TIME + delay ); m_flPaintTime = CANNON_PAINT_ENEMY_TIME + delay; } } else { // Use a random time m_flPaintTime = CANNON_PAINT_ENEMY_TIME + random->RandomFloat( 0, CANNON_PAINT_NPC_TIME_NOISE ); SetWait( m_flPaintTime ); } // Try to start the laser where the player can't miss seeing it! Vector vecCursor; AngleVectors( GetEnemy()->GetLocalAngles(), &vecCursor ); vecCursor *= 300; vecCursor += GetEnemy()->EyePosition(); LaserOn( vecCursor, Vector( 16, 16, 16 ) ); } break; default: BaseClass::StartTask( pTask ); break; } } //--------------------------------------------------------- //--------------------------------------------------------- void CNPC_Combine_Cannon::RunTask( const Task_t *pTask ) { switch( pTask->iTask ) { case TASK_CANNON_ATTACK_CURSOR: if( FireBullet( m_vecPaintCursor, true ) ) { TaskComplete(); } break; case TASK_RANGE_ATTACK1: { // Where we're focusing our fire Vector vecTarget = ( m_hBarrageTarget == NULL ) ? m_vecPaintCursor : LeadTarget( m_hBarrageTarget ); // Fire at enemy if ( FireBullet( vecTarget, true ) ) { bool bPlayerIsEnemy = ( m_hBarrageTarget && m_hBarrageTarget->IsPlayer() ); bool bBarrageFinished = m_flBarrageDuration < gpGlobals->curtime; bool bNoShot = ( QuerySeeEntity( m_hBarrageTarget ) == false ); // FIXME: Store this info off better bool bSeePlayer = HasCondition( COND_SEE_PLAYER ); // Treat the player differently to normal NPCs if ( bPlayerIsEnemy ) { // Store the last time we shot for doing an abbreviated attack telegraph m_flTimeLastAttackedPlayer = gpGlobals->curtime; // If we've got no shot and we're done with our current barrage if ( bNoShot && bBarrageFinished ) { TaskComplete(); } } else if ( bBarrageFinished || bSeePlayer ) { // Done with the barrage or we saw the player as a better target TaskComplete(); } } } break; case TASK_CANNON_PAINT_ENEMY: { // See if we're done painting our target if ( IsWaitFinished() ) { TaskComplete(); } // Continue to paint the target PaintTarget( LeadTarget( GetEnemy() ), m_flPaintTime ); } break; default: BaseClass::RunTask( pTask ); break; } } //----------------------------------------------------------------------------- // The sniper throws away the circular list of old decoys when we restore. //----------------------------------------------------------------------------- int CNPC_Combine_Cannon::Restore( IRestore &restore ) { return BaseClass::Restore( restore ); } //----------------------------------------------------------------------------- // Purpose: // // //----------------------------------------------------------------------------- float CNPC_Combine_Cannon::MaxYawSpeed( void ) { return 60; } //--------------------------------------------------------- //--------------------------------------------------------- void CNPC_Combine_Cannon::PrescheduleThink( void ) { BaseClass::PrescheduleThink(); // NOTE: We'll deal with this on the client // Think faster if the beam is on, this gives the beam higher resolution. if( m_pBeam ) { SetNextThink( gpGlobals->curtime + 0.03 ); } else { SetNextThink( gpGlobals->curtime + 0.1f ); } // If the enemy has just stepped into view, or we've acquired a new enemy, // Record the last time we've seen the enemy as right now. // // If the enemy has been out of sight for a full second, mark him eluded. if( GetEnemy() != NULL ) { if( gpGlobals->curtime - GetEnemies()->LastTimeSeen( GetEnemy() ) > 30 ) { // Stop pestering enemies after 30 seconds of frustration. GetEnemies()->ClearMemory( GetEnemy() ); SetEnemy(NULL); } } } //--------------------------------------------------------- //--------------------------------------------------------- Vector CNPC_Combine_Cannon::EyePosition( void ) { return GetAbsOrigin(); } //--------------------------------------------------------- //--------------------------------------------------------- Vector CNPC_Combine_Cannon::DesiredBodyTarget( CBaseEntity *pTarget ) { // By default, aim for the center Vector vecTarget = pTarget->WorldSpaceCenter(); float flTimeSinceLastMiss = gpGlobals->curtime - m_flTimeLastShotMissed; if( pTarget->GetFlags() & FL_CLIENT ) { if( !BaseClass::FVisible( vecTarget ) ) { // go to the player's eyes if his center is concealed. // Bump up an inch so the player's not looking straight down a beam. vecTarget = pTarget->EyePosition() + Vector( 0, 0, 1 ); } } else { if( pTarget->Classify() == CLASS_HEADCRAB ) { // Headcrabs are tiny inside their boxes. vecTarget = pTarget->GetAbsOrigin(); vecTarget.z += 4.0; } else if( pTarget->Classify() == CLASS_ZOMBIE ) { if( flTimeSinceLastMiss > 0.0f && flTimeSinceLastMiss < 4.0f && hl2_episodic.GetBool() ) { vecTarget = pTarget->BodyTarget( GetBulletOrigin(), false ); } else { // Shoot zombies in the headcrab vecTarget = pTarget->HeadTarget( GetBulletOrigin() ); } } else if( pTarget->Classify() == CLASS_ANTLION ) { // Shoot about a few inches above the origin. This makes it easy to hit antlions // even if they are on their backs. vecTarget = pTarget->GetAbsOrigin(); vecTarget.z += 18.0f; } else if( pTarget->Classify() == CLASS_EARTH_FAUNA ) { // Shoot birds in the center } else { // Shoot NPCs in the chest vecTarget.z += 8.0f; } } return vecTarget; } //--------------------------------------------------------- //--------------------------------------------------------- Vector CNPC_Combine_Cannon::LeadTarget( CBaseEntity *pTarget ) { if ( pTarget != NULL ) { Vector vecFuturePos; UTIL_PredictedPosition( pTarget, 0.05f, &vecFuturePos ); AdjustShotPosition( pTarget, &vecFuturePos ); return vecFuturePos; } return vec3_origin; } //--------------------------------------------------------- //--------------------------------------------------------- void CNPC_Combine_Cannon::InputEnableSniper( inputdata_t &inputdata ) { ClearCondition( COND_CANNON_DISABLED ); SetCondition( COND_CANNON_ENABLED ); m_fEnabled = true; } //--------------------------------------------------------- //--------------------------------------------------------- void CNPC_Combine_Cannon::InputDisableSniper( inputdata_t &inputdata ) { ClearCondition( COND_CANNON_ENABLED ); SetCondition( COND_CANNON_DISABLED ); m_fEnabled = false; } //--------------------------------------------------------- // See all NPC's easily. // // Only see the player if you can trace to both of his // eyeballs. That is, allow the player to peek around corners. // This is a little more expensive than the base class' check! //--------------------------------------------------------- #define CANNON_EYE_DIST 0.75 #define CANNON_TARGET_VERTICAL_OFFSET Vector( 0, 0, 5 ); bool CNPC_Combine_Cannon::FVisible( CBaseEntity *pEntity, int traceMask, CBaseEntity **ppBlocker ) { // NPC if ( pEntity->IsPlayer() == false ) return BaseClass::FVisible( pEntity, traceMask, ppBlocker ); if ( pEntity->GetFlags() & FL_NOTARGET ) return false; Vector vecVerticalOffset; Vector vecRight; Vector vecEye; trace_t tr; if( fabs( GetAbsOrigin().z - pEntity->WorldSpaceCenter().z ) <= 120.f ) { // If the player is around the same elevation, look straight at his eyes. // At the same elevation, the vertical peeking allowance makes it too easy // for a player to dispatch the sniper from cover. vecVerticalOffset = vec3_origin; } else { // Otherwise, look at a spot below his eyes. This allows the player to back away // from his cover a bit and have a peek at the sniper without being detected. vecVerticalOffset = CANNON_TARGET_VERTICAL_OFFSET; } AngleVectors( pEntity->GetLocalAngles(), NULL, &vecRight, NULL ); vecEye = vecRight * CANNON_EYE_DIST - vecVerticalOffset; UTIL_TraceLine( EyePosition(), pEntity->EyePosition() + vecEye, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); #if 0 NDebugOverlay::Line(EyePosition(), tr.endpos, 0,255,0, true, 0.1); #endif bool fCheckFailed = false; if ( tr.fraction != 1.0 && tr.m_pEnt != pEntity ) { fCheckFailed = true; } // Don't check the other eye if the first eye failed. if( !fCheckFailed ) { vecEye = -vecRight * CANNON_EYE_DIST - vecVerticalOffset; UTIL_TraceLine( EyePosition(), pEntity->EyePosition() + vecEye, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); #if 0 NDebugOverlay::Line(EyePosition(), tr.endpos, 0,255,0, true, 0.1); #endif if ( tr.fraction != 1.0 && tr.m_pEnt != pEntity ) { fCheckFailed = true; } } if( !fCheckFailed ) { // Can see the player. return true; } // Now, if the check failed, see if the player is ducking and has recently // fired a muzzleflash. If yes, see if you'd be able to see the player if // they were standing in their current position instead of ducking. Since // the sniper doesn't have a clear shot in this situation, he will harrass // near the player. CBasePlayer *pPlayer; pPlayer = ToBasePlayer( pEntity ); if( (pPlayer->GetFlags() & FL_DUCKING) && pPlayer->MuzzleFlashTime() > gpGlobals->curtime ) { vecEye = pPlayer->EyePosition() + Vector( 0, 0, 32 ); UTIL_TraceLine( EyePosition(), vecEye, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); if( tr.fraction != 1.0 ) { // Everything failed. if (ppBlocker) { *ppBlocker = tr.m_pEnt; } return false; } else { // Fake being able to see the player. return true; } } if (ppBlocker) { *ppBlocker = tr.m_pEnt; } return false; } //----------------------------------------------------------------------------- // // Schedules // //----------------------------------------------------------------------------- AI_BEGIN_CUSTOM_NPC( npc_combine_cannon, CNPC_Combine_Cannon ) DECLARE_CONDITION( COND_CANNON_ENABLED ); DECLARE_CONDITION( COND_CANNON_DISABLED ); DECLARE_CONDITION( COND_CANNON_NO_SHOT ); DECLARE_TASK( TASK_CANNON_PAINT_ENEMY ); DECLARE_TASK( TASK_CANNON_ATTACK_CURSOR ); //========================================================= // CAMP //========================================================= DEFINE_SCHEDULE ( SCHED_CANNON_CAMP, " Tasks" " TASK_WAIT 1" " " " Interrupts" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" " COND_CAN_RANGE_ATTACK1" " COND_HEAR_DANGER" " COND_CANNON_DISABLED" ) //========================================================= // ATTACK //========================================================= DEFINE_SCHEDULE ( SCHED_CANNON_ATTACK, " Tasks" " TASK_CANNON_PAINT_ENEMY 0" " TASK_RANGE_ATTACK1 0" " " " Interrupts" " COND_HEAR_DANGER" " COND_CANNON_DISABLED" ) //========================================================= // ATTACK //========================================================= DEFINE_SCHEDULE ( SCHED_CANNON_SNAPATTACK, " Tasks" " TASK_CANNON_ATTACK_CURSOR 0" " " " Interrupts" " COND_ENEMY_OCCLUDED" " COND_ENEMY_DEAD" " COND_NEW_ENEMY" " COND_HEAR_DANGER" " COND_CANNON_DISABLED" ) //========================================================= // Sniper is allowed to process a couple conditions while // disabled, but mostly he waits until he's enabled. //========================================================= DEFINE_SCHEDULE ( SCHED_CANNON_DISABLEDWAIT, " Tasks" " TASK_WAIT 0.5" " " " Interrupts" " COND_CANNON_ENABLED" " COND_NEW_ENEMY" " COND_ENEMY_DEAD" ) AI_END_CUSTOM_NPC()