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.
1030 lines
28 KiB
1030 lines
28 KiB
#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()
|
|
|