diff options
Diffstat (limited to 'game/server/hl2/npc_turret_floor.cpp')
| -rw-r--r-- | game/server/hl2/npc_turret_floor.cpp | 2330 |
1 files changed, 2330 insertions, 0 deletions
diff --git a/game/server/hl2/npc_turret_floor.cpp b/game/server/hl2/npc_turret_floor.cpp new file mode 100644 index 0000000..d934e72 --- /dev/null +++ b/game/server/hl2/npc_turret_floor.cpp @@ -0,0 +1,2330 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "cbase.h" +#include "npc_turret_floor.h" +#include "ai_senses.h" +#include "ai_memory.h" +#include "engine/IEngineSound.h" +#include "ammodef.h" +#include "hl2/hl2_player.h" +#include "soundenvelope.h" +#include "physics_saverestore.h" +#include "IEffects.h" +#include "basehlcombatweapon_shared.h" +#include "phys_controller.h" +#include "ai_interactions.h" +#include "Sprite.h" +#include "beam_shared.h" +#include "props.h" +#include "particle_parse.h" + +#ifdef PORTAL + #include "prop_portal_shared.h" + #include "portal_util_shared.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +const char *GetMassEquivalent(float flMass); + +#define DISABLE_SHOT 0 + +//Debug visualization +ConVar g_debug_turret( "g_debug_turret", "0" ); + +extern ConVar physcannon_tracelength; + +// Interactions +int g_interactionTurretStillStanding = 0; + +float CNPC_FloorTurret::fMaxTipControllerVelocity = 300.0f * 300.0f; +float CNPC_FloorTurret::fMaxTipControllerAngularVelocity = 90.0f * 90.0f; + +#define LASER_BEAM_SPRITE "effects/laser1.vmt" + +#define FLOOR_TURRET_MODEL "models/combine_turrets/floor_turret.mdl" +#define FLOOR_TURRET_MODEL_CITIZEN "models/combine_turrets/citizen_turret.mdl" +#define FLOOR_TURRET_GLOW_SPRITE "sprites/glow1.vmt" +// #define FLOOR_TURRET_BC_YAW "aim_yaw" +// #define FLOOR_TURRET_BC_PITCH "aim_pitch" +#define FLOOR_TURRET_RANGE 1200 +#define FLOOR_TURRET_MAX_WAIT 5 +#define FLOOR_TURRET_SHORT_WAIT 2.0 // Used for FAST_RETIRE spawnflag +#define FLOOR_TURRET_PING_TIME 1.0f //LPB!! + +#define FLOOR_TURRET_VOICE_PITCH_LOW 45 +#define FLOOR_TURRET_VOICE_PITCH_HIGH 100 + +//Aiming variables +#define FLOOR_TURRET_MAX_NOHARM_PERIOD 0.0f +#define FLOOR_TURRET_MAX_GRACE_PERIOD 3.0f + +//Activities +int ACT_FLOOR_TURRET_OPEN; +int ACT_FLOOR_TURRET_CLOSE; +int ACT_FLOOR_TURRET_OPEN_IDLE; +int ACT_FLOOR_TURRET_CLOSED_IDLE; +int ACT_FLOOR_TURRET_FIRE; + +//Datatable +BEGIN_DATADESC( CNPC_FloorTurret ) + + DEFINE_FIELD( m_iAmmoType, FIELD_INTEGER ), + DEFINE_FIELD( m_bAutoStart, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bActive, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bBlinkState, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bNoAlarmSounds, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_flShotTime, FIELD_TIME ), + DEFINE_FIELD( m_flLastSight, FIELD_TIME ), + DEFINE_FIELD( m_flThrashTime, FIELD_TIME ), + DEFINE_FIELD( m_flPingTime, FIELD_TIME ), + DEFINE_FIELD( m_flNextActivateSoundTime, FIELD_TIME ), + DEFINE_FIELD( m_bCarriedByPlayer, FIELD_BOOLEAN ), + DEFINE_FIELD( m_bUseCarryAngles, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flPlayerDropTime, FIELD_TIME ), + DEFINE_FIELD( m_hLastNPCToKickMe, FIELD_EHANDLE ), + DEFINE_FIELD( m_flKnockOverFailedTime, FIELD_TIME ), + DEFINE_FIELD( m_flDestructStartTime, FIELD_TIME ), + DEFINE_FIELD( m_hFizzleEffect, FIELD_EHANDLE ), + + DEFINE_FIELD( m_vecGoalAngles,FIELD_VECTOR ), + DEFINE_FIELD( m_iEyeAttachment, FIELD_INTEGER ), + DEFINE_FIELD( m_iMuzzleAttachment, FIELD_INTEGER ), + DEFINE_FIELD( m_iEyeState, FIELD_INTEGER ), + DEFINE_FIELD( m_hEyeGlow, FIELD_EHANDLE ), + DEFINE_FIELD( m_pMotionController,FIELD_EHANDLE), + DEFINE_FIELD( m_vecEnemyLKP, FIELD_VECTOR ), + DEFINE_FIELD( m_hLaser, FIELD_EHANDLE ), + DEFINE_FIELD( m_bSelfDestructing, FIELD_BOOLEAN ), + + DEFINE_FIELD( m_hPhysicsAttacker, FIELD_EHANDLE ), + DEFINE_FIELD( m_flLastPhysicsInfluenceTime, FIELD_TIME ), + + DEFINE_FIELD( m_bHackedByAlyx, FIELD_BOOLEAN ), + + DEFINE_KEYFIELD( m_iKeySkin, FIELD_INTEGER, "SkinNumber" ), + + DEFINE_THINKFUNC( Retire ), + DEFINE_THINKFUNC( Deploy ), + DEFINE_THINKFUNC( ActiveThink ), + DEFINE_THINKFUNC( SearchThink ), + DEFINE_THINKFUNC( AutoSearchThink ), + DEFINE_THINKFUNC( TippedThink ), + DEFINE_THINKFUNC( InactiveThink ), + DEFINE_THINKFUNC( SuppressThink ), + DEFINE_THINKFUNC( DisabledThink ), + DEFINE_THINKFUNC( SelfDestructThink ), + DEFINE_THINKFUNC( BreakThink ), + + DEFINE_USEFUNC( ToggleUse ), + + // Inputs + DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), + DEFINE_INPUTFUNC( FIELD_VOID, "Enable", InputEnable ), + DEFINE_INPUTFUNC( FIELD_VOID, "Disable", InputDisable ), + DEFINE_INPUTFUNC( FIELD_VOID, "DepleteAmmo", InputDepleteAmmo ), + DEFINE_INPUTFUNC( FIELD_VOID, "RestoreAmmo", InputRestoreAmmo ), + DEFINE_INPUTFUNC( FIELD_VOID, "SelfDestruct", InputSelfDestruct ), + + DEFINE_OUTPUT( m_OnDeploy, "OnDeploy" ), + DEFINE_OUTPUT( m_OnRetire, "OnRetire" ), + DEFINE_OUTPUT( m_OnTipped, "OnTipped" ), + DEFINE_OUTPUT( m_OnPhysGunPickup, "OnPhysGunPickup" ), + DEFINE_OUTPUT( m_OnPhysGunDrop, "OnPhysGunDrop" ), + + DEFINE_BASENPCINTERACTABLE_DATADESC(), + + // DEFINE_FIELD( m_ShotSounds, FIELD_SHORT ), + +END_DATADESC() + +LINK_ENTITY_TO_CLASS( npc_turret_floor, CNPC_FloorTurret ); + +//----------------------------------------------------------------------------- +// Constructor +//----------------------------------------------------------------------------- +CNPC_FloorTurret::CNPC_FloorTurret( void ) : + m_bActive( false ), + m_hEyeGlow( NULL ), + m_hLaser( NULL ), + m_iAmmoType( -1 ), + m_bAutoStart( false ), + m_flPingTime( 0.0f ), + m_flNextActivateSoundTime( 0.0f ), + m_bCarriedByPlayer( false ), + m_bUseCarryAngles( false ), + m_flPlayerDropTime( 0.0f ), + m_flShotTime( 0.0f ), + m_flLastSight( 0.0f ), + m_bBlinkState( false ), + m_flThrashTime( 0.0f ), + m_pMotionController( NULL ), + m_bEnabled( false ), + m_bSelfDestructing( false ) +{ + m_vecGoalAngles.Init(); + + m_vecEnemyLKP = vec3_invalid; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +Class_T CNPC_FloorTurret::Classify( void ) +{ + if ( m_bEnabled ) + { + // Hacked or friendly turrets don't attack players + if( m_bHackedByAlyx || IsCitizenTurret() ) + return CLASS_PLAYER_ALLY; + + return CLASS_COMBINE; + } + + return CLASS_NONE; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::UpdateOnRemove( void ) +{ + if ( m_pMotionController != NULL ) + { + UTIL_Remove( m_pMotionController ); + m_pMotionController = NULL; + } + + if ( m_hLaser != NULL ) + { + UTIL_Remove( m_hLaser ); + m_hLaser = NULL; + } + + if ( m_hEyeGlow != NULL ) + { + UTIL_Remove( m_hEyeGlow ); + m_hEyeGlow = NULL; + } + + BaseClass::UpdateOnRemove(); +} + +//----------------------------------------------------------------------------- +// Purpose: Precache +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::Precache( void ) +{ + const char *pModelName = STRING( GetModelName() ); + pModelName = ( pModelName && pModelName[ 0 ] != '\0' ) ? pModelName : FLOOR_TURRET_MODEL; + PrecacheModel( pModelName ); + PrecacheModel( FLOOR_TURRET_GLOW_SPRITE ); + + PropBreakablePrecacheAll( MAKE_STRING( pModelName ) ); + + if ( IsCitizenTurret() ) + { + PrecacheModel( LASER_BEAM_SPRITE ); + PrecacheScriptSound( "NPC_FloorTurret.AlarmPing"); + } + + // Activities + ADD_CUSTOM_ACTIVITY( CNPC_FloorTurret, ACT_FLOOR_TURRET_OPEN ); + ADD_CUSTOM_ACTIVITY( CNPC_FloorTurret, ACT_FLOOR_TURRET_CLOSE ); + ADD_CUSTOM_ACTIVITY( CNPC_FloorTurret, ACT_FLOOR_TURRET_CLOSED_IDLE ); + ADD_CUSTOM_ACTIVITY( CNPC_FloorTurret, ACT_FLOOR_TURRET_OPEN_IDLE ); + ADD_CUSTOM_ACTIVITY( CNPC_FloorTurret, ACT_FLOOR_TURRET_FIRE ); + + PrecacheScriptSound( "NPC_FloorTurret.Retire" ); + PrecacheScriptSound( "NPC_FloorTurret.Deploy" ); + PrecacheScriptSound( "NPC_FloorTurret.Move" ); + PrecacheScriptSound( "NPC_Combine.WeaponBash" ); + PrecacheScriptSound( "NPC_FloorTurret.Activate" ); + PrecacheScriptSound( "NPC_FloorTurret.Alert" ); + m_ShotSounds = PrecacheScriptSound( "NPC_FloorTurret.ShotSounds" ); + PrecacheScriptSound( "NPC_FloorTurret.Die" ); + PrecacheScriptSound( "NPC_FloorTurret.Retract"); + PrecacheScriptSound( "NPC_FloorTurret.Alarm"); + PrecacheScriptSound( "NPC_FloorTurret.Ping"); + PrecacheScriptSound( "NPC_FloorTurret.DryFire"); + PrecacheScriptSound( "NPC_FloorTurret.Destruct" ); + +#ifdef HL2_EPISODIC + PrecacheParticleSystem( "explosion_turret_break" ); +#endif // HL2_EPISODIC + + BaseClass::Precache(); +} + +//----------------------------------------------------------------------------- +// Purpose: Spawn the entity +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::Spawn( void ) +{ + Precache(); + + const char *pModelName = STRING( GetModelName() ); + SetModel( ( pModelName && pModelName[ 0 ] != '\0' ) ? pModelName : FLOOR_TURRET_MODEL ); + + // If we're a citizen turret, we use a different skin + if ( IsCitizenTurret() ) + { + if (m_iKeySkin == 0) + { // select a "random" skin -- rather than being truly random, use a static variable + // to cycle through them evenly. The static won't be saved across save/load, but + // frankly I don't care so much about that. + // m_nSkin = random->RandomInt( 1, 4 ); + static unsigned int nextSkin = 0; + m_nSkin = nextSkin + 1; + + // add one mod 4 + nextSkin = (nextSkin + 1) & 0x03; + } + else + { // at least make sure that it's in the right range + m_nSkin = clamp(m_iKeySkin,1,4); + } + } + + BaseClass::Spawn(); + + SetBlocksLOS( false ); + + m_HackedGunPos = Vector( 0, 0, 12.75 ); + SetViewOffset( EyeOffset( ACT_IDLE ) ); + m_flFieldOfView = 0.4f; // 60 degrees + m_takedamage = DAMAGE_EVENTS_ONLY; + m_iHealth = 100; + m_iMaxHealth = 100; + + AddEFlags( EFL_NO_DISSOLVE ); + + SetPoseParameter( m_poseAim_Yaw, 0 ); + SetPoseParameter( m_poseAim_Pitch, 0 ); + + m_iAmmoType = GetAmmoDef()->Index( "PISTOL" ); + + m_iMuzzleAttachment = LookupAttachment( "eyes" ); + m_iEyeAttachment = LookupAttachment( "light" ); + + // FIXME: Do we ever need m_bAutoStart? (Sawyer) + m_spawnflags |= SF_FLOOR_TURRET_AUTOACTIVATE; + + //Set our autostart state + m_bAutoStart = !!( m_spawnflags & SF_FLOOR_TURRET_AUTOACTIVATE ); + m_bEnabled = ( ( m_spawnflags & SF_FLOOR_TURRET_STARTINACTIVE ) == false ); + + //Do we start active? + if ( m_bAutoStart && m_bEnabled ) + { + SetThink( &CNPC_FloorTurret::AutoSearchThink ); + SetEyeState( TURRET_EYE_DORMANT ); + } + else + { + SetThink( &CNPC_FloorTurret::DisabledThink ); + SetEyeState( TURRET_EYE_DISABLED ); + } + + // Start + if ( OnSide() ) + { + SetThink( &CNPC_FloorTurret::DisabledThink ); + SetEyeState( TURRET_EYE_DISABLED ); + } + + //Stagger our starting times + SetNextThink( gpGlobals->curtime + random->RandomFloat( 0.1f, 0.3f ) ); + + SetUse( &CNPC_FloorTurret::ToggleUse ); + + // Don't allow us to skip animation setup because our attachments are critical to us! + SetBoneCacheFlags( BCF_NO_ANIMATION_SKIP ); + + CreateVPhysics(); + + SetState(NPC_STATE_IDLE); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::Activate( void ) +{ + BaseClass::Activate(); + + // Force the eye state to the current state so that our glows are recreated after transitions + SetEyeState( m_iEyeState ); + + if ( !m_pMotionController ) + { + // Create the motion controller + m_pMotionController = CTurretTipController::CreateTipController( this ); + + // Enable the controller + if ( m_pMotionController != NULL ) + { + m_pMotionController->Enable(); + } + } +} + +//----------------------------------------------------------------------------- + +bool CNPC_FloorTurret::CreateVPhysics( void ) +{ + //Spawn our physics hull + if ( VPhysicsInitNormal( SOLID_VPHYSICS, 0, false ) == NULL ) + { + DevMsg( "npc_turret_floor unable to spawn physics object!\n" ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Retract and stop attacking +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::Retire( void ) +{ + if ( PreThink( TURRET_RETIRING ) ) + return; + + //Level out the turret + m_vecGoalAngles = GetAbsAngles(); + SetNextThink( gpGlobals->curtime + 0.05f ); + + //Set ourselves to close + if ( GetActivity() != ACT_FLOOR_TURRET_CLOSE ) + { + //Set our visible state to dormant + SetEyeState( TURRET_EYE_DORMANT ); + + SetActivity( (Activity) ACT_FLOOR_TURRET_OPEN_IDLE ); + + //If we're done moving to our desired facing, close up + if ( UpdateFacing() == false ) + { + SetActivity( (Activity) ACT_FLOOR_TURRET_CLOSE ); + EmitSound( "NPC_FloorTurret.Retire" ); + + //Notify of the retraction + m_OnRetire.FireOutput( NULL, this ); + } + } + else if ( IsActivityFinished() ) + { + m_bActive = false; + m_flLastSight = 0; + + SetActivity( (Activity) ACT_FLOOR_TURRET_CLOSED_IDLE ); + + //Go back to auto searching + if ( m_bAutoStart ) + { + SetThink( &CNPC_FloorTurret::AutoSearchThink ); + SetNextThink( gpGlobals->curtime + 0.05f ); + } + else + { + //Set our visible state to dormant + SetEyeState( TURRET_EYE_DISABLED ); + SetThink( &CNPC_FloorTurret::DisabledThink ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Deploy and start attacking +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::Deploy( void ) +{ + if ( PreThink( TURRET_DEPLOYING ) ) + return; + + m_vecGoalAngles = GetAbsAngles(); + + SetNextThink( gpGlobals->curtime + 0.05f ); + + //Show we've seen a target + SetEyeState( TURRET_EYE_SEE_TARGET ); + + //Open if we're not already + if ( GetActivity() != ACT_FLOOR_TURRET_OPEN ) + { + m_bActive = true; + SetActivity( (Activity) ACT_FLOOR_TURRET_OPEN ); + EmitSound( "NPC_FloorTurret.Deploy" ); + + //Notify we're deploying + m_OnDeploy.FireOutput( NULL, this ); + } + + //If we're done, then start searching + if ( IsActivityFinished() ) + { + SetActivity( (Activity) ACT_FLOOR_TURRET_OPEN_IDLE ); + + m_flShotTime = gpGlobals->curtime + 1.0f; + + m_flPlaybackRate = 0; + SetThink( &CNPC_FloorTurret::SearchThink ); + + EmitSound( "NPC_FloorTurret.Move" ); + } + + m_flLastSight = gpGlobals->curtime + FLOOR_TURRET_MAX_WAIT; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + m_hPhysicsAttacker = pPhysGunUser; + m_flLastPhysicsInfluenceTime = gpGlobals->curtime; + + // Drop our mass a lot so that we can be moved easily with +USE + if ( reason != PUNTED_BY_CANNON ) + { + Assert( VPhysicsGetObject() ); + + m_bCarriedByPlayer = true; + m_OnPhysGunPickup.FireOutput( this, this ); + + // We want to use preferred carry angles if we're not nicely upright + Vector vecToTurret = pPhysGunUser->GetAbsOrigin() - GetAbsOrigin(); + vecToTurret.z = 0; + VectorNormalize( vecToTurret ); + + // We want to use preferred carry angles if we're not nicely upright + Vector forward, up; + GetVectors( &forward, NULL, &up ); + + bool bUpright = DotProduct( up, Vector(0,0,1) ) > 0.9f; + bool bBehind = DotProduct( vecToTurret, forward ) < 0.85f; + + // Correct our angles only if we're not upright or we're mostly behind the turret + if ( hl2_episodic.GetBool() ) + { + m_bUseCarryAngles = ( bUpright == false || bBehind ); + } + else + { + m_bUseCarryAngles = ( bUpright == false ); + } + } + + // Clear out our last NPC to kick me, because it makes no sense now + m_hLastNPCToKickMe = NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) +{ + m_hPhysicsAttacker = pPhysGunUser; + m_flLastPhysicsInfluenceTime = gpGlobals->curtime; + + m_bCarriedByPlayer = false; + m_bUseCarryAngles = false; + m_OnPhysGunDrop.FireOutput( this, this ); + + // If this is a friendly turret, remember that it was just dropped + if ( IRelationType( pPhysGunUser ) != D_HT ) + { + m_flPlayerDropTime = gpGlobals->curtime + 2.0; + } + + // Restore our mass to the original value + Assert( VPhysicsGetObject() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Whether this should return carry angles +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_FloorTurret::HasPreferredCarryAnglesForPlayer( CBasePlayer *pPlayer ) +{ + // Don't use preferred angles on enemy turrets + if ( IRelationType( pPlayer ) == D_HT ) + return false; + + return m_bUseCarryAngles; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_FloorTurret::OnAttemptPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) +{ + // Prevent players pulling enemy turrets from afar if they're in front of the turret + if ( reason == PICKED_UP_BY_CANNON && IRelationType( pPhysGunUser ) == D_HT ) + { + Vector vecForward; + GetVectors( &vecForward, NULL, NULL ); + Vector vecForce = (pPhysGunUser->GetAbsOrigin() - GetAbsOrigin()); + float flDistance = VectorNormalize( vecForce ); + + // If it's over the physcannon tracelength, we're pulling it + if ( flDistance > physcannon_tracelength.GetFloat() ) + { + float flDot = DotProduct( vecForward, vecForce ); + if ( flDot > 0.5 ) + return false; + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CNPC_FloorTurret::HandleInteraction(int interactionType, void *data, CBaseCombatCharacter *sourceEnt) +{ + if ( interactionType == g_interactionCombineBash ) + { + // We've been bashed by a combine soldier. Remember who it was, if we haven't got an active kicker + if ( !m_hLastNPCToKickMe ) + { + m_hLastNPCToKickMe = sourceEnt; + m_flKnockOverFailedTime = gpGlobals->curtime + 3.0; + } + + // Get knocked away + Vector forward, up; + AngleVectors( sourceEnt->GetLocalAngles(), &forward, NULL, &up ); + ApplyAbsVelocityImpulse( forward * 100 + up * 50 ); + CTakeDamageInfo info( sourceEnt, sourceEnt, 30, DMG_CLUB ); + CalculateMeleeDamageForce( &info, forward, GetAbsOrigin() ); + TakeDamage( info ); + + EmitSound( "NPC_Combine.WeaponBash" ); + return true; + } + + return BaseClass::HandleInteraction( interactionType, data, sourceEnt ); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the speed at which the turret can face a target +//----------------------------------------------------------------------------- +float CNPC_FloorTurret::MaxYawSpeed( void ) +{ + //TODO: Scale by difficulty? + return 360.0f; +} + +//----------------------------------------------------------------------------- +// Purpose: Return true if this turret was recently dropped by a player +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_FloorTurret::WasJustDroppedByPlayer( void ) +{ + if ( m_flPlayerDropTime > gpGlobals->curtime ) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Causes the turret to face its desired angles +//----------------------------------------------------------------------------- +bool CNPC_FloorTurret::UpdateFacing( void ) +{ + bool bMoved = false; + UpdateMuzzleMatrix(); + + Vector vecGoalDir; + AngleVectors( m_vecGoalAngles, &vecGoalDir ); + + Vector vecGoalLocalDir; + VectorIRotate( vecGoalDir, m_muzzleToWorld, vecGoalLocalDir ); + + if ( g_debug_turret.GetBool() ) + { + Vector vecMuzzle, vecMuzzleDir; + + MatrixGetColumn( m_muzzleToWorld, 3, vecMuzzle ); + MatrixGetColumn( m_muzzleToWorld, 0, vecMuzzleDir ); + + NDebugOverlay::Cross3D( vecMuzzle, -Vector(2,2,2), Vector(2,2,2), 255, 255, 0, false, 0.05 ); + NDebugOverlay::Cross3D( vecMuzzle+(vecMuzzleDir*256), -Vector(2,2,2), Vector(2,2,2), 255, 255, 0, false, 0.05 ); + NDebugOverlay::Line( vecMuzzle, vecMuzzle+(vecMuzzleDir*256), 255, 255, 0, false, 0.05 ); + + NDebugOverlay::Cross3D( vecMuzzle, -Vector(2,2,2), Vector(2,2,2), 255, 0, 0, false, 0.05 ); + NDebugOverlay::Cross3D( vecMuzzle+(vecGoalDir*256), -Vector(2,2,2), Vector(2,2,2), 255, 0, 0, false, 0.05 ); + NDebugOverlay::Line( vecMuzzle, vecMuzzle+(vecGoalDir*256), 255, 0, 0, false, 0.05 ); + } + + QAngle vecGoalLocalAngles; + VectorAngles( vecGoalLocalDir, vecGoalLocalAngles ); + + // Update pitch + float flDiff = AngleNormalize( UTIL_ApproachAngle( vecGoalLocalAngles.x, 0.0, 0.05f * MaxYawSpeed() ) ); + + SetPoseParameter( m_poseAim_Pitch, GetPoseParameter( m_poseAim_Pitch ) + ( flDiff / 1.5f ) ); + + if ( fabs( flDiff ) > 0.1f ) + { + bMoved = true; + } + + // Update yaw + flDiff = AngleNormalize( UTIL_ApproachAngle( vecGoalLocalAngles.y, 0.0, 0.05f * MaxYawSpeed() ) ); + + SetPoseParameter( m_poseAim_Yaw, GetPoseParameter( m_poseAim_Yaw ) + ( flDiff / 1.5f ) ); + + if ( fabs( flDiff ) > 0.1f ) + { + bMoved = true; + } + + // You're going to make decisions based on this info. So bump the bone cache after you calculate everything + InvalidateBoneCache(); + + return bMoved; +} + +void CNPC_FloorTurret::DryFire( void ) +{ + EmitSound( "NPC_FloorTurret.DryFire"); + EmitSound( "NPC_FloorTurret.Activate" ); + + if ( RandomFloat( 0, 1 ) > 0.5 ) + { + m_flShotTime = gpGlobals->curtime + random->RandomFloat( 1, 2.5 ); + } + else + { + m_flShotTime = gpGlobals->curtime; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Turret will continue to fire on a target's position when it loses sight of it +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::SuppressThink( void ) +{ + //Allow descended classes a chance to do something before the think function + if ( PreThink( TURRET_SUPPRESSING ) ) + return; + + //Update our think time + SetNextThink( gpGlobals->curtime + 0.1f ); + + // Look for a new enemy + HackFindEnemy(); + + //If we've acquired an enemy, start firing at it + if ( !GetEnemy() ) + { + SetThink( &CNPC_FloorTurret::ActiveThink ); + return; + } + + //See if we're done suppressing + if ( gpGlobals->curtime > m_flLastSight ) + { + // Should we look for a new target? + ClearEnemyMemory(); + SetEnemy( NULL ); + SetThink( &CNPC_FloorTurret::SearchThink ); + m_vecGoalAngles = GetAbsAngles(); + + SpinDown(); + + if ( m_spawnflags & SF_FLOOR_TURRET_FASTRETIRE ) + { + // Retire quickly in this case. (The case where we saw the player, but he hid again). + m_flLastSight = gpGlobals->curtime + FLOOR_TURRET_SHORT_WAIT; + } + else + { + m_flLastSight = gpGlobals->curtime + FLOOR_TURRET_MAX_WAIT; + } + + return; + } + + //Get our shot positions + Vector vecMid = EyePosition(); + Vector vecMidEnemy = m_vecEnemyLKP; + + //Calculate dir and dist to enemy + Vector vecDirToEnemy = vecMidEnemy - vecMid; + + //We want to look at the enemy's eyes so we don't jitter + Vector vecDirToEnemyEyes = vecMidEnemy - vecMid; + VectorNormalize( vecDirToEnemyEyes ); + + QAngle vecAnglesToEnemy; + VectorAngles( vecDirToEnemyEyes, vecAnglesToEnemy ); + + //Draw debug info + if ( g_debug_turret.GetBool() ) + { + NDebugOverlay::Cross3D( vecMid, -Vector(2,2,2), Vector(2,2,2), 0, 255, 0, false, 0.05 ); + NDebugOverlay::Cross3D( vecMidEnemy, -Vector(2,2,2), Vector(2,2,2), 0, 255, 0, false, 0.05 ); + NDebugOverlay::Line( vecMid, vecMidEnemy, 0, 255, 0, false, 0.05f ); + } + + if ( m_flShotTime < gpGlobals->curtime && m_vecEnemyLKP != vec3_invalid ) + { + Vector vecMuzzle, vecMuzzleDir; + UpdateMuzzleMatrix(); + MatrixGetColumn( m_muzzleToWorld, 0, vecMuzzleDir ); + MatrixGetColumn( m_muzzleToWorld, 3, vecMuzzle ); + + //Fire the gun + if ( DotProduct( vecDirToEnemy, vecMuzzleDir ) >= 0.9848 ) // 10 degree slop + { + if( m_spawnflags & SF_FLOOR_TURRET_OUT_OF_AMMO ) + { + DryFire(); + } + else + { + ResetActivity(); + SetActivity( (Activity) ACT_FLOOR_TURRET_FIRE ); + + //Fire the weapon +#if !DISABLE_SHOT + Shoot( vecMuzzle, vecMuzzleDir ); +#endif + } + } + } + else + { + SetActivity( (Activity) ACT_FLOOR_TURRET_OPEN_IDLE ); + } + + //If we can see our enemy, face it + m_vecGoalAngles.y = vecAnglesToEnemy.y; + m_vecGoalAngles.x = vecAnglesToEnemy.x; + + //Turn to face + UpdateFacing(); +} + +//----------------------------------------------------------------------------- +// Purpose: Allows the turret to fire on targets if they're visible +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::ActiveThink( void ) +{ + //Allow descended classes a chance to do something before the think function + if ( PreThink( TURRET_ACTIVE ) ) + return; + + HackFindEnemy(); + + //Update our think time + SetNextThink( gpGlobals->curtime + 0.1f ); + + //If we've become inactive, go back to searching + if ( ( m_bActive == false ) || ( GetEnemy() == NULL ) ) + { + SetEnemy( NULL ); + m_flLastSight = gpGlobals->curtime + FLOOR_TURRET_MAX_WAIT; + SetThink( &CNPC_FloorTurret::SearchThink ); + m_vecGoalAngles = GetAbsAngles(); + return; + } + + //Get our shot positions + Vector vecMid = EyePosition(); + Vector vecMidEnemy = GetEnemy()->BodyTarget( vecMid ); + + // Store off our last seen location so we can suppress it later + m_vecEnemyLKP = vecMidEnemy; + + //Look for our current enemy + bool bEnemyInFOV = FInViewCone( GetEnemy() ); + bool bEnemyVisible = FVisible( GetEnemy() ) && GetEnemy()->IsAlive(); + + // Robin: This is a hack to get around the fact that the muzzle for the turret + // is outside it's vcollide. This means that if it leans against a thin wall, + // the muzzle can be on the other side of the wall, where it's then able to see + // and shoot at targets. This check ensures that nothing has come between the + // center of the turret and the muzzle. + if ( bEnemyVisible ) + { + trace_t tr; + Vector vecCenter; + CollisionProp()->CollisionToWorldSpace( Vector(0,0,52), &vecCenter ); + UTIL_TraceLine( vecCenter, vecMid, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr ); + if ( tr.fraction != 1.0 ) + { + bEnemyVisible = false; + } + } + + //Calculate dir and dist to enemy + Vector vecDirToEnemy = vecMidEnemy - vecMid; + float flDistToEnemy = VectorNormalize( vecDirToEnemy ); + + //Draw debug info + if ( g_debug_turret.GetBool() ) + { + NDebugOverlay::Cross3D( vecMid, -Vector(2,2,2), Vector(2,2,2), 0, 255, 0, false, 0.05 ); + NDebugOverlay::Cross3D( GetEnemy()->WorldSpaceCenter(), -Vector(2,2,2), Vector(2,2,2), 0, 255, 0, false, 0.05 ); + NDebugOverlay::Line( vecMid, GetEnemy()->WorldSpaceCenter(), 0, 255, 0, false, 0.05 ); + + NDebugOverlay::Cross3D( vecMid, -Vector(2,2,2), Vector(2,2,2), 0, 255, 0, false, 0.05 ); + NDebugOverlay::Cross3D( vecMidEnemy, -Vector(2,2,2), Vector(2,2,2), 0, 255, 0, false, 0.05 ); + NDebugOverlay::Line( vecMid, vecMidEnemy, 0, 255, 0, false, 0.05f ); + } + + //See if they're past our FOV of attack + if ( bEnemyInFOV == false ) + { + // Should we look for a new target? + ClearEnemyMemory(); + SetEnemy( NULL ); + + if ( m_spawnflags & SF_FLOOR_TURRET_FASTRETIRE ) + { + // Retire quickly in this case. (The case where we saw the player, but he hid again). + m_flLastSight = gpGlobals->curtime + FLOOR_TURRET_SHORT_WAIT; + } + else + { + m_flLastSight = gpGlobals->curtime + FLOOR_TURRET_MAX_WAIT; + } + + SetThink( &CNPC_FloorTurret::SearchThink ); + m_vecGoalAngles = GetAbsAngles(); + + SpinDown(); + + return; + } + + //Current enemy is not visible + if ( ( bEnemyVisible == false ) || ( flDistToEnemy > FLOOR_TURRET_RANGE )) + { + m_flLastSight = gpGlobals->curtime + 2.0f; + + ClearEnemyMemory(); + SetEnemy( NULL ); + SetThink( &CNPC_FloorTurret::SuppressThink ); + + return; + } + + if ( g_debug_turret.GetBool() ) + { + Vector vecMuzzle, vecMuzzleDir; + + UpdateMuzzleMatrix(); + MatrixGetColumn( m_muzzleToWorld, 0, vecMuzzleDir ); + MatrixGetColumn( m_muzzleToWorld, 3, vecMuzzle ); + + // Visualize vertical firing ranges + for ( int i = 0; i < 4; i++ ) + { + QAngle angMaxDownPitch = GetAbsAngles(); + + switch( i ) + { + case 0: angMaxDownPitch.x -= 15; break; + case 1: angMaxDownPitch.x += 15; break; + case 2: angMaxDownPitch.x -= 25; break; + case 3: angMaxDownPitch.x += 25; break; + default: + break; + } + + Vector vecMaxDownPitch; + AngleVectors( angMaxDownPitch, &vecMaxDownPitch ); + NDebugOverlay::Line( vecMuzzle, vecMuzzle + (vecMaxDownPitch*256), 255, 255, 255, false, 0.1 ); + } + } + + if ( m_flShotTime < gpGlobals->curtime ) + { + Vector vecMuzzle, vecMuzzleDir; + + UpdateMuzzleMatrix(); + MatrixGetColumn( m_muzzleToWorld, 0, vecMuzzleDir ); + MatrixGetColumn( m_muzzleToWorld, 3, vecMuzzle ); + + Vector2D vecDirToEnemy2D = vecDirToEnemy.AsVector2D(); + Vector2D vecMuzzleDir2D = vecMuzzleDir.AsVector2D(); + + bool bCanShoot = true; + float minCos3d = DOT_10DEGREE; // 10 degrees slop + + if ( flDistToEnemy < 60.0 ) + { + vecDirToEnemy2D.NormalizeInPlace(); + vecMuzzleDir2D.NormalizeInPlace(); + + bCanShoot = ( vecDirToEnemy2D.Dot(vecMuzzleDir2D) >= DOT_10DEGREE ); + minCos3d = 0.7071; // 45 degrees + } + + //Fire the gun + if ( bCanShoot ) // 10 degree slop XY + { + float dot3d = DotProduct( vecDirToEnemy, vecMuzzleDir ); + + if( m_spawnflags & SF_FLOOR_TURRET_OUT_OF_AMMO ) + { + DryFire(); + } + else + { + if ( dot3d >= minCos3d ) + { + ResetActivity(); + SetActivity( (Activity) ACT_FLOOR_TURRET_FIRE ); + + //Fire the weapon +#if !DISABLE_SHOT + Shoot( vecMuzzle, vecMuzzleDir, (dot3d < DOT_10DEGREE) ); +#endif + } + } + } + } + else + { + SetActivity( (Activity) ACT_FLOOR_TURRET_OPEN_IDLE ); + } + + //If we can see our enemy, face it + if ( bEnemyVisible ) + { + //We want to look at the enemy's eyes so we don't jitter + Vector vecDirToEnemyEyes = GetEnemy()->WorldSpaceCenter() - vecMid; + VectorNormalize( vecDirToEnemyEyes ); + + QAngle vecAnglesToEnemy; + VectorAngles( vecDirToEnemyEyes, vecAnglesToEnemy ); + + m_vecGoalAngles.y = vecAnglesToEnemy.y; + m_vecGoalAngles.x = vecAnglesToEnemy.x; + } + + //Turn to face + UpdateFacing(); +} + +//----------------------------------------------------------------------------- +// Purpose: Target doesn't exist or has eluded us, so search for one +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::SearchThink( void ) +{ + //Allow descended classes a chance to do something before the think function + if ( PreThink( TURRET_SEARCHING ) ) + return; + + SetNextThink( gpGlobals->curtime + 0.05f ); + + SetActivity( (Activity) ACT_FLOOR_TURRET_OPEN_IDLE ); + + //If our enemy has died, pick a new enemy + if ( ( GetEnemy() != NULL ) && ( GetEnemy()->IsAlive() == false ) ) + { + SetEnemy( NULL ); + } + + //Acquire the target + if ( GetEnemy() == NULL ) + { + HackFindEnemy(); + } + + //If we've found a target, spin up the barrel and start to attack + if ( GetEnemy() != NULL ) + { + //Give players a grace period + if ( GetEnemy()->IsPlayer() ) + { + m_flShotTime = gpGlobals->curtime + 0.5f; + } + else + { + m_flShotTime = gpGlobals->curtime + 0.1f; + } + + m_flLastSight = 0; + SetThink( &CNPC_FloorTurret::ActiveThink ); + SetEyeState( TURRET_EYE_SEE_TARGET ); + + SpinUp(); + + if ( gpGlobals->curtime > m_flNextActivateSoundTime ) + { + EmitSound( "NPC_FloorTurret.Activate" ); + m_flNextActivateSoundTime = gpGlobals->curtime + 3.0; + } + return; + } + + //Are we out of time and need to retract? + if ( gpGlobals->curtime > m_flLastSight ) + { + //Before we retrace, make sure that we are spun down. + m_flLastSight = 0; + SetThink( &CNPC_FloorTurret::Retire ); + return; + } + + //Display that we're scanning + m_vecGoalAngles.x = GetAbsAngles().x + ( sin( gpGlobals->curtime * 1.0f ) * 15.0f ); + m_vecGoalAngles.y = GetAbsAngles().y + ( sin( gpGlobals->curtime * 2.0f ) * 60.0f ); + + //Turn and ping + UpdateFacing(); + Ping(); +} + +//----------------------------------------------------------------------------- +// Purpose: Watch for a target to wander into our view +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::AutoSearchThink( void ) +{ + //Allow descended classes a chance to do something before the think function + if ( PreThink( TURRET_AUTO_SEARCHING ) ) + return; + + //Spread out our thinking + SetNextThink( gpGlobals->curtime + random->RandomFloat( 0.2f, 0.4f ) ); + + //If the enemy is dead, find a new one + if ( ( GetEnemy() != NULL ) && ( GetEnemy()->IsAlive() == false ) ) + { + SetEnemy( NULL ); + } + + //Acquire Target + if ( GetEnemy() == NULL ) + { + HackFindEnemy(); + } + + //Deploy if we've got an active target + if ( GetEnemy() != NULL ) + { + SetThink( &CNPC_FloorTurret::Deploy ); + if ( !m_bNoAlarmSounds ) + { + EmitSound( "NPC_FloorTurret.Alert" ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Fire! +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::Shoot( const Vector &vecSrc, const Vector &vecDirToEnemy, bool bStrict ) +{ + FireBulletsInfo_t info; + + if ( !bStrict && GetEnemy() != NULL ) + { + Vector vecDir = GetActualShootTrajectory( vecSrc ); + + info.m_vecSrc = vecSrc; + info.m_vecDirShooting = vecDir; + info.m_iTracerFreq = 1; + info.m_iShots = 1; + info.m_pAttacker = this; + info.m_vecSpread = VECTOR_CONE_PRECALCULATED; + info.m_flDistance = MAX_COORD_RANGE; + info.m_iAmmoType = m_iAmmoType; + } + else + { + info.m_vecSrc = vecSrc; + info.m_vecDirShooting = vecDirToEnemy; + info.m_iTracerFreq = 1; + info.m_iShots = 1; + info.m_pAttacker = this; + info.m_vecSpread = GetAttackSpread( NULL, GetEnemy() ); + info.m_flDistance = MAX_COORD_RANGE; + info.m_iAmmoType = m_iAmmoType; + } + + FireBullets( info ); + EmitSound( "NPC_FloorTurret.ShotSounds", m_ShotSounds ); + DoMuzzleFlash(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEnemy - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_FloorTurret::IsValidEnemy( CBaseEntity *pEnemy ) +{ + if ( m_NPCState == NPC_STATE_DEAD ) + return false; + + // Don't shoot at other turrets. + if ( pEnemy->m_iClassname == m_iClassname ) + return false; + + // If our eye is stuck in something, don't shoot + if ( UTIL_PointContents(EyePosition()) & MASK_SHOT ) + return false; + + // Turrets have limited vertical aim capability + // - Can only aim +-15 degrees, + the 10 degree slop they're allowed. + Vector vEnemyPos = pEnemy->EyePosition(); + +#ifdef PORTAL + if ( !FInViewCone( pEnemy ) || !FVisible( pEnemy ) ) + { + CProp_Portal *pPortal = FInViewConeThroughPortal( pEnemy ); + + if ( pPortal ) + { + // Translate our target across the portal + UTIL_Portal_PointTransform( pPortal->m_hLinkedPortal->MatrixThisToLinked(), vEnemyPos, vEnemyPos ); + } + } +#endif + + Vector los = ( vEnemyPos - EyePosition() ); + + QAngle angleToTarget; + VectorAngles( los, angleToTarget ); + float flZDiff = fabs( AngleNormalize( angleToTarget.x - GetAbsAngles().x) ); + if ( flZDiff > 28.0f && los.LengthSqr() > 4096.0f ) + return false; + + return BaseClass::IsValidEnemy( pEnemy ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pEnemy - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CNPC_FloorTurret::CanBeAnEnemyOf( CBaseEntity *pEnemy ) +{ + // If we're out of ammo, make friendly companions ignore us + if ( m_spawnflags & SF_FLOOR_TURRET_OUT_OF_AMMO ) + { + if ( pEnemy->Classify() == CLASS_PLAYER_ALLY_VITAL ) + return false; + } + + // If we're on the side, we're never anyone's enemy + if ( OnSide() ) + return false; + + return BaseClass::CanBeAnEnemyOf( pEnemy ); +} + +//----------------------------------------------------------------------------- +// Purpose: The turret has been tipped over and will thrash for awhile +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::TippedThink( void ) +{ + // Update our PVS state + CheckPVSCondition(); + + //Animate + StudioFrameAdvance(); + + SetNextThink( gpGlobals->curtime + 0.05f ); + SetEnemy( NULL ); + + // If we're not on side anymore, stop thrashing + if ( !OnSide() ) + { + ReturnToLife(); + return; + } + + //See if we should continue to thrash + if ( gpGlobals->curtime < m_flThrashTime ) + { + if ( m_flShotTime < gpGlobals->curtime ) + { + if( m_spawnflags & SF_FLOOR_TURRET_OUT_OF_AMMO ) + { + DryFire(); + } + else if ( IsCitizenTurret() == false ) // Citizen turrets don't wildly fire + { + Vector vecMuzzle, vecMuzzleDir; + UpdateMuzzleMatrix(); + MatrixGetColumn( m_muzzleToWorld, 0, vecMuzzleDir ); + MatrixGetColumn( m_muzzleToWorld, 3, vecMuzzle ); + + ResetActivity(); + SetActivity( (Activity) ACT_FLOOR_TURRET_FIRE ); + +#if !DISABLE_SHOT + Shoot( vecMuzzle, vecMuzzleDir ); +#endif + } + + m_flShotTime = gpGlobals->curtime + 0.05f; + } + + m_vecGoalAngles.x = GetAbsAngles().x + random->RandomFloat( -60, 60 ); + m_vecGoalAngles.y = GetAbsAngles().y + random->RandomFloat( -60, 60 ); + + UpdateFacing(); + } + else + { + //Face forward + m_vecGoalAngles = GetAbsAngles(); + + //Set ourselves to close + if ( GetActivity() != ACT_FLOOR_TURRET_CLOSE ) + { + SetActivity( (Activity) ACT_FLOOR_TURRET_OPEN_IDLE ); + + //If we're done moving to our desired facing, close up + if ( UpdateFacing() == false ) + { + //Make any last death noises and anims + EmitSound( "NPC_FloorTurret.Die" ); + SpinDown(); + + SetActivity( (Activity) ACT_FLOOR_TURRET_CLOSE ); + EmitSound( "NPC_FloorTurret.Retract" ); + + CTakeDamageInfo info; + info.SetDamage( 1 ); + info.SetDamageType( DMG_CRUSH ); + Event_Killed( info ); + } + } + else if ( IsActivityFinished() ) + { + m_bActive = false; + m_flLastSight = 0; + + SetActivity( (Activity) ACT_FLOOR_TURRET_CLOSED_IDLE ); + + // Don't need to store last NPC anymore, because I've been knocked over + if ( m_hLastNPCToKickMe ) + { + m_hLastNPCToKickMe = NULL; + m_flKnockOverFailedTime = 0; + } + + //Try to look straight + if ( UpdateFacing() == false ) + { + m_OnTipped.FireOutput( this, this ); + SetEyeState( TURRET_EYE_DEAD ); + SetCollisionGroup( COLLISION_GROUP_DEBRIS_TRIGGER ); + + // Start thinking slowly to see if we're ever set upright somehow + SetThink( &CNPC_FloorTurret::InactiveThink ); + SetNextThink( gpGlobals->curtime + 1.0f ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: This turret is dead. See if it ever becomes upright again, and if +// so, become active again. +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::InactiveThink( void ) +{ + // Update our PVS state + CheckPVSCondition(); + + // Wake up if we're not on our side + if ( !OnSide() && m_bEnabled ) + { + ReturnToLife(); + return; + } + + if ( IsCitizenTurret() ) + { + // Blink if we have ammo or our current blink is "on" and we need to turn it off again + if ( HasSpawnFlags( SF_FLOOR_TURRET_OUT_OF_AMMO ) == false || m_bBlinkState ) + { + // If we're on our side, ping and complain to the player + if ( m_bBlinkState == false ) + { + // Ping when the light is going to come back on + EmitSound( "NPC_FloorTurret.AlarmPing" ); + } + + SetEyeState( TURRET_EYE_ALARM ); + SetNextThink( gpGlobals->curtime + 0.25f ); + } + } + else + { + SetNextThink( gpGlobals->curtime + 1.0f ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::ReturnToLife( void ) +{ + m_flThrashTime = 0; + + // Enable the tip controller + m_pMotionController->Enable( true ); + + // Return to life + SetState( NPC_STATE_IDLE ); + m_lifeState = LIFE_ALIVE; + SetCollisionGroup( COLLISION_GROUP_NONE ); + + // Become active again + Enable(); +} + +//----------------------------------------------------------------------------- +// Purpose: The turret is not doing anything at all +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::DisabledThink( void ) +{ + SetNextThink( gpGlobals->curtime + 0.5 ); + if ( OnSide() ) + { + m_OnTipped.FireOutput( this, this ); + SetEyeState( TURRET_EYE_DEAD ); + SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + SetThink( NULL ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: The turret doesn't run base AI properly, which is a bad decision. +// As a result, it has to manually find enemies. +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::HackFindEnemy( void ) +{ + // We have to refresh our memories before finding enemies, so + // dead enemies are cleared out before new ones are added. + GetEnemies()->RefreshMemories(); + + GetSenses()->Look( FLOOR_TURRET_RANGE ); + SetEnemy( BestEnemy() ); +} + +//----------------------------------------------------------------------------- +// Purpose: Determines whether the turret is upright enough to function +// Output : Returns true if the turret is tipped over +//----------------------------------------------------------------------------- +inline bool CNPC_FloorTurret::OnSide( void ) +{ + Vector up; + GetVectors( NULL, NULL, &up ); + + return ( DotProduct( up, Vector(0,0,1) ) < 0.5f ); +} + +//----------------------------------------------------------------------------- +// Purpose: Allows a generic think function before the others are called +// Input : state - which state the turret is currently in +//----------------------------------------------------------------------------- +bool CNPC_FloorTurret::PreThink( turretState_e state ) +{ + // Hack to disable turrets when ai is disabled + if ( CAI_BaseNPC::m_nDebugBits & bits_debugDisableAI ) + { + // Push our think out into the future + SetNextThink( gpGlobals->curtime + 0.1f ); + return true; + } + + CheckPVSCondition(); + + //Animate + StudioFrameAdvance(); + + // We're gonna blow up, so don't interrupt us + if ( state == TURRET_SELF_DESTRUCTING ) + return false; + + //See if we've tipped, but only do this if we're not being carried + if ( !IsBeingCarriedByPlayer() ) + { + if ( OnSide() == false ) + { + // If I still haven't fallen over after an NPC has tried to knock me down, let them know + if ( m_hLastNPCToKickMe && m_flKnockOverFailedTime < gpGlobals->curtime ) + { + m_hLastNPCToKickMe->DispatchInteraction( g_interactionTurretStillStanding, NULL, this ); + m_hLastNPCToKickMe = NULL; + } + + //Debug visualization + if ( g_debug_turret.GetBool() ) + { + Vector up; + GetVectors( NULL, NULL, &up ); + + NDebugOverlay::Line( GetAbsOrigin()+(up*32), GetAbsOrigin()+(up*128), 0, 255, 0, false, 2.0f ); + NDebugOverlay::Cross3D( GetAbsOrigin()+(up*32), -Vector(2,2,2), Vector(2,2,2), 0, 255, 0, false, 2.0f ); + NDebugOverlay::Cross3D( GetAbsOrigin()+(up*128), -Vector(2,2,2), Vector(2,2,2), 0, 255, 0, false, 2.0f ); + } + } + else + { + if ( HasSpawnFlags( SF_FLOOR_TURRET_OUT_OF_AMMO ) == false ) + { + //Thrash around for a bit + m_flThrashTime = gpGlobals->curtime + random->RandomFloat( 2.0f, 2.5f ); + SetNextThink( gpGlobals->curtime + 0.05f ); + + SetThink( &CNPC_FloorTurret::TippedThink ); + SetEyeState( TURRET_EYE_SEE_TARGET ); + + SpinUp(); + if ( !m_bNoAlarmSounds ) + { + EmitSound( "NPC_FloorTurret.Alarm" ); + } + } + else + { + // Take away the laser + UTIL_Remove( m_hLaser ); + m_hLaser = NULL; + + // Become inactive + SetThink( &CNPC_FloorTurret::InactiveThink ); + SetEyeState( TURRET_EYE_DEAD ); + } + + //Stop being targetted + SetState( NPC_STATE_DEAD ); + m_lifeState = LIFE_DEAD; + + //Disable the tip controller + m_pMotionController->Enable( false ); + + //Debug visualization + if ( g_debug_turret.GetBool() ) + { + Vector up; + GetVectors( NULL, NULL, &up ); + + NDebugOverlay::Line( GetAbsOrigin()+(up*32), GetAbsOrigin()+(up*128), 255, 0, 0, false, 2.0f ); + NDebugOverlay::Cross3D( GetAbsOrigin()+(up*32), -Vector(2,2,2), Vector(2,2,2), 255, 0, 0, false, 2.0f ); + NDebugOverlay::Cross3D( GetAbsOrigin()+(up*128), -Vector(2,2,2), Vector(2,2,2), 255, 0, 0, false, 2.0f ); + } + + //Interrupt current think function + return true; + } + } + + //Do not interrupt current think function + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the state of the glowing eye attached to the turret +// Input : state - state the eye should be in +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::SetEyeState( eyeState_t state ) +{ + // Must have a valid eye to affect + if ( !m_hEyeGlow ) + { + // Create our eye sprite + m_hEyeGlow = CSprite::SpriteCreate( FLOOR_TURRET_GLOW_SPRITE, GetLocalOrigin(), false ); + if ( !m_hEyeGlow ) + return; + + m_hEyeGlow->SetTransparency( kRenderWorldGlow, 255, 0, 0, 128, kRenderFxNoDissipation ); + m_hEyeGlow->SetAttachment( this, m_iEyeAttachment ); + } + + // Add the laser if it doesn't already exist + if ( IsCitizenTurret() && HasSpawnFlags( SF_FLOOR_TURRET_OUT_OF_AMMO ) == false && m_hLaser == NULL ) + { + m_hLaser = CBeam::BeamCreate( LASER_BEAM_SPRITE, 1.0f ); + if ( m_hLaser == NULL ) + return; + + m_hLaser->EntsInit( this, this ); + m_hLaser->FollowEntity( this ); + m_hLaser->SetStartAttachment( LookupAttachment( "laser_start" ) ); + m_hLaser->SetEndAttachment( LookupAttachment( "laser_end" ) ); + m_hLaser->SetNoise( 0 ); + m_hLaser->SetColor( 255, 0, 0 ); + m_hLaser->SetScrollRate( 0 ); + m_hLaser->SetWidth( 1.0f ); + m_hLaser->SetEndWidth( 1.0f ); + m_hLaser->SetBrightness( 160 ); + m_hLaser->SetBeamFlags( SF_BEAM_SHADEIN ); + } + + m_iEyeState = state; + + //Set the state + switch( state ) + { + default: + case TURRET_EYE_SEE_TARGET: //Fade in and scale up + m_hEyeGlow->SetColor( 255, 0, 0 ); + m_hEyeGlow->SetBrightness( 164, 0.1f ); + m_hEyeGlow->SetScale( 0.4f, 0.1f ); + break; + + case TURRET_EYE_SEEKING_TARGET: //Ping-pongs + + //Toggle our state + m_bBlinkState = !m_bBlinkState; + m_hEyeGlow->SetColor( 255, 128, 0 ); + + if ( m_bBlinkState ) + { + //Fade up and scale up + m_hEyeGlow->SetScale( 0.25f, 0.1f ); + m_hEyeGlow->SetBrightness( 164, 0.1f ); + } + else + { + //Fade down and scale down + m_hEyeGlow->SetScale( 0.2f, 0.1f ); + m_hEyeGlow->SetBrightness( 64, 0.1f ); + } + + break; + + case TURRET_EYE_DORMANT: //Fade out and scale down + m_hEyeGlow->SetColor( 0, 255, 0 ); + m_hEyeGlow->SetScale( 0.1f, 0.5f ); + m_hEyeGlow->SetBrightness( 64, 0.5f ); + break; + + case TURRET_EYE_DEAD: //Fade out slowly + m_hEyeGlow->SetColor( 255, 0, 0 ); + m_hEyeGlow->SetScale( 0.1f, 3.0f ); + m_hEyeGlow->SetBrightness( 0, 3.0f ); + break; + + case TURRET_EYE_DISABLED: + m_hEyeGlow->SetColor( 0, 255, 0 ); + m_hEyeGlow->SetScale( 0.1f, 1.0f ); + m_hEyeGlow->SetBrightness( 0, 1.0f ); + break; + + case TURRET_EYE_ALARM: + { + //Toggle our state + m_bBlinkState = !m_bBlinkState; + m_hEyeGlow->SetColor( 255, 0, 0 ); + + if ( m_bBlinkState ) + { + //Fade up and scale up + m_hEyeGlow->SetScale( 0.75f, 0.05f ); + m_hEyeGlow->SetBrightness( 192, 0.05f ); + } + else + { + //Fade down and scale down + m_hEyeGlow->SetScale( 0.25f, 0.25f ); + m_hEyeGlow->SetBrightness( 64, 0.25f ); + } + } + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Make a pinging noise so the player knows where we are +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::Ping( void ) +{ + //See if it's time to ping again + if ( m_flPingTime > gpGlobals->curtime ) + return; + + //Ping! + EmitSound( "NPC_FloorTurret.Ping" ); + + SetEyeState( TURRET_EYE_SEEKING_TARGET ); + + m_flPingTime = gpGlobals->curtime + FLOOR_TURRET_PING_TIME; +} + +//----------------------------------------------------------------------------- +// Purpose: Toggle the turret's state +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::Toggle( void ) +{ + //This turret is on its side, it can't function + if ( OnSide() || ( IsAlive() == false ) ) + return; + + //Toggle the state + if ( m_bEnabled ) + { + Disable(); + } + else + { + Enable(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Enable the turret and deploy +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::Enable( void ) +{ + // Don't interrupt blowing up! + if ( m_bSelfDestructing ) + return; + + // Always allow us to come back to life, even if not right now + m_bEnabled = true; + + //This turret is on its side, it can't function + if ( OnSide() || ( IsAlive() == false ) ) + return; + + // if the turret is flagged as an autoactivate turret, re-enable its ability open self. + if ( m_spawnflags & SF_FLOOR_TURRET_AUTOACTIVATE ) + { + m_bAutoStart = true; + } + + SetThink( &CNPC_FloorTurret::Deploy ); + SetNextThink( gpGlobals->curtime + 0.05f ); +} + +//----------------------------------------------------------------------------- +// Purpose: Retire the turret until enabled again +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::Disable( void ) +{ + //This turret is on its side, it can't function + if ( OnSide() || ( IsAlive() == false ) || m_bSelfDestructing ) + return; + + if ( m_bEnabled ) + { + m_bEnabled = false; + m_bAutoStart = false; + + SetEnemy( NULL ); + SetThink( &CNPC_FloorTurret::Retire ); + SetNextThink( gpGlobals->curtime + 0.1f ); + } + else + SetThink( &CNPC_FloorTurret::DisabledThink ); +} + +//----------------------------------------------------------------------------- +// Purpose: Toggle the turret's state via input function +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::InputToggle( inputdata_t &inputdata ) +{ + Toggle(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::InputEnable( inputdata_t &inputdata ) +{ + Enable(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::InputDisable( inputdata_t &inputdata ) +{ + Disable(); +} + +//----------------------------------------------------------------------------- +// Purpose: Stops the turret from firing live rounds (still attempts to though) +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::InputDepleteAmmo( inputdata_t &inputdata ) +{ + AddSpawnFlags( SF_FLOOR_TURRET_OUT_OF_AMMO ); +} + +//----------------------------------------------------------------------------- +// Purpose: Allows the turret to fire live rounds again +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::InputRestoreAmmo( inputdata_t &inputdata ) +{ + RemoveSpawnFlags( SF_FLOOR_TURRET_OUT_OF_AMMO ); +} + +//----------------------------------------------------------------------------- +// Purpose: Allow players and npc's to turn the turret on and off +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::ToggleUse ( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value ) +{ + switch( useType ) + { + case USE_OFF: + Disable(); + break; + case USE_ON: + Enable(); + break; + case USE_SET: + break; + case USE_TOGGLE: + Toggle( ); + break; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Reduce physics forces from the front +//----------------------------------------------------------------------------- +int CNPC_FloorTurret::VPhysicsTakeDamage( const CTakeDamageInfo &info ) +{ + bool bShouldIgnoreFromFront = false; + + // Ignore crossbow bolts hitting us from the front + bShouldIgnoreFromFront = ( info.GetDamageType() & DMG_BULLET ) != 0; + + // Ignore bullets from the front + if ( !bShouldIgnoreFromFront ) + { + bShouldIgnoreFromFront = FClassnameIs( info.GetInflictor(), "crossbow_bolt" ); + } + + // Did it hit us on the front? + if ( bShouldIgnoreFromFront ) + { + Vector vecForward; + GetVectors( &vecForward, NULL, NULL ); + Vector vecForce = info.GetDamageForce(); + VectorNormalize( vecForce ); + float flDot = DotProduct( vecForward, vecForce ); + if ( flDot < -0.85 ) + return 0; + } + + return BaseClass::VPhysicsTakeDamage( info ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &info - +//----------------------------------------------------------------------------- +int CNPC_FloorTurret::OnTakeDamage( const CTakeDamageInfo &info ) +{ + CTakeDamageInfo newInfo = info; + + if ( info.GetDamageType() & (DMG_SLASH|DMG_CLUB) ) + { + // Take extra force from melee hits + newInfo.ScaleDamageForce( 2.0f ); + + // Disable our upright controller for some time + if ( m_pMotionController != NULL ) + { + m_pMotionController->Suspend( 2.0f ); + } + } + else if ( info.GetDamageType() & DMG_BLAST ) + { + newInfo.ScaleDamageForce( 2.0f ); + } + else if ( (info.GetDamageType() & DMG_BULLET) && !(info.GetDamageType() & DMG_BUCKSHOT) ) + { + // Bullets, but not buckshot, do extra push + newInfo.ScaleDamageForce( 2.5f ); + } + + // Manually apply vphysics because AI_BaseNPC takedamage doesn't call back to CBaseEntity OnTakeDamage + VPhysicsTakeDamage( newInfo ); + + // Bump up our search time + m_flLastSight = gpGlobals->curtime + FLOOR_TURRET_MAX_WAIT; + + // Start looking around in anger if we were idle + if ( IsAlive() && m_bEnabled && m_bAutoStart && GetActivity() == ACT_FLOOR_TURRET_CLOSED_IDLE && m_bSelfDestructing == false ) + { + SetThink( &CNPC_FloorTurret::Deploy ); + } + + return BaseClass::OnTakeDamage( newInfo ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::SpinUp( void ) +{ +} + +#define FLOOR_TURRET_MIN_SPIN_DOWN 1.0f + +//----------------------------------------------------------------------------- +// Purpose: +// Output : const QAngle +//----------------------------------------------------------------------------- +QAngle CNPC_FloorTurret::PreferredCarryAngles( void ) +{ + // FIXME: Embed this into the class + static QAngle g_prefAngles; + + Vector vecUserForward; + CBasePlayer *pPlayer = AI_GetSinglePlayer(); + pPlayer->EyeVectors( &vecUserForward ); + + // If we're looking up, then face directly forward + if ( vecUserForward.z >= 0.0f ) + return vec3_angle; + + // Otherwise, stay "upright" + g_prefAngles.Init(); + g_prefAngles.x = -pPlayer->EyeAngles().x; + + return g_prefAngles; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::SpinDown( void ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pVictim - +// Output : float +//----------------------------------------------------------------------------- +float CNPC_FloorTurret::GetAttackDamageScale( CBaseEntity *pVictim ) +{ + CBaseCombatCharacter *pBCC = pVictim->MyCombatCharacterPointer(); + + // Do extra damage to antlions & combine + if ( pBCC ) + { + if ( pBCC->Classify() == CLASS_ANTLION ) + return 2.0; + + if ( pBCC->Classify() == CLASS_COMBINE ) + return 2.0; + } + + return BaseClass::GetAttackDamageScale( pVictim ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +Vector CNPC_FloorTurret::GetAttackSpread( CBaseCombatWeapon *pWeapon, CBaseEntity *pTarget ) +{ + WeaponProficiency_t weaponProficiency = WEAPON_PROFICIENCY_AVERAGE; + + // Switch our weapon proficiency based upon our target + if ( pTarget ) + { + if ( pTarget->Classify() == CLASS_PLAYER || pTarget->Classify() == CLASS_ANTLION || pTarget->Classify() == CLASS_ZOMBIE ) + { + // Make me much more accurate + weaponProficiency = WEAPON_PROFICIENCY_PERFECT; + } + else if ( pTarget->Classify() == CLASS_COMBINE ) + { + // Make me more accurate + weaponProficiency = WEAPON_PROFICIENCY_VERY_GOOD; + } + } + + return VECTOR_CONE_10DEGREES * ((CBaseHLCombatWeapon::GetDefaultProficiencyValues())[ weaponProficiency ].spreadscale); +} + +//------------------------------------------------------------------------------ +// Do we have a physics attacker? +//------------------------------------------------------------------------------ +CBasePlayer *CNPC_FloorTurret::HasPhysicsAttacker( float dt ) +{ + // If the player is holding me now, or I've been recently thrown + // then return a pointer to that player + if ( IsHeldByPhyscannon( ) || (gpGlobals->curtime - dt <= m_flLastPhysicsInfluenceTime) ) + { + return m_hPhysicsAttacker; + } + return NULL; +} +//----------------------------------------------------------------------------- +// Purpose: Draw any debug text overlays +// Output : Current text offset from the top +//----------------------------------------------------------------------------- +int CNPC_FloorTurret::DrawDebugTextOverlays( void ) +{ + int text_offset = BaseClass::DrawDebugTextOverlays(); + + if (m_debugOverlays & OVERLAY_TEXT_BIT) + { + if (VPhysicsGetObject()) + { + char tempstr[512]; + Q_snprintf(tempstr, sizeof(tempstr),"Mass: %.2f kg / %.2f lb (%s)", VPhysicsGetObject()->GetMass(), kg2lbs(VPhysicsGetObject()->GetMass()), GetMassEquivalent(VPhysicsGetObject()->GetMass())); + EntityText( text_offset, tempstr, 0); + text_offset++; + } + } + + return text_offset; +} + +void CNPC_FloorTurret::UpdateMuzzleMatrix() +{ + if ( gpGlobals->tickcount != m_muzzleToWorldTick ) + { + m_muzzleToWorldTick = gpGlobals->tickcount; + GetAttachment( m_iMuzzleAttachment, m_muzzleToWorld ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: We override this code because otherwise we start to move into the +// tricky realm of player avoidance. Since we don't go through the +// normal NPC thinking but we ARE an NPC (...) we miss a bunch of +// book keeping. This means we can become invisible and then never +// reappear. +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::PlayerPenetratingVPhysics( void ) +{ + // We don't care! +} + +#define SELF_DESTRUCT_DURATION 4.0f +#define SELF_DESTRUCT_BEEP_MIN_DELAY 0.1f +#define SELF_DESTRUCT_BEEP_MAX_DELAY 0.75f +#define SELF_DESTRUCT_BEEP_MIN_PITCH 100.0f +#define SELF_DESTRUCT_BEEP_MAX_PITCH 225.0f + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::BreakThink( void ) +{ + Vector vecUp; + GetVectors( NULL, NULL, &vecUp ); + Vector vecOrigin = WorldSpaceCenter() + ( vecUp * 12.0f ); + + // Our effect +#ifdef HL2_EPISODIC + DispatchParticleEffect( "explosion_turret_break", vecOrigin, GetAbsAngles() ); +#endif // HL2_EPISODIC + + // K-boom + RadiusDamage( CTakeDamageInfo( this, this, 15.0f, DMG_BLAST ), vecOrigin, (10*12), CLASS_NONE, this ); + + EmitSound( "NPC_FloorTurret.Destruct" ); + + breakablepropparams_t params( GetAbsOrigin(), GetAbsAngles(), vec3_origin, RandomAngularImpulse( -800.0f, 800.0f ) ); + params.impactEnergyScale = 1.0f; + params.defCollisionGroup = COLLISION_GROUP_INTERACTIVE; + + // no damage/damage force? set a burst of 100 for some movement + params.defBurstScale = 100; + PropBreakableCreateAll( GetModelIndex(), VPhysicsGetObject(), params, this, -1, true ); + + // Throw out some small chunks too obscure the explosion even more + CPVSFilter filter( vecOrigin ); + for ( int i = 0; i < 4; i++ ) + { + Vector gibVelocity = RandomVector(-100,100); + int iModelIndex = modelinfo->GetModelIndex( g_PropDataSystem.GetRandomChunkModel( "MetalChunks" ) ); + te->BreakModel( filter, 0.0, vecOrigin, GetAbsAngles(), Vector(40,40,40), gibVelocity, iModelIndex, 150, 4, 2.5, BREAK_METAL ); + } + + // We're done! + UTIL_Remove( this ); +} + +//----------------------------------------------------------------------------- +// Purpose: The countdown to destruction! +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::SelfDestructThink( void ) +{ + // Continue to animate + PreThink( TURRET_SELF_DESTRUCTING ); + + // If we're done, explode + if ( ( gpGlobals->curtime - m_flDestructStartTime ) >= SELF_DESTRUCT_DURATION ) + { + SetThink( &CNPC_FloorTurret::BreakThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + UTIL_Remove( m_hFizzleEffect ); + m_hFizzleEffect = NULL; + return; + } + + // Find out where we are in the cycle of our destruction + float flDestructPerc = clamp( ( gpGlobals->curtime - m_flDestructStartTime ) / SELF_DESTRUCT_DURATION, 0.0f, 1.0f ); + + // Figure out when our next beep should occur + float flBeepTime = SELF_DESTRUCT_BEEP_MAX_DELAY + ( ( SELF_DESTRUCT_BEEP_MIN_DELAY - SELF_DESTRUCT_BEEP_MAX_DELAY ) * flDestructPerc ); + + // If it's time to beep again, do so + if ( gpGlobals->curtime > ( m_flPingTime + flBeepTime ) ) + { + // Figure out what our beep pitch will be + float flBeepPitch = SELF_DESTRUCT_BEEP_MIN_PITCH + ( ( SELF_DESTRUCT_BEEP_MAX_PITCH - SELF_DESTRUCT_BEEP_MIN_PITCH ) * flDestructPerc ); + + StopSound( "NPC_FloorTurret.AlarmPing" ); + + // Play the beep + CPASAttenuationFilter filter( this, "NPC_FloorTurret.AlarmPing" ); + EmitSound_t params; + params.m_pSoundName = "NPC_FloorTurret.AlarmPing"; + params.m_nPitch = floor( flBeepPitch ); + params.m_nFlags = SND_CHANGE_PITCH; + EmitSound( filter, entindex(), params ); + + // Flash our eye + SetEyeState( TURRET_EYE_ALARM ); + + // Save this as the last time we pinged + m_flPingTime = gpGlobals->curtime; + + // Randomly twitch + m_vecGoalAngles.x = GetAbsAngles().x + random->RandomFloat( -60*flDestructPerc, 60*flDestructPerc ); + m_vecGoalAngles.y = GetAbsAngles().y + random->RandomFloat( -60*flDestructPerc, 60*flDestructPerc ); + } + + UpdateFacing(); + + // Think again! + SetNextThink( gpGlobals->curtime + 0.05f ); +} + +//----------------------------------------------------------------------------- +// Purpose: Make us explode +//----------------------------------------------------------------------------- +void CNPC_FloorTurret::InputSelfDestruct( inputdata_t &inputdata ) +{ + // Ka-boom! + m_flDestructStartTime = gpGlobals->curtime; + m_flPingTime = gpGlobals->curtime; + m_bSelfDestructing = true; + + SetThink( &CNPC_FloorTurret::SelfDestructThink ); + SetNextThink( gpGlobals->curtime + 0.1f ); + + // Create the dust effect in place + m_hFizzleEffect = (CParticleSystem *) CreateEntityByName( "info_particle_system" ); + if ( m_hFizzleEffect != NULL ) + { + Vector vecUp; + GetVectors( NULL, NULL, &vecUp ); + + // Setup our basic parameters + m_hFizzleEffect->KeyValue( "start_active", "1" ); + m_hFizzleEffect->KeyValue( "effect_name", "explosion_turret_fizzle" ); + m_hFizzleEffect->SetParent( this ); + m_hFizzleEffect->SetAbsOrigin( WorldSpaceCenter() + ( vecUp * 12.0f ) ); + DispatchSpawn( m_hFizzleEffect ); + m_hFizzleEffect->Activate(); + } +} + +// +// Tip controller +// + +LINK_ENTITY_TO_CLASS( floorturret_tipcontroller, CTurretTipController ); + + +//--------------------------------------------------------- +// Save/Restore +//--------------------------------------------------------- +BEGIN_DATADESC( CTurretTipController ) + + DEFINE_FIELD( m_bEnabled, FIELD_BOOLEAN ), + DEFINE_FIELD( m_flSuspendTime, FIELD_TIME ), + DEFINE_FIELD( m_worldGoalAxis, FIELD_VECTOR ), + DEFINE_FIELD( m_localTestAxis, FIELD_VECTOR ), + DEFINE_PHYSPTR( m_pController ), + DEFINE_FIELD( m_angularLimit, FIELD_FLOAT ), + DEFINE_FIELD( m_pParentTurret, FIELD_CLASSPTR ), + +END_DATADESC() + + + +CTurretTipController::~CTurretTipController() +{ + if ( m_pController ) + { + physenv->DestroyMotionController( m_pController ); + m_pController = NULL; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTurretTipController::Spawn( void ) +{ + m_bEnabled = true; + + // align the object's local Z axis + m_localTestAxis.Init( 0, 0, 1 ); + // with the world's Z axis + m_worldGoalAxis.Init( 0, 0, 1 ); + + // recover from up to 25 degrees / sec angular velocity + m_angularLimit = 25; + m_flSuspendTime = 0; + + SetMoveType( MOVETYPE_NONE ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTurretTipController::Activate( void ) +{ + BaseClass::Activate(); + + if ( m_pParentTurret == NULL ) + { + UTIL_Remove(this); + return; + } + + IPhysicsObject *pPhys = m_pParentTurret->VPhysicsGetObject(); + + if ( pPhys == NULL ) + { + UTIL_Remove(this); + return; + } + + //Setup the motion controller + if ( !m_pController ) + { + m_pController = physenv->CreateMotionController( (IMotionEvent *)this ); + m_pController->AttachObject( pPhys, true ); + } + else + { + m_pController->SetEventHandler( this ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Actual simulation for tip controller +//----------------------------------------------------------------------------- +IMotionEvent::simresult_e CTurretTipController::Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) +{ + if ( Enabled() == false ) + return SIM_NOTHING; + + // Don't simulate if we're being carried by the player + if ( m_pParentTurret->IsBeingCarriedByPlayer() ) + return SIM_NOTHING; + + float flAngularLimit = m_angularLimit; + + // If we were just dropped by a friendly player, stabilise better + if ( m_pParentTurret->WasJustDroppedByPlayer() ) + { + // Increase the controller strength a little + flAngularLimit += 20; + } + else + { + // If the turret has some vertical velocity, don't simulate + Vector vecVelocity; + AngularImpulse angImpulse; + pObject->GetVelocity( &vecVelocity, &angImpulse ); + if ( (vecVelocity.LengthSqr() > CNPC_FloorTurret::fMaxTipControllerVelocity) || (angImpulse.LengthSqr() > CNPC_FloorTurret::fMaxTipControllerAngularVelocity) ) + return SIM_NOTHING; + } + + linear.Init(); + + AngularImpulse angVel; + pObject->GetVelocity( NULL, &angVel ); + + matrix3x4_t matrix; + // get the object's local to world transform + pObject->GetPositionMatrix( &matrix ); + + // Get the alignment axis in object space + Vector currentLocalTargetAxis; + VectorIRotate( m_worldGoalAxis, matrix, currentLocalTargetAxis ); + + float invDeltaTime = (1/deltaTime); + angular = ComputeRotSpeedToAlignAxes( m_localTestAxis, currentLocalTargetAxis, angVel, 1.0, invDeltaTime * invDeltaTime, flAngularLimit * invDeltaTime ); + + return SIM_LOCAL_ACCELERATION; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CTurretTipController::Enable( bool state ) +{ + m_bEnabled = state; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : time - +//----------------------------------------------------------------------------- +void CTurretTipController::Suspend( float time ) +{ + m_flSuspendTime = gpGlobals->curtime + time; +} + + +float CTurretTipController::SuspendedTill( void ) +{ + return m_flSuspendTime; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CTurretTipController::Enabled( void ) +{ + if ( m_flSuspendTime > gpGlobals->curtime ) + return false; + + return m_bEnabled; +} + +//----------------------------------------------------------------------------- +// +// Schedules +// +//----------------------------------------------------------------------------- +AI_BEGIN_CUSTOM_NPC( npc_turret_floor, CNPC_FloorTurret ) + + DECLARE_INTERACTION( g_interactionTurretStillStanding ); + +AI_END_CUSTOM_NPC() |