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.
431 lines
12 KiB
431 lines
12 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: This is the soldier version of the combine, analogous to the HL1 grunt. |
|
// |
|
//=============================================================================// |
|
|
|
#include "cbase.h" |
|
#include "ai_hull.h" |
|
#include "ai_motor.h" |
|
#include "npc_combines.h" |
|
#include "bitstring.h" |
|
#include "engine/IEngineSound.h" |
|
#include "soundent.h" |
|
#include "ndebugoverlay.h" |
|
#include "npcevent.h" |
|
#include "hl2/hl2_player.h" |
|
#include "game.h" |
|
#include "ammodef.h" |
|
#include "explode.h" |
|
#include "ai_memory.h" |
|
#include "Sprite.h" |
|
#include "soundenvelope.h" |
|
#include "weapon_physcannon.h" |
|
#include "hl2_gamerules.h" |
|
#include "gameweaponmanager.h" |
|
#include "vehicle_base.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
ConVar sk_combine_s_health( "sk_combine_s_health","0"); |
|
ConVar sk_combine_s_kick( "sk_combine_s_kick","0"); |
|
|
|
ConVar sk_combine_guard_health( "sk_combine_guard_health", "0"); |
|
ConVar sk_combine_guard_kick( "sk_combine_guard_kick", "0"); |
|
|
|
// Whether or not the combine guard should spawn health on death |
|
ConVar combine_guard_spawn_health( "combine_guard_spawn_health", "1" ); |
|
|
|
extern ConVar sk_plr_dmg_buckshot; |
|
extern ConVar sk_plr_num_shotgun_pellets; |
|
|
|
//Whether or not the combine should spawn health on death |
|
ConVar combine_spawn_health( "combine_spawn_health", "1" ); |
|
|
|
LINK_ENTITY_TO_CLASS( npc_combine_s, CNPC_CombineS ); |
|
|
|
|
|
#define AE_SOLDIER_BLOCK_PHYSICS 20 // trying to block an incoming physics object |
|
|
|
extern Activity ACT_WALK_EASY; |
|
extern Activity ACT_WALK_MARCH; |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_CombineS::Spawn( void ) |
|
{ |
|
Precache(); |
|
SetModel( STRING( GetModelName() ) ); |
|
|
|
if( IsElite() ) |
|
{ |
|
// Stronger, tougher. |
|
SetHealth( sk_combine_guard_health.GetFloat() ); |
|
SetMaxHealth( sk_combine_guard_health.GetFloat() ); |
|
SetKickDamage( sk_combine_guard_kick.GetFloat() ); |
|
} |
|
else |
|
{ |
|
SetHealth( sk_combine_s_health.GetFloat() ); |
|
SetMaxHealth( sk_combine_s_health.GetFloat() ); |
|
SetKickDamage( sk_combine_s_kick.GetFloat() ); |
|
} |
|
|
|
CapabilitiesAdd( bits_CAP_ANIMATEDFACE ); |
|
CapabilitiesAdd( bits_CAP_MOVE_SHOOT ); |
|
CapabilitiesAdd( bits_CAP_DOORS_GROUP ); |
|
|
|
BaseClass::Spawn(); |
|
|
|
#if HL2_EPISODIC |
|
if (m_iUseMarch && !HasSpawnFlags(SF_NPC_START_EFFICIENT)) |
|
{ |
|
Msg( "Soldier %s is set to use march anim, but is not an efficient AI. The blended march anim can only be used for dead-ahead walks!\n", GetDebugName() ); |
|
} |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
void CNPC_CombineS::Precache() |
|
{ |
|
const char *pModelName = STRING( GetModelName() ); |
|
|
|
if( !Q_stricmp( pModelName, "models/combine_super_soldier.mdl" ) ) |
|
{ |
|
m_fIsElite = true; |
|
} |
|
else |
|
{ |
|
m_fIsElite = false; |
|
} |
|
|
|
if( !GetModelName() ) |
|
{ |
|
SetModelName( MAKE_STRING( "models/combine_soldier.mdl" ) ); |
|
} |
|
|
|
PrecacheModel( STRING( GetModelName() ) ); |
|
|
|
UTIL_PrecacheOther( "item_healthvial" ); |
|
UTIL_PrecacheOther( "weapon_frag" ); |
|
UTIL_PrecacheOther( "item_ammo_ar2_altfire" ); |
|
|
|
BaseClass::Precache(); |
|
} |
|
|
|
|
|
void CNPC_CombineS::DeathSound( const CTakeDamageInfo &info ) |
|
{ |
|
// NOTE: The response system deals with this at the moment |
|
if ( GetFlags() & FL_DISSOLVING ) |
|
return; |
|
|
|
GetSentences()->Speak( "COMBINE_DIE", SENTENCE_PRIORITY_INVALID, SENTENCE_CRITERIA_ALWAYS ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Soldiers use CAN_RANGE_ATTACK2 to indicate whether they can throw |
|
// a grenade. Because they check only every half-second or so, this |
|
// condition must persist until it is updated again by the code |
|
// that determines whether a grenade can be thrown, so prevent the |
|
// base class from clearing it out. (sjb) |
|
//----------------------------------------------------------------------------- |
|
void CNPC_CombineS::ClearAttackConditions( ) |
|
{ |
|
bool fCanRangeAttack2 = HasCondition( COND_CAN_RANGE_ATTACK2 ); |
|
|
|
// Call the base class. |
|
BaseClass::ClearAttackConditions(); |
|
|
|
if( fCanRangeAttack2 ) |
|
{ |
|
// We don't allow the base class to clear this condition because we |
|
// don't sense for it every frame. |
|
SetCondition( COND_CAN_RANGE_ATTACK2 ); |
|
} |
|
} |
|
|
|
void CNPC_CombineS::PrescheduleThink( void ) |
|
{ |
|
/*//FIXME: This doesn't need to be in here, it's all debug info |
|
if( HasCondition( COND_HEAR_PHYSICS_DANGER ) ) |
|
{ |
|
// Don't react unless we see the item!! |
|
CSound *pSound = NULL; |
|
|
|
pSound = GetLoudestSoundOfType( SOUND_PHYSICS_DANGER ); |
|
|
|
if( pSound ) |
|
{ |
|
if( FInViewCone( pSound->GetSoundReactOrigin() ) ) |
|
{ |
|
DevMsg( "OH CRAP!\n" ); |
|
NDebugOverlay::Line( EyePosition(), pSound->GetSoundReactOrigin(), 0, 0, 255, false, 2.0f ); |
|
} |
|
} |
|
} |
|
*/ |
|
|
|
BaseClass::PrescheduleThink(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Allows for modification of the interrupt mask for the current schedule. |
|
// In the most cases the base implementation should be called first. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_CombineS::BuildScheduleTestBits( void ) |
|
{ |
|
//Interrupt any schedule with physics danger (as long as I'm not moving or already trying to block) |
|
if ( m_flGroundSpeed == 0.0 && !IsCurSchedule( SCHED_FLINCH_PHYSICS ) ) |
|
{ |
|
SetCustomInterruptCondition( COND_HEAR_PHYSICS_DANGER ); |
|
} |
|
|
|
BaseClass::BuildScheduleTestBits(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : |
|
// Output : |
|
//----------------------------------------------------------------------------- |
|
int CNPC_CombineS::SelectSchedule ( void ) |
|
{ |
|
return BaseClass::SelectSchedule(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
float CNPC_CombineS::GetHitgroupDamageMultiplier( int iHitGroup, const CTakeDamageInfo &info ) |
|
{ |
|
switch( iHitGroup ) |
|
{ |
|
case HITGROUP_HEAD: |
|
{ |
|
// Soldiers take double headshot damage |
|
return 2.0f; |
|
} |
|
} |
|
|
|
return BaseClass::GetHitgroupDamageMultiplier( iHitGroup, info ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
void CNPC_CombineS::HandleAnimEvent( animevent_t *pEvent ) |
|
{ |
|
switch( pEvent->event ) |
|
{ |
|
case AE_SOLDIER_BLOCK_PHYSICS: |
|
DevMsg( "BLOCKING!\n" ); |
|
m_fIsBlocking = true; |
|
break; |
|
|
|
default: |
|
BaseClass::HandleAnimEvent( pEvent ); |
|
break; |
|
} |
|
} |
|
|
|
void CNPC_CombineS::OnChangeActivity( Activity eNewActivity ) |
|
{ |
|
// Any new sequence stops us blocking. |
|
m_fIsBlocking = false; |
|
|
|
BaseClass::OnChangeActivity( eNewActivity ); |
|
|
|
#if HL2_EPISODIC |
|
// Give each trooper a varied look for his march. Done here because if you do it earlier (eg Spawn, StartTask), the |
|
// pose param gets overwritten. |
|
if (m_iUseMarch) |
|
{ |
|
SetPoseParameter("casual", RandomFloat()); |
|
} |
|
#endif |
|
} |
|
|
|
void CNPC_CombineS::OnListened() |
|
{ |
|
BaseClass::OnListened(); |
|
|
|
if ( HasCondition( COND_HEAR_DANGER ) && HasCondition( COND_HEAR_PHYSICS_DANGER ) ) |
|
{ |
|
if ( HasInterruptCondition( COND_HEAR_DANGER ) ) |
|
{ |
|
ClearCondition( COND_HEAR_PHYSICS_DANGER ); |
|
} |
|
} |
|
|
|
// debugging to find missed schedules |
|
#if 0 |
|
if ( HasCondition( COND_HEAR_DANGER ) && !HasInterruptCondition( COND_HEAR_DANGER ) ) |
|
{ |
|
DevMsg("Ignore danger in %s\n", GetCurSchedule()->GetName() ); |
|
} |
|
#endif |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &info - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_CombineS::Event_Killed( const CTakeDamageInfo &info ) |
|
{ |
|
// Don't bother if we've been told not to, or the player has a megaphyscannon |
|
if ( combine_spawn_health.GetBool() == false || PlayerHasMegaPhysCannon() ) |
|
{ |
|
BaseClass::Event_Killed( info ); |
|
return; |
|
} |
|
|
|
CBasePlayer *pPlayer = ToBasePlayer( info.GetAttacker() ); |
|
|
|
if ( !pPlayer ) |
|
{ |
|
CPropVehicleDriveable *pVehicle = dynamic_cast<CPropVehicleDriveable *>( info.GetAttacker() ) ; |
|
if ( pVehicle && pVehicle->GetDriver() && pVehicle->GetDriver()->IsPlayer() ) |
|
{ |
|
pPlayer = assert_cast<CBasePlayer *>( pVehicle->GetDriver() ); |
|
} |
|
} |
|
|
|
if ( pPlayer != NULL ) |
|
{ |
|
// Elites drop alt-fire ammo, so long as they weren't killed by dissolving. |
|
if( IsElite() ) |
|
{ |
|
#ifdef HL2_EPISODIC |
|
if ( HasSpawnFlags( SF_COMBINE_NO_AR2DROP ) == false ) |
|
#endif |
|
{ |
|
CBaseEntity *pItem = DropItem( "item_ammo_ar2_altfire", WorldSpaceCenter()+RandomVector(-4,4), RandomAngle(0,360) ); |
|
|
|
if ( pItem ) |
|
{ |
|
IPhysicsObject *pObj = pItem->VPhysicsGetObject(); |
|
|
|
if ( pObj ) |
|
{ |
|
Vector vel = RandomVector( -64.0f, 64.0f ); |
|
AngularImpulse angImp = RandomAngularImpulse( -300.0f, 300.0f ); |
|
|
|
vel[2] = 0.0f; |
|
pObj->AddVelocity( &vel, &angImp ); |
|
} |
|
|
|
if( info.GetDamageType() & DMG_DISSOLVE ) |
|
{ |
|
CBaseAnimating *pAnimating = dynamic_cast<CBaseAnimating*>(pItem); |
|
|
|
if( pAnimating ) |
|
{ |
|
pAnimating->Dissolve( NULL, gpGlobals->curtime, false, ENTITY_DISSOLVE_NORMAL ); |
|
} |
|
} |
|
else |
|
{ |
|
WeaponManager_AddManaged( pItem ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
CHalfLife2 *pHL2GameRules = static_cast<CHalfLife2 *>(g_pGameRules); |
|
|
|
// Attempt to drop health |
|
if ( pHL2GameRules->NPC_ShouldDropHealth( pPlayer ) ) |
|
{ |
|
DropItem( "item_healthvial", WorldSpaceCenter()+RandomVector(-4,4), RandomAngle(0,360) ); |
|
pHL2GameRules->NPC_DroppedHealth(); |
|
} |
|
|
|
if ( HasSpawnFlags( SF_COMBINE_NO_GRENADEDROP ) == false ) |
|
{ |
|
// Attempt to drop a grenade |
|
if ( pHL2GameRules->NPC_ShouldDropGrenade( pPlayer ) ) |
|
{ |
|
DropItem( "weapon_frag", WorldSpaceCenter()+RandomVector(-4,4), RandomAngle(0,360) ); |
|
pHL2GameRules->NPC_DroppedGrenade(); |
|
} |
|
} |
|
} |
|
|
|
BaseClass::Event_Killed( info ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &info - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CNPC_CombineS::IsLightDamage( const CTakeDamageInfo &info ) |
|
{ |
|
return BaseClass::IsLightDamage( info ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &info - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CNPC_CombineS::IsHeavyDamage( const CTakeDamageInfo &info ) |
|
{ |
|
// Combine considers AR2 fire to be heavy damage |
|
if ( info.GetAmmoType() == GetAmmoDef()->Index("AR2") ) |
|
return true; |
|
|
|
// 357 rounds are heavy damage |
|
if ( info.GetAmmoType() == GetAmmoDef()->Index("357") ) |
|
return true; |
|
|
|
// Shotgun blasts where at least half the pellets hit me are heavy damage |
|
if ( info.GetDamageType() & DMG_BUCKSHOT ) |
|
{ |
|
int iHalfMax = sk_plr_dmg_buckshot.GetFloat() * sk_plr_num_shotgun_pellets.GetInt() * 0.5; |
|
if ( info.GetDamage() >= iHalfMax ) |
|
return true; |
|
} |
|
|
|
// Rollermine shocks |
|
if( (info.GetDamageType() & DMG_SHOCK) && hl2_episodic.GetBool() ) |
|
{ |
|
return true; |
|
} |
|
|
|
return BaseClass::IsHeavyDamage( info ); |
|
} |
|
|
|
#if HL2_EPISODIC |
|
//----------------------------------------------------------------------------- |
|
// Purpose: Translate base class activities into combot activites |
|
//----------------------------------------------------------------------------- |
|
Activity CNPC_CombineS::NPC_TranslateActivity( Activity eNewActivity ) |
|
{ |
|
// If the special ep2_outland_05 "use march" flag is set, use the more casual marching anim. |
|
if ( m_iUseMarch && eNewActivity == ACT_WALK ) |
|
{ |
|
eNewActivity = ACT_WALK_MARCH; |
|
} |
|
|
|
return BaseClass::NPC_TranslateActivity( eNewActivity ); |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
// Save/Restore |
|
//--------------------------------------------------------- |
|
BEGIN_DATADESC( CNPC_CombineS ) |
|
|
|
DEFINE_KEYFIELD( m_iUseMarch, FIELD_INTEGER, "usemarch" ), |
|
|
|
END_DATADESC() |
|
#endif |