You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2330 lines
67 KiB
2330 lines
67 KiB
//========= 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()
|
|
|