1031 lines
29 KiB
C++
1031 lines
29 KiB
C++
#include "cbase.h"
|
|
#include "asw_mortarbug.h"
|
|
#include "te_effect_dispatch.h"
|
|
#include "npc_bullseye.h"
|
|
#include "npcevent.h"
|
|
#include "asw_marine.h"
|
|
#include "asw_parasite.h"
|
|
#include "soundenvelope.h"
|
|
#include "ai_memory.h"
|
|
#include "asw_gamerules.h"
|
|
#include "asw_weapon.h"
|
|
#include "asw_shareddefs.h"
|
|
#include "asw_weapon_assault_shotgun_shared.h"
|
|
#include "asw_mortarbug_shell_shared.h"
|
|
#include "particle_parse.h"
|
|
#include "ai_network.h"
|
|
#include "ai_networkmanager.h"
|
|
#include "ai_pathfinder.h"
|
|
#include "ai_link.h"
|
|
#include "asw_util_shared.h"
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
#define ASW_MORTARBUG_MAX_ATTACK_DISTANCE 1500
|
|
|
|
LINK_ENTITY_TO_CLASS( asw_mortarbug, CASW_Mortarbug );
|
|
|
|
float CASW_Mortarbug::s_fNextSpawnSoundTime = 0;
|
|
float CASW_Mortarbug::s_fNextPainSoundTime = 0;
|
|
|
|
#define ASW_MORTARBUG_PROJECTILE "asw_mortarbug_shell"
|
|
|
|
BEGIN_DATADESC( CASW_Mortarbug )
|
|
DEFINE_FIELD( m_fLastFireTime, FIELD_TIME ),
|
|
DEFINE_FIELD( m_fLastTouchHurtTime, FIELD_TIME ),
|
|
DEFINE_FIELD( m_fGibTime, FIELD_TIME ),
|
|
DEFINE_FIELD( m_flIdleDelay, FIELD_TIME ),
|
|
DEFINE_FIELD( m_vecSaveSpitVelocity, FIELD_VECTOR ),
|
|
END_DATADESC()
|
|
|
|
ConVar asw_mortarbug_speedboost( "asw_mortarbug_speedboost", "1.0",FCVAR_CHEAT , "boost speed for the mortarbug" );
|
|
ConVar asw_mortarbug_touch_damage( "asw_mortarbug_touch_damage", "5",FCVAR_CHEAT , "Damage caused by mortarbug on touch" );
|
|
ConVar asw_mortarbug_spitspeed( "asw_mortarbug_spitspeed", "350", FCVAR_CHEAT, "Speed at which mortarbug grenade travels." );
|
|
ConVar asw_debug_mortarbug( "asw_debug_mortarbug", "0", FCVAR_NONE, "Display mortarbug debug info" );
|
|
ConVar asw_mortarbug_face_target("asw_mortarbug_face_target", "1", FCVAR_CHEAT, "Mortarbug faces his target when moving" );
|
|
|
|
extern ConVar sv_gravity;
|
|
extern ConVar asw_mortarbug_shell_gravity; // TODO: Replace with proper spit projectile's gravity
|
|
|
|
// Anim Events
|
|
int AE_MORTARBUG_CHARGE; // charging up to spit
|
|
int AE_MORTARBUG_LAUNCH; // actual launch of the projectile
|
|
|
|
// Activities
|
|
int ACT_MORTARBUG_SPIT;
|
|
|
|
CASW_Mortarbug::CASW_Mortarbug()
|
|
{
|
|
m_fLastFireTime = 0;
|
|
m_fLastTouchHurtTime = 0;
|
|
m_pszAlienModelName = SWARM_MORTARBUG_MODEL;
|
|
m_nAlienCollisionGroup = ASW_COLLISION_GROUP_ALIEN;
|
|
}
|
|
|
|
CASW_Mortarbug::~CASW_Mortarbug()
|
|
{
|
|
}
|
|
|
|
void CASW_Mortarbug::Spawn( void )
|
|
{
|
|
SetHullType(HULL_WIDE_SHORT);
|
|
|
|
BaseClass::Spawn();
|
|
|
|
SetHullType(HULL_WIDE_SHORT);
|
|
UTIL_SetSize(this, Vector(-23,-23,0), Vector(23,23,69));
|
|
|
|
m_iHealth = ASWGameRules()->ModifyAlienHealthBySkillLevel(350);
|
|
|
|
CapabilitiesAdd( bits_CAP_MOVE_GROUND | bits_CAP_INNATE_RANGE_ATTACK1 );
|
|
|
|
m_takedamage = DAMAGE_NO; // alien is invulnerable until she finds her first enemy
|
|
}
|
|
|
|
void CASW_Mortarbug::Precache( void )
|
|
{
|
|
PrecacheScriptSound( "ASW_MortarBug.Idle" );
|
|
PrecacheScriptSound( "ASW_MortarBug.Pain" );
|
|
PrecacheScriptSound( "ASW_MortarBug.Spit" );
|
|
PrecacheScriptSound( "ASW_MortarBug.OnFire" );
|
|
PrecacheScriptSound( "ASW_MortarBug.Death" );
|
|
PrecacheParticleSystem( "mortar_launch" );
|
|
|
|
UTIL_PrecacheOther( ASW_MORTARBUG_PROJECTILE );
|
|
|
|
BaseClass::Precache();
|
|
}
|
|
|
|
float CASW_Mortarbug::GetIdealSpeed() const
|
|
{
|
|
return asw_mortarbug_speedboost.GetFloat() * BaseClass::GetIdealSpeed() * m_flPlaybackRate;
|
|
}
|
|
|
|
|
|
float CASW_Mortarbug::GetIdealAccel( ) const
|
|
{
|
|
return GetIdealSpeed() * 1.5f;
|
|
}
|
|
|
|
float CASW_Mortarbug::MaxYawSpeed( void )
|
|
{
|
|
if ( m_bElectroStunned.Get() )
|
|
return 0.1f;
|
|
|
|
return 16.0f * asw_mortarbug_speedboost.GetFloat();
|
|
}
|
|
|
|
void CASW_Mortarbug::AlertSound()
|
|
{
|
|
EmitSound( "ASW_MortarBug.Idle" );
|
|
}
|
|
|
|
void CASW_Mortarbug::PainSound( const CTakeDamageInfo &info )
|
|
{
|
|
if (gpGlobals->curtime > m_fNextPainSound && gpGlobals->curtime > s_fNextPainSoundTime)
|
|
{
|
|
m_fNextPainSound = gpGlobals->curtime + 0.5f;
|
|
s_fNextPainSoundTime = gpGlobals->curtime + 1.0f;
|
|
EmitSound("ASW_MortarBug.Pain");
|
|
}
|
|
}
|
|
|
|
void CASW_Mortarbug::AttackSound()
|
|
{
|
|
if (gpGlobals->curtime > s_fNextSpawnSoundTime)
|
|
{
|
|
EmitSound("ASW_MortarBug.Spit");
|
|
s_fNextSpawnSoundTime = gpGlobals->curtime + 2.0f;
|
|
}
|
|
}
|
|
|
|
void CASW_Mortarbug::IdleSound()
|
|
{
|
|
EmitSound( "ASW_MortarBug.Idle" );
|
|
m_flIdleDelay = gpGlobals->curtime + 4.0f;
|
|
}
|
|
|
|
void CASW_Mortarbug::DeathSound( const CTakeDamageInfo &info )
|
|
{
|
|
EmitSound("ASW_MortarBug.Death");
|
|
}
|
|
|
|
// make the mortarbug look at his enemy
|
|
bool CASW_Mortarbug::OverrideMoveFacing( const AILocalMoveGoal_t &move, float flInterval )
|
|
{
|
|
Vector vecFacePosition = vec3_origin;
|
|
CBaseEntity *pFaceTarget = NULL;
|
|
bool bFaceTarget = false;
|
|
|
|
if ( GetEnemy() && GetNavigator()->GetMovementActivity() == ACT_RUN )
|
|
{
|
|
Vector vecEnemyLKP = GetEnemyLKP();
|
|
|
|
// Only start facing when we're close enough
|
|
if ( ( UTIL_DistApprox( vecEnemyLKP, GetAbsOrigin() ) < 1500 ) )
|
|
{
|
|
vecFacePosition = vecEnemyLKP;
|
|
pFaceTarget = GetEnemy();
|
|
bFaceTarget = true;
|
|
}
|
|
}
|
|
|
|
// Face
|
|
if ( bFaceTarget && asw_mortarbug_face_target.GetBool() )
|
|
{
|
|
AddFacingTarget( pFaceTarget, vecFacePosition, 1.0, 0.2 );
|
|
}
|
|
|
|
return BaseClass::OverrideMoveFacing( move, flInterval );
|
|
}
|
|
|
|
void CASW_Mortarbug::HandleAnimEvent( animevent_t *pEvent )
|
|
{
|
|
int nEvent = pEvent->Event();
|
|
|
|
if ( nEvent == AE_MORTARBUG_LAUNCH )
|
|
{
|
|
// The point in our spit animation where we should actually spawn the projectile
|
|
AttackSound();
|
|
SpawnProjectile();
|
|
m_flNextAttack = gpGlobals->curtime + 2.0f;
|
|
return;
|
|
}
|
|
else if ( nEvent == AE_MORTARBUG_CHARGE )
|
|
{
|
|
// TODO: Get ivan to make a pre-attack sound and play it here?
|
|
return;
|
|
}
|
|
|
|
BaseClass::HandleAnimEvent( pEvent );
|
|
}
|
|
|
|
// harvester can attack without LOS so long as they're near enough
|
|
bool CASW_Mortarbug::WeaponLOSCondition(const Vector &ownerPos, const Vector &targetPos, bool bSetConditions)
|
|
{
|
|
if (targetPos.DistTo(ownerPos) < ASW_MORTARBUG_MAX_ATTACK_DISTANCE)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool CASW_Mortarbug::FCanCheckAttacks()
|
|
{
|
|
if ( GetNavType() == NAV_CLIMB || GetNavType() == NAV_JUMP )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: For innate melee attack
|
|
// Input :
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
float CASW_Mortarbug::InnateRange1MinRange( void )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
float CASW_Mortarbug::InnateRange1MaxRange( void )
|
|
{
|
|
return ASW_MORTARBUG_MAX_ATTACK_DISTANCE;
|
|
}
|
|
|
|
// make sure the harvester backs away when he's near us (he should sit back and lay critters)
|
|
int CASW_Mortarbug::RangeAttack1Conditions( float flDot, float flDist )
|
|
{
|
|
if (GetEnemy() == NULL)
|
|
{
|
|
return( COND_NONE );
|
|
}
|
|
|
|
if ( gpGlobals->curtime < m_flNextAttack )
|
|
{
|
|
return( COND_NONE );
|
|
}
|
|
|
|
float flTooClose = InnateRange1MinRange();
|
|
if ( flDist > InnateRange1MaxRange() )
|
|
{
|
|
return( COND_TOO_FAR_TO_ATTACK );
|
|
}
|
|
else if ( flDist < flTooClose )
|
|
{
|
|
return( COND_TOO_CLOSE_TO_ATTACK );
|
|
}
|
|
else if ( flDot < 0.65 )
|
|
{
|
|
return( COND_NOT_FACING_ATTACK );
|
|
}
|
|
|
|
return( COND_CAN_RANGE_ATTACK1 );
|
|
}
|
|
|
|
int CASW_Mortarbug::SelectSchedule()
|
|
{
|
|
if ( HasCondition( COND_NEW_ENEMY ) && GetHealth() > 0 )
|
|
{
|
|
m_takedamage = DAMAGE_YES;
|
|
}
|
|
|
|
if ( HasCondition( COND_FLOATING_OFF_GROUND ) )
|
|
{
|
|
SetGravity( 1.0 );
|
|
SetGroundEntity( NULL );
|
|
return SCHED_FALL_TO_GROUND;
|
|
}
|
|
|
|
if (m_NPCState == NPC_STATE_COMBAT)
|
|
return SelectMortarbugCombatSchedule();
|
|
|
|
return BaseClass::SelectSchedule();
|
|
}
|
|
|
|
int CASW_Mortarbug::SelectMortarbugCombatSchedule()
|
|
{
|
|
int nSched = SelectFlinchSchedule_ASW();
|
|
if ( nSched != SCHED_NONE )
|
|
{
|
|
// if we flinch, push forward the next attack
|
|
float spawn_interval = 2.0f;
|
|
m_flNextAttack = gpGlobals->curtime + spawn_interval;
|
|
return nSched;
|
|
}
|
|
|
|
if ( HasCondition(COND_NEW_ENEMY) && gpGlobals->curtime - GetEnemies()->FirstTimeSeen(GetEnemy()) < 2.0 )
|
|
{
|
|
return SCHED_WAKE_ANGRY;
|
|
}
|
|
|
|
if ( HasCondition( COND_ENEMY_DEAD ) )
|
|
{
|
|
// clear the current (dead) enemy and try to find another.
|
|
SetEnemy( NULL );
|
|
|
|
if ( ChooseEnemy() )
|
|
{
|
|
ClearCondition( COND_ENEMY_DEAD );
|
|
return SelectSchedule();
|
|
}
|
|
|
|
SetState( NPC_STATE_ALERT );
|
|
return SelectSchedule();
|
|
}
|
|
|
|
if ( GetShotRegulator()->IsInRestInterval() )
|
|
{
|
|
if ( HasCondition(COND_CAN_RANGE_ATTACK1) )
|
|
return SCHED_COMBAT_FACE;
|
|
}
|
|
|
|
// we can see the enemy
|
|
if ( HasCondition(COND_CAN_RANGE_ATTACK1) )
|
|
{
|
|
if ( GetEnemy() )
|
|
{
|
|
m_vSavePosition = GetEnemy()->BodyTarget( GetAbsOrigin() );
|
|
//if ( CanFireMortar( GetMortarFireSource( &GetAbsOrigin() ), m_vSavePosition, false ) )
|
|
{
|
|
return SCHED_RANGE_ATTACK1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( HasCondition(COND_CAN_RANGE_ATTACK2) )
|
|
return SCHED_RANGE_ATTACK2;
|
|
|
|
if ( HasCondition(COND_CAN_MELEE_ATTACK1) )
|
|
return SCHED_MELEE_ATTACK1;
|
|
|
|
if ( HasCondition(COND_CAN_MELEE_ATTACK2) )
|
|
return SCHED_MELEE_ATTACK2;
|
|
|
|
if ( HasCondition(COND_NOT_FACING_ATTACK) )
|
|
return SCHED_COMBAT_FACE;
|
|
|
|
return SCHED_ESTABLISH_LINE_OF_MORTAR_FIRE;
|
|
|
|
// if we're not attacking, then back away
|
|
//return SCHED_RUN_FROM_ENEMY;
|
|
}
|
|
|
|
int CASW_Mortarbug::TranslateSchedule( int scheduleType )
|
|
{
|
|
if ( scheduleType == SCHED_RANGE_ATTACK1 )
|
|
{
|
|
RemoveAllGestures();
|
|
return SCHED_ASW_MORTARBUG_SPIT;
|
|
}
|
|
|
|
if ( scheduleType == SCHED_COMBAT_FACE && IsUnreachable( GetEnemy() ) )
|
|
return SCHED_TAKE_COVER_FROM_ENEMY;
|
|
|
|
return BaseClass::TranslateSchedule( scheduleType );
|
|
}
|
|
|
|
CBaseEntity* CASW_Mortarbug::SpawnProjectile()
|
|
{
|
|
if ( !GetEnemy() )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
// TODO: Replace with launch attachment point
|
|
Vector vSpitPos = GetAbsOrigin() + Vector( 0, 0, 10 );
|
|
GetAttachment( "attach_spit", vSpitPos );
|
|
|
|
Vector vTarget;
|
|
|
|
// If our enemy is looking at us and far enough away, lead him
|
|
if ( HasCondition( COND_ENEMY_FACING_ME ) && UTIL_DistApprox( GetAbsOrigin(), GetEnemy()->GetAbsOrigin() ) > (40*12) )
|
|
{
|
|
UTIL_PredictedPosition( GetEnemy(), 0.5f, &vTarget );
|
|
vTarget.z = GetEnemy()->GetAbsOrigin().z;
|
|
}
|
|
else
|
|
{
|
|
// Otherwise he can't see us and he won't be able to dodge
|
|
vTarget = GetEnemy()->BodyTarget( vSpitPos, true );
|
|
}
|
|
|
|
vTarget[2] += random->RandomFloat( 0.0f, 32.0f );
|
|
|
|
// Try and spit at our target
|
|
Vector vecToss;
|
|
if ( GetSpitVector( vSpitPos, vTarget, &vecToss ) == false )
|
|
{
|
|
// Now try where they were
|
|
if ( GetSpitVector( vSpitPos, m_vSavePosition, &vecToss ) == false )
|
|
{
|
|
// Failing that, just shoot with the old velocity we calculated initially!
|
|
vecToss = m_vecSaveSpitVelocity;
|
|
}
|
|
}
|
|
|
|
// Find what our vertical theta is to estimate the time we'll impact the ground
|
|
Vector vecToTarget = ( vTarget - vSpitPos );
|
|
VectorNormalize( vecToTarget );
|
|
float flVelocity = VectorNormalize( vecToss );
|
|
float flCosTheta = DotProduct( vecToTarget, vecToss );
|
|
float flTime = (vSpitPos-vTarget).Length2D() / ( flVelocity * flCosTheta );
|
|
|
|
// Emit a sound where this is going to hit so that targets get a chance to act correctly
|
|
CSoundEnt::InsertSound( SOUND_DANGER, vTarget, (15*12), flTime, this );
|
|
|
|
// Don't fire again until this volley would have hit the ground (with some lag behind it)
|
|
SetNextAttack( gpGlobals->curtime + flTime + random->RandomFloat( 0.5f, 2.0f ) );
|
|
|
|
CASW_Mortarbug_Shell *pShell = NULL;
|
|
for ( int i = 0; i < 3; i++ )
|
|
{
|
|
pShell = CASW_Mortarbug_Shell::CreateShell( vSpitPos, vecToss, this );
|
|
pShell->m_bDoScreenShake = ( i == 1 );
|
|
pShell->SetAbsVelocity( vecToss * ( flVelocity + 40.0f * (i-1) ) );
|
|
//pShell->SetAbsVelocity( ( vecToss + RandomVector( -0.035f, 0.035f ) ) * flVelocity );
|
|
}
|
|
|
|
// only do effects if we havent done them in the last second
|
|
if ( gpGlobals->curtime > m_fLastFireTime + 1.0f )
|
|
{
|
|
DispatchParticleEffect( "mortar_launch", PATTACH_POINT_FOLLOW, this, "attach_spit" );
|
|
EmitSound( "ASW_MortarBug.Spit" ); // TODO: Replace with launching sound
|
|
}
|
|
|
|
m_fLastFireTime = gpGlobals->curtime;
|
|
|
|
return pShell;
|
|
}
|
|
|
|
void CASW_Mortarbug::StartTouch( CBaseEntity *pOther )
|
|
{
|
|
BaseClass::StartTouch( pOther );
|
|
|
|
CASW_Marine *pMarine = CASW_Marine::AsMarine( pOther );
|
|
if (pMarine)
|
|
{
|
|
// don't hurt him if he was hurt recently
|
|
if (m_fLastTouchHurtTime + 0.6f > gpGlobals->curtime)
|
|
{
|
|
return;
|
|
}
|
|
// hurt the marine
|
|
Vector vecForceDir = (pMarine->GetAbsOrigin() - GetAbsOrigin());
|
|
CTakeDamageInfo info( this, this, asw_mortarbug_touch_damage.GetInt(), DMG_SLASH );
|
|
CalculateMeleeDamageForce( &info, vecForceDir, pMarine->GetAbsOrigin() );
|
|
pMarine->TakeDamage( info );
|
|
m_fLastTouchHurtTime = gpGlobals->curtime;
|
|
}
|
|
}
|
|
|
|
bool CASW_Mortarbug::ShouldGib( const CTakeDamageInfo &info )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int CASW_Mortarbug::SelectDeadSchedule()
|
|
{
|
|
// Adrian - Alread dead (by animation event maybe?)
|
|
// Is it safe to set it to SCHED_NONE?
|
|
if ( m_lifeState == LIFE_DEAD )
|
|
return SCHED_NONE;
|
|
|
|
CleanupOnDeath();
|
|
return SCHED_DIE;
|
|
}
|
|
|
|
int CASW_Mortarbug::SelectFlinchSchedule_ASW()
|
|
{
|
|
// only flinch in easy mode
|
|
if (ASWGameRules() && ASWGameRules()->GetSkillLevel() != 1)
|
|
return SCHED_NONE;
|
|
|
|
if ( !HasCondition(COND_HEAVY_DAMAGE) && !HasCondition(COND_LIGHT_DAMAGE) )
|
|
return SCHED_NONE;
|
|
|
|
if ( IsCurSchedule( SCHED_BIG_FLINCH ) )
|
|
return SCHED_NONE;
|
|
|
|
// only flinch if shot during a spit
|
|
if (! (GetTask() && (GetTask()->iTask == TASK_MORTARBUG_SPIT)) )
|
|
return SCHED_NONE;
|
|
|
|
// Any damage. Break out of my current schedule and flinch.
|
|
Activity iFlinchActivity = GetFlinchActivity( true, false );
|
|
if ( HaveSequenceForActivity( iFlinchActivity ) )
|
|
return SCHED_BIG_FLINCH;
|
|
|
|
return SCHED_NONE;
|
|
}
|
|
|
|
void CASW_Mortarbug::BuildScheduleTestBits()
|
|
{
|
|
BaseClass::BuildScheduleTestBits();
|
|
|
|
if ( IsCurSchedule( SCHED_RUN_FROM_ENEMY ) )
|
|
{
|
|
SetCustomInterruptCondition( COND_CAN_RANGE_ATTACK1 );
|
|
}
|
|
}
|
|
|
|
void CASW_Mortarbug::StartTask( const Task_t *pTask )
|
|
{
|
|
switch( pTask->iTask )
|
|
{
|
|
case TASK_MORTARBUG_SPIT:
|
|
{
|
|
ResetIdealActivity((Activity) ACT_MORTARBUG_SPIT);
|
|
break;
|
|
}
|
|
case TASK_GET_PATH_TO_MORTAR_ENEMY:
|
|
{
|
|
if ( !GetEnemy() )
|
|
{
|
|
TaskFail(FAIL_NO_ENEMY);
|
|
return;
|
|
}
|
|
|
|
float flMaxRange = 2000;
|
|
float flMinRange = 100;
|
|
|
|
Vector vecEnemy = GetEnemy()->BodyTarget( GetAbsOrigin() );
|
|
|
|
|
|
int iNode = FindMortarNode( vecEnemy, flMinRange, flMaxRange, 1.0f );
|
|
if ( iNode != NO_NODE )
|
|
{
|
|
// move to the spot with line of sight
|
|
m_vInterruptSavePosition = g_pBigAINet->GetNode(iNode)->GetPosition(GetHullType());
|
|
}
|
|
else
|
|
{
|
|
TaskFail( FAIL_NO_SHOOT );
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
BaseClass::StartTask( pTask );
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CASW_Mortarbug::RunTask( const Task_t *pTask )
|
|
{
|
|
switch ( pTask->iTask )
|
|
{
|
|
case TASK_MORTARBUG_SPIT:
|
|
{
|
|
if (IsActivityFinished())
|
|
{
|
|
TaskComplete();
|
|
}
|
|
break;
|
|
}
|
|
case TASK_GET_PATH_TO_MORTAR_ENEMY:
|
|
{
|
|
if ( GetEnemy() == NULL )
|
|
{
|
|
TaskFail(FAIL_NO_ENEMY);
|
|
return;
|
|
}
|
|
if ( GetTaskInterrupt() > 0 )
|
|
{
|
|
ClearTaskInterrupt();
|
|
|
|
Vector vecEnemy = GetEnemy()->GetAbsOrigin();
|
|
AI_NavGoal_t goal( m_vInterruptSavePosition, ACT_RUN, AIN_HULL_TOLERANCE );
|
|
|
|
GetNavigator()->SetGoal( goal, AIN_CLEAR_TARGET );
|
|
GetNavigator()->SetArrivalDirection( vecEnemy - goal.dest );
|
|
}
|
|
else
|
|
TaskInterrupt();
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
BaseClass::RunTask(pTask);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// only shock damage counts as heavy (and thus causes a flinch even during normal running)
|
|
bool CASW_Mortarbug::IsHeavyDamage( const CTakeDamageInfo &info )
|
|
{
|
|
// shock damage never causes flinching
|
|
if (( info.GetDamageType() & DMG_SHOCK ) != 0 )
|
|
return false;
|
|
|
|
// explosions always cause a flinch
|
|
if (( info.GetDamageType() & DMG_BLAST ) != 0 )
|
|
return true;
|
|
|
|
CASW_Marine *pMarine = dynamic_cast<CASW_Marine*>(info.GetAttacker());
|
|
if (pMarine && pMarine->GetActiveASWWeapon())
|
|
{
|
|
return pMarine->GetActiveASWWeapon()->ShouldAlienFlinch(this, info);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void CASW_Mortarbug::Event_Killed( const CTakeDamageInfo &info )
|
|
{
|
|
BaseClass::Event_Killed(info);
|
|
|
|
m_fGibTime = gpGlobals->curtime + random->RandomFloat(20.0f, 30.0f);
|
|
}
|
|
|
|
int CASW_Mortarbug::OnTakeDamage_Alive( const CTakeDamageInfo &info )
|
|
{
|
|
if ( info.GetInflictor() && info.GetInflictor()->Classify() == CLASS_ASW_MORTAR_SHELL )
|
|
return 0;
|
|
|
|
CTakeDamageInfo newInfo(info);
|
|
|
|
float damage = info.GetDamage();
|
|
// reduce damage from shotguns and mining laser
|
|
if (info.GetDamageType() & DMG_ENERGYBEAM)
|
|
{
|
|
damage *= 0.4f;
|
|
}
|
|
if (info.GetDamageType() & DMG_BUCKSHOT)
|
|
{
|
|
// hack to reduce vindicator damage (not reducing normal shotty as much as it's not too strong)
|
|
if (info.GetAttacker() && info.GetAttacker()->Classify() == CLASS_ASW_MARINE)
|
|
{
|
|
CASW_Marine *pMarine = dynamic_cast<CASW_Marine*>(info.GetAttacker());
|
|
if (pMarine)
|
|
{
|
|
CASW_Weapon_Assault_Shotgun *pVindicator = dynamic_cast<CASW_Weapon_Assault_Shotgun*>(pMarine->GetActiveASWWeapon());
|
|
if (pVindicator)
|
|
damage *= 0.45f;
|
|
else
|
|
damage *= 0.6f;
|
|
}
|
|
}
|
|
}
|
|
|
|
newInfo.SetDamage(damage);
|
|
|
|
return BaseClass::OnTakeDamage_Alive(newInfo);
|
|
}
|
|
|
|
void CASW_Mortarbug::NPCThink()
|
|
{
|
|
BaseClass::NPCThink();
|
|
|
|
if (m_fGibTime != 0 && gpGlobals->curtime > m_fGibTime)
|
|
{
|
|
CEffectData data;
|
|
|
|
data.m_vOrigin = WorldSpaceCenter();
|
|
data.m_vNormal = Vector(0,0,1);
|
|
|
|
data.m_flScale = RemapVal( m_iHealth, 0, -500, 1, 3 );
|
|
data.m_flScale = clamp( data.m_flScale, 1, 3 );
|
|
data.m_nColor = 1;
|
|
data.m_fFlags = IsOnFire() ? ASW_GIBFLAG_ON_FIRE : 0;
|
|
|
|
DispatchEffect( "HarvesterGib", data );
|
|
UTIL_Remove( this );
|
|
SetThink( NULL );
|
|
}
|
|
}
|
|
|
|
|
|
bool CASW_Mortarbug::ShouldPlayIdleSound( void )
|
|
{
|
|
return false; // asw temp
|
|
|
|
//Only do idles in the right states
|
|
if ( ( m_NPCState != NPC_STATE_IDLE && m_NPCState != NPC_STATE_ALERT ) )
|
|
return false;
|
|
|
|
//Gagged monsters don't talk
|
|
if ( m_spawnflags & SF_NPC_GAG )
|
|
return false;
|
|
|
|
//Don't cut off another sound or play again too soon
|
|
if ( m_flIdleDelay > gpGlobals->curtime )
|
|
return false;
|
|
|
|
//Randomize it a bit
|
|
if ( random->RandomInt( 0, 20 ) )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Returns whether the enemy has been seen within the time period supplied
|
|
// Input : flTime - Timespan we consider
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
bool CASW_Mortarbug::SeenEnemyWithinTime( float flTime )
|
|
{
|
|
float flLastSeenTime = GetEnemies()->LastTimeSeen( GetEnemy() );
|
|
return ( flLastSeenTime != 0.0f && ( gpGlobals->curtime - flLastSeenTime ) < flTime );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Test whether this mortarbug can hit the target
|
|
//-----------------------------------------------------------------------------
|
|
bool CASW_Mortarbug::InnateWeaponLOSCondition( const Vector &ownerPos, const Vector &targetPos, bool bSetConditions )
|
|
{
|
|
if ( GetNextAttack() > gpGlobals->curtime )
|
|
return false;
|
|
|
|
// If we can see the enemy, or we've seen them in the last few seconds just try to lob in there
|
|
if ( SeenEnemyWithinTime( 3.0f ) )
|
|
{
|
|
Vector vSpitPos = GetAbsOrigin() + Vector(0, 0, 10); // TODO: replace with attachment point to fire from
|
|
|
|
return GetSpitVector( vSpitPos, targetPos, &m_vecSaveSpitVelocity );
|
|
}
|
|
|
|
return BaseClass::InnateWeaponLOSCondition( ownerPos, targetPos, bSetConditions );
|
|
}
|
|
|
|
//
|
|
// FIXME: Create this in a better fashion!
|
|
//
|
|
|
|
Vector VecCheckThrowTolerance( CBaseEntity *pEdict, const Vector &vecSpot1, Vector vecSpot2, float flSpeed, float flTolerance )
|
|
{
|
|
flSpeed = MAX( 1.0f, flSpeed );
|
|
|
|
float flGravity = sv_gravity.GetFloat() * asw_mortarbug_shell_gravity.GetFloat();
|
|
|
|
Vector vecGrenadeVel = (vecSpot2 - vecSpot1);
|
|
|
|
// throw at a constant time
|
|
float time = vecGrenadeVel.Length( ) / flSpeed;
|
|
vecGrenadeVel = vecGrenadeVel * (1.0 / time);
|
|
|
|
// adjust upward toss to compensate for gravity loss
|
|
vecGrenadeVel.z += flGravity * time * 0.5;
|
|
|
|
Vector vecApex = vecSpot1 + (vecSpot2 - vecSpot1) * 0.5;
|
|
vecApex.z += 0.5 * flGravity * (time * 0.5) * (time * 0.5);
|
|
|
|
|
|
trace_t tr;
|
|
UTIL_TraceLine( vecSpot1, vecApex, MASK_SOLID, pEdict, COLLISION_GROUP_NONE, &tr );
|
|
if (tr.fraction != 1.0)
|
|
{
|
|
// fail!
|
|
if ( asw_debug_mortarbug.GetBool() )
|
|
{
|
|
NDebugOverlay::Line( vecSpot1, vecApex, 255, 0, 0, true, 5.0 );
|
|
}
|
|
|
|
return vec3_origin;
|
|
}
|
|
|
|
if ( asw_debug_mortarbug.GetBool() )
|
|
{
|
|
NDebugOverlay::Line( vecSpot1, vecApex, 0, 255, 0, true, 5.0 );
|
|
}
|
|
|
|
UTIL_TraceLine( vecApex, vecSpot2, MASK_SOLID_BRUSHONLY, pEdict, COLLISION_GROUP_NONE, &tr );
|
|
if ( tr.fraction != 1.0 )
|
|
{
|
|
bool bFail = true;
|
|
|
|
// Didn't make it all the way there, but check if we're within our tolerance range
|
|
if ( flTolerance > 0.0f )
|
|
{
|
|
float flNearness = ( tr.endpos - vecSpot2 ).LengthSqr();
|
|
if ( flNearness < Square( flTolerance ) )
|
|
{
|
|
if ( asw_debug_mortarbug.GetBool() )
|
|
{
|
|
NDebugOverlay::Sphere( tr.endpos, vec3_angle, flTolerance, 0, 255, 0, 0, true, 5.0 );
|
|
}
|
|
|
|
bFail = false;
|
|
}
|
|
}
|
|
|
|
if ( bFail )
|
|
{
|
|
if ( asw_debug_mortarbug.GetBool() )
|
|
{
|
|
NDebugOverlay::Line( vecApex, vecSpot2, 255, 0, 0, true, 5.0 );
|
|
NDebugOverlay::Sphere( tr.endpos, vec3_angle, flTolerance, 255, 0, 0, 0, true, 5.0 );
|
|
}
|
|
return vec3_origin;
|
|
}
|
|
}
|
|
|
|
if ( asw_debug_mortarbug.GetBool() )
|
|
{
|
|
NDebugOverlay::Line( vecApex, vecSpot2, 0, 255, 0, true, 5.0 );
|
|
}
|
|
|
|
return vecGrenadeVel;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Get a toss direction that will properly lob spit to hit a target
|
|
// Input : &vecStartPos - Where the spit will start from
|
|
// &vecTarget - Where the spit is meant to land
|
|
// *vecOut - The resulting vector to lob the spit
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
bool CASW_Mortarbug::GetSpitVector( const Vector &vecStartPos, const Vector &vecTarget, Vector *vecOut )
|
|
{
|
|
// Try the most direct route
|
|
Vector vecToss = VecCheckThrowTolerance( this, vecStartPos, vecTarget, asw_mortarbug_spitspeed.GetFloat(), (10.0f*12.0f) );
|
|
|
|
// If this failed then try a little faster (flattens the arc)
|
|
if ( vecToss == vec3_origin )
|
|
{
|
|
vecToss = VecCheckThrowTolerance( this, vecStartPos, vecTarget, asw_mortarbug_spitspeed.GetFloat() * 1.5f, (10.0f*12.0f) );
|
|
if ( vecToss == vec3_origin )
|
|
return false;
|
|
}
|
|
|
|
// Save out the result
|
|
if ( vecOut )
|
|
{
|
|
*vecOut = vecToss;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CASW_Mortarbug::CanFireMortar( const Vector &vecSrc, const Vector &vecDest, bool bDrawArc )
|
|
{
|
|
float flGravity = asw_mortarbug_shell_gravity.GetFloat();
|
|
|
|
Vector vecVelocity = UTIL_LaunchVector( vecSrc, vecDest, flGravity ) * 28.0f;
|
|
|
|
Vector vecResult = UTIL_Check_Throw( vecSrc, vecVelocity, flGravity,
|
|
-Vector( 12,12,12 ), Vector( 12,12,12 ), MASK_NPCSOLID, COLLISION_GROUP_PROJECTILE, this, bDrawArc );
|
|
|
|
float flDist = vecResult.DistTo( vecDest );
|
|
return ( flDist < 50.0f );
|
|
}
|
|
|
|
Vector CASW_Mortarbug::GetMortarFireSource( const Vector *vecStandingPos )
|
|
{
|
|
Vector vecOrigin = GetAbsOrigin();
|
|
Vector vSpitPos = GetAbsOrigin() + Vector( 0, 0, 10 );
|
|
GetAttachment( "attach_spit", vSpitPos );
|
|
|
|
|
|
return *vecStandingPos + vSpitPos - vecOrigin;
|
|
}
|
|
|
|
int CASW_Mortarbug::FindMortarNode( const Vector &vThreatPos, float flMinThreatDist, float flMaxThreatDist, float flBlockTime )
|
|
{
|
|
if ( !CAI_NetworkManager::NetworksLoaded() )
|
|
return NO_NODE;
|
|
|
|
AI_PROFILE_SCOPE( CASW_Mortarbug::FindMortarNode );
|
|
|
|
Remember( bits_MEMORY_TASK_EXPENSIVE );
|
|
|
|
int iMyNode = GetPathfinder()->NearestNodeToNPC();
|
|
if ( iMyNode == NO_NODE )
|
|
{
|
|
Vector pos = GetAbsOrigin();
|
|
DevWarning( 2, "FindMortarNode() - %s has no nearest node! (Check near %f %f %f)\n", GetClassname(), pos.x, pos.y, pos.z);
|
|
return NO_NODE;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------
|
|
// We're going to search for a shoot node by expanding to our current node's neighbors
|
|
// and then their neighbors, until a shooting position is found, or all nodes are beyond MaxDist
|
|
// ------------------------------------------------------------------------------------
|
|
AI_NearNode_t *pBuffer = (AI_NearNode_t *)stackalloc( sizeof(AI_NearNode_t) * g_pBigAINet->NumNodes() );
|
|
CNodeList list( pBuffer, g_pBigAINet->NumNodes() );
|
|
CVarBitVec wasVisited(g_pBigAINet->NumNodes()); // Nodes visited
|
|
|
|
// mark start as visited
|
|
wasVisited.Set( iMyNode );
|
|
list.Insert( AI_NearNode_t(iMyNode, 0) );
|
|
|
|
static int nSearchRandomizer = 0; // tries to ensure the links are searched in a different order each time;
|
|
|
|
while ( list.Count() )
|
|
{
|
|
int nodeIndex = list.ElementAtHead().nodeIndex;
|
|
// remove this item from the list
|
|
list.RemoveAtHead();
|
|
|
|
const Vector &nodeOrigin = g_pBigAINet->GetNode(nodeIndex)->GetPosition(GetHullType());
|
|
|
|
// HACKHACK: Can't we rework this loop and get rid of this?
|
|
// skip the starting node, or we probably wouldn't have called this function.
|
|
if ( nodeIndex != iMyNode )
|
|
{
|
|
bool skip = false;
|
|
|
|
// Don't accept climb nodes, and assume my nearest node isn't valid because
|
|
// we decided to make this check in the first place. Keep moving
|
|
if ( !skip && !g_pBigAINet->GetNode(nodeIndex)->IsLocked() &&
|
|
g_pBigAINet->GetNode(nodeIndex)->GetType() != NODE_CLIMB )
|
|
{
|
|
// Now check its distance and only accept if in range
|
|
float flThreatDist = ( nodeOrigin - vThreatPos ).Length();
|
|
|
|
if ( flThreatDist < flMaxThreatDist &&
|
|
flThreatDist > flMinThreatDist )
|
|
{
|
|
//CAI_Node *pNode = g_pBigAINet->GetNode(nodeIndex);
|
|
if ( CanFireMortar( GetMortarFireSource( &nodeOrigin ), vThreatPos, asw_debug_mortarbug.GetInt() == 2 ) )
|
|
{
|
|
// Note when this node was used, so we don't try
|
|
// to use it again right away.
|
|
g_pBigAINet->GetNode(nodeIndex)->Lock( flBlockTime );
|
|
|
|
if ( asw_debug_mortarbug.GetBool() )
|
|
{
|
|
NDebugOverlay::Text( nodeOrigin, CFmtStr( "%d:los", nodeIndex), false, 1 );
|
|
|
|
// draw the arc
|
|
CanFireMortar( GetMortarFireSource( &nodeOrigin ), vThreatPos, true );
|
|
}
|
|
|
|
// The next NPC who searches should use a slight different pattern
|
|
nSearchRandomizer = nodeIndex;
|
|
return nodeIndex;
|
|
}
|
|
else
|
|
{
|
|
if ( asw_debug_mortarbug.GetBool() )
|
|
{
|
|
NDebugOverlay::Text( nodeOrigin, CFmtStr( "%d:!throw", nodeIndex), false, 1 );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( asw_debug_mortarbug.GetBool() )
|
|
{
|
|
CFmtStr msg( "%d:%s", nodeIndex, ( flThreatDist < flMaxThreatDist ) ? "too close" : "too far" );
|
|
NDebugOverlay::Text( nodeOrigin, msg, false, 1 );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Go through each link and add connected nodes to the list
|
|
for (int link=0; link < g_pBigAINet->GetNode(nodeIndex)->NumLinks();link++)
|
|
{
|
|
int index = (link + nSearchRandomizer) % g_pBigAINet->GetNode(nodeIndex)->NumLinks();
|
|
CAI_Link *nodeLink = g_pBigAINet->GetNode(nodeIndex)->GetLinkByIndex(index);
|
|
|
|
if ( !GetPathfinder()->IsLinkUsable( nodeLink, iMyNode ) )
|
|
continue;
|
|
|
|
int newID = nodeLink->DestNodeID(nodeIndex);
|
|
|
|
// If not already visited, add to the list
|
|
if (!wasVisited.IsBitSet(newID))
|
|
{
|
|
float dist = (GetLocalOrigin() - g_pBigAINet->GetNode(newID)->GetPosition(GetHullType())).LengthSqr();
|
|
list.Insert( AI_NearNode_t(newID, dist) );
|
|
wasVisited.Set( newID );
|
|
}
|
|
}
|
|
}
|
|
// We failed. No range attack node node was found
|
|
return NO_NODE;
|
|
}
|
|
|
|
AI_BEGIN_CUSTOM_NPC( asw_mortarbug, CASW_Mortarbug )
|
|
|
|
// Tasks
|
|
DECLARE_TASK( TASK_MORTARBUG_SPIT )
|
|
DECLARE_TASK( TASK_GET_PATH_TO_MORTAR_ENEMY )
|
|
// Activities
|
|
DECLARE_ACTIVITY( ACT_MORTARBUG_SPIT )
|
|
// Events
|
|
DECLARE_ANIMEVENT( AE_MORTARBUG_CHARGE )
|
|
DECLARE_ANIMEVENT( AE_MORTARBUG_LAUNCH )
|
|
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_ASW_MORTARBUG_SPIT,
|
|
|
|
" Tasks"
|
|
" TASK_STOP_MOVING 0"
|
|
" TASK_FACE_ENEMY 0"
|
|
" TASK_ANNOUNCE_ATTACK 1" // 1 = primary attack
|
|
" TASK_MORTARBUG_SPIT 0"
|
|
""
|
|
" Interrupts"
|
|
)
|
|
|
|
DEFINE_SCHEDULE
|
|
(
|
|
SCHED_ESTABLISH_LINE_OF_MORTAR_FIRE,
|
|
|
|
" Tasks"
|
|
" TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_ESTABLISH_LINE_OF_FIRE"
|
|
" TASK_GET_PATH_TO_MORTAR_ENEMY 0"
|
|
" TASK_SPEAK_SENTENCE 1"
|
|
" TASK_RUN_PATH 0"
|
|
" TASK_WAIT_FOR_MOVEMENT 0"
|
|
" TASK_SET_SCHEDULE SCHEDULE:SCHED_COMBAT_FACE"
|
|
""
|
|
" Interrupts "
|
|
" COND_NEW_ENEMY"
|
|
" COND_ENEMY_DEAD"
|
|
" COND_LOST_ENEMY"
|
|
" COND_CAN_RANGE_ATTACK1"
|
|
" COND_CAN_MELEE_ATTACK1"
|
|
" COND_CAN_RANGE_ATTACK2"
|
|
" COND_CAN_MELEE_ATTACK2"
|
|
" COND_HEAR_DANGER"
|
|
)
|
|
|
|
|
|
AI_END_CUSTOM_NPC()
|