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.
1847 lines
53 KiB
1847 lines
53 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
//============================================================================= |
|
|
|
#include "cbase.h" |
|
#include "npcevent.h" |
|
#include "ai_basenpc_physicsflyer.h" |
|
#include "weapon_physcannon.h" |
|
#include "hl2_player.h" |
|
#include "npc_scanner.h" |
|
#include "IEffects.h" |
|
#include "explode.h" |
|
#include "ai_route.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
ConVar g_debug_basescanner( "g_debug_basescanner", "0", FCVAR_CHEAT ); |
|
|
|
BEGIN_DATADESC( CNPC_BaseScanner ) |
|
DEFINE_EMBEDDED( m_KilledInfo ), |
|
DEFINE_SOUNDPATCH( m_pEngineSound ), |
|
|
|
DEFINE_FIELD( m_flFlyNoiseBase, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_flEngineStallTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_fNextFlySoundTime, FIELD_TIME ), |
|
DEFINE_FIELD( m_nFlyMode, FIELD_INTEGER ), |
|
|
|
DEFINE_FIELD( m_vecDiveBombDirection, FIELD_VECTOR ), |
|
DEFINE_FIELD( m_flDiveBombRollForce, FIELD_FLOAT ), |
|
|
|
// Physics Influence |
|
DEFINE_FIELD( m_hPhysicsAttacker, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_flLastPhysicsInfluenceTime, FIELD_TIME ), |
|
|
|
DEFINE_FIELD( m_flGoalOverrideDistance, FIELD_FLOAT ), |
|
|
|
DEFINE_FIELD( m_flAttackNearDist, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_flAttackFarDist, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_flAttackRange, FIELD_FLOAT ), |
|
|
|
DEFINE_FIELD( m_nPoseTail, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_nPoseDynamo, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_nPoseFlare, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_nPoseFaceVert, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_nPoseFaceHoriz, FIELD_INTEGER ), |
|
|
|
// DEFINE_FIELD( m_bHasSpoken, FIELD_BOOLEAN ), |
|
|
|
DEFINE_FIELD( m_pSmokeTrail, FIELD_CLASSPTR ), |
|
|
|
DEFINE_INPUTFUNC( FIELD_FLOAT, "SetDistanceOverride", InputSetDistanceOverride ), |
|
DEFINE_INPUTFUNC( FIELD_INTEGER, "SetFlightSpeed", InputSetFlightSpeed ), |
|
|
|
DEFINE_THINKFUNC( DiveBombSoundThink ), |
|
END_DATADESC() |
|
|
|
ConVar sk_scanner_dmg_dive( "sk_scanner_dmg_dive","0"); |
|
|
|
//----------------------------------------------------------------------------- |
|
// Think contexts |
|
//----------------------------------------------------------------------------- |
|
static const char *s_pDiveBombSoundThinkContext = "DiveBombSoundThinkContext"; |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
CNPC_BaseScanner::CNPC_BaseScanner() |
|
{ |
|
#ifdef _DEBUG |
|
m_vCurrentBanking.Init(); |
|
#endif |
|
m_pEngineSound = NULL; |
|
m_bHasSpoken = false; |
|
|
|
m_flAttackNearDist = SCANNER_ATTACK_NEAR_DIST; |
|
m_flAttackFarDist = SCANNER_ATTACK_FAR_DIST; |
|
m_flAttackRange = SCANNER_ATTACK_RANGE; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::Spawn(void) |
|
{ |
|
#ifdef _XBOX |
|
// Always fade the corpse |
|
AddSpawnFlags( SF_NPC_FADE_CORPSE ); |
|
AddEffects( EF_NOSHADOW ); |
|
#endif // _XBOX |
|
|
|
SetHullType( HULL_TINY_CENTERED ); |
|
SetHullSizeNormal(); |
|
|
|
SetSolid( SOLID_BBOX ); |
|
AddSolidFlags( FSOLID_NOT_STANDABLE ); |
|
|
|
SetMoveType( MOVETYPE_VPHYSICS ); |
|
|
|
m_bloodColor = DONT_BLEED; |
|
SetViewOffset( Vector(0, 0, 10) ); // Position of the eyes relative to NPC's origin. |
|
m_flFieldOfView = 0.2; |
|
m_NPCState = NPC_STATE_NONE; |
|
|
|
SetNavType( NAV_FLY ); |
|
|
|
AddFlag( FL_FLY ); |
|
|
|
// This entity cannot be dissolved by the combine balls, |
|
// nor does it get killed by the mega physcannon. |
|
AddEFlags( EFL_NO_DISSOLVE | EFL_NO_MEGAPHYSCANNON_RAGDOLL ); |
|
|
|
m_flGoalOverrideDistance = 0.0f; |
|
|
|
m_nFlyMode = SCANNER_FLY_PATROL; |
|
AngleVectors( GetLocalAngles(), &m_vCurrentBanking ); |
|
m_fHeadYaw = 0; |
|
m_pSmokeTrail = NULL; |
|
|
|
SetCurrentVelocity( vec3_origin ); |
|
|
|
// Noise modifier |
|
Vector bobAmount; |
|
bobAmount.x = random->RandomFloat( -2.0f, 2.0f ); |
|
bobAmount.y = random->RandomFloat( -2.0f, 2.0f ); |
|
bobAmount.z = random->RandomFloat( 2.0f, 4.0f ); |
|
if ( random->RandomInt( 0, 1 ) ) |
|
{ |
|
bobAmount.z *= -1.0f; |
|
} |
|
SetNoiseMod( bobAmount ); |
|
|
|
// set flight speed |
|
m_flSpeed = GetMaxSpeed(); |
|
|
|
// -------------------------------------------- |
|
|
|
CapabilitiesAdd( bits_CAP_MOVE_FLY | bits_CAP_SQUAD | bits_CAP_TURN_HEAD | bits_CAP_SKIP_NAV_GROUND_CHECK ); |
|
|
|
NPCInit(); |
|
|
|
m_flFlyNoiseBase = random->RandomFloat( 0, M_PI ); |
|
|
|
m_flNextAttack = gpGlobals->curtime; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::UpdateEfficiency( bool bInPVS ) |
|
{ |
|
SetEfficiency( ( GetSleepState() != AISS_AWAKE ) ? AIE_DORMANT : AIE_NORMAL ); |
|
SetMoveEfficiency( AIME_NORMAL ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
//----------------------------------------------------------------------------- |
|
float CNPC_BaseScanner::GetAutoAimRadius() |
|
{ |
|
if( g_pGameRules->GetAutoAimMode() == AUTOAIM_ON_CONSOLE ) |
|
{ |
|
return 24.0f; |
|
} |
|
|
|
return 12.0f; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Called just before we are deleted. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::UpdateOnRemove( void ) |
|
{ |
|
// Stop combat loops if I'm alive. If I'm dead, the die sound will already have stopped it. |
|
if ( IsAlive() && m_bHasSpoken ) |
|
{ |
|
SentenceStop(); |
|
} |
|
|
|
BaseClass::UpdateOnRemove(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Gets the appropriate next schedule based on current condition |
|
// bits. |
|
//----------------------------------------------------------------------------- |
|
int CNPC_BaseScanner::SelectSchedule(void) |
|
{ |
|
// ---------------------------------------------------- |
|
// If I'm dead, go into a dive bomb |
|
// ---------------------------------------------------- |
|
if ( m_iHealth <= 0 ) |
|
{ |
|
m_flSpeed = SCANNER_MAX_DIVE_BOMB_SPEED; |
|
return SCHED_SCANNER_ATTACK_DIVEBOMB; |
|
} |
|
|
|
// ------------------------------- |
|
// If I'm in a script sequence |
|
// ------------------------------- |
|
if ( m_NPCState == NPC_STATE_SCRIPT ) |
|
return(BaseClass::SelectSchedule()); |
|
|
|
// ------------------------------- |
|
// Flinch |
|
// ------------------------------- |
|
if ( HasCondition(COND_LIGHT_DAMAGE) || HasCondition(COND_HEAVY_DAMAGE) ) |
|
{ |
|
if ( IsHeldByPhyscannon( ) ) |
|
return SCHED_SMALL_FLINCH; |
|
|
|
if ( m_NPCState == NPC_STATE_IDLE ) |
|
return SCHED_SMALL_FLINCH; |
|
|
|
if ( m_NPCState == NPC_STATE_ALERT ) |
|
{ |
|
if ( m_iHealth < ( 3 * m_iMaxHealth / 4 )) |
|
return SCHED_TAKE_COVER_FROM_ORIGIN; |
|
|
|
if ( SelectWeightedSequence( ACT_SMALL_FLINCH ) != -1 ) |
|
return SCHED_SMALL_FLINCH; |
|
} |
|
else |
|
{ |
|
if ( random->RandomInt( 0, 10 ) < 4 ) |
|
return SCHED_SMALL_FLINCH; |
|
} |
|
} |
|
|
|
// I'm being held by the physcannon... struggle! |
|
if ( IsHeldByPhyscannon( ) ) |
|
return SCHED_SCANNER_HELD_BY_PHYSCANNON; |
|
|
|
// ---------------------------------------------------------- |
|
// If I have an enemy |
|
// ---------------------------------------------------------- |
|
if ( GetEnemy() != NULL && GetEnemy()->IsAlive() ) |
|
{ |
|
// Patrol if the enemy has vanished |
|
if ( HasCondition( COND_LOST_ENEMY ) ) |
|
return SCHED_SCANNER_PATROL; |
|
|
|
// Chase via route if we're directly blocked |
|
if ( HasCondition( COND_SCANNER_FLY_BLOCKED ) ) |
|
return SCHED_SCANNER_CHASE_ENEMY; |
|
|
|
// Attack if it's time |
|
if ( gpGlobals->curtime >= m_flNextAttack ) |
|
{ |
|
if ( HasCondition( COND_CAN_MELEE_ATTACK1 ) ) |
|
return SCHED_SCANNER_ATTACK; |
|
} |
|
|
|
// Otherwise fly in low for attack |
|
return SCHED_SCANNER_ATTACK_HOVER; |
|
} |
|
|
|
// Default to patrolling around |
|
return SCHED_SCANNER_PATROL; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::OnScheduleChange( void ) |
|
{ |
|
m_flSpeed = GetMaxSpeed(); |
|
|
|
BaseClass::OnScheduleChange(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: For innate melee attack |
|
//----------------------------------------------------------------------------- |
|
int CNPC_BaseScanner::MeleeAttack1Conditions( float flDot, float flDist ) |
|
{ |
|
if (GetEnemy() == NULL) |
|
{ |
|
return COND_NONE; |
|
} |
|
|
|
// Check too far to attack with 2D distance |
|
float vEnemyDist2D = (GetEnemy()->GetLocalOrigin() - GetLocalOrigin()).Length2D(); |
|
|
|
if (m_flNextAttack > gpGlobals->curtime) |
|
{ |
|
return COND_NONE; |
|
} |
|
else if (vEnemyDist2D > m_flAttackRange) |
|
{ |
|
return COND_TOO_FAR_TO_ATTACK; |
|
} |
|
else if (flDot < 0.7) |
|
{ |
|
return COND_NOT_FACING_ATTACK; |
|
} |
|
return COND_CAN_MELEE_ATTACK1; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : eOldState - |
|
// eNewState - |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::OnStateChange( NPC_STATE eOldState, NPC_STATE eNewState ) |
|
{ |
|
if (( eNewState == NPC_STATE_ALERT ) || ( eNewState == NPC_STATE_COMBAT )) |
|
{ |
|
SetPoseParameter(m_nPoseFlare, 1.0f); |
|
} |
|
else |
|
{ |
|
SetPoseParameter(m_nPoseFlare, 0); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : pTask - |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::StartTask( const Task_t *pTask ) |
|
{ |
|
switch (pTask->iTask) |
|
{ |
|
case TASK_SCANNER_SET_FLY_PATROL: |
|
{ |
|
// Fly in patrol mode and clear any |
|
// remaining target entity |
|
m_nFlyMode = SCANNER_FLY_PATROL; |
|
TaskComplete(); |
|
break; |
|
} |
|
case TASK_SCANNER_SET_FLY_CHASE: |
|
{ |
|
m_nFlyMode = SCANNER_FLY_CHASE; |
|
TaskComplete(); |
|
break; |
|
} |
|
case TASK_SCANNER_SET_FLY_ATTACK: |
|
{ |
|
m_nFlyMode = SCANNER_FLY_ATTACK; |
|
TaskComplete(); |
|
break; |
|
} |
|
|
|
case TASK_SCANNER_SET_FLY_DIVE: |
|
{ |
|
// Pick a direction to divebomb. |
|
if ( GetEnemy() != NULL ) |
|
{ |
|
// Fly towards my enemy |
|
Vector vEnemyPos = GetEnemyLKP(); |
|
m_vecDiveBombDirection = vEnemyPos - GetLocalOrigin(); |
|
} |
|
else |
|
{ |
|
// Pick a random forward and down direction. |
|
Vector forward; |
|
GetVectors( &forward, NULL, NULL ); |
|
m_vecDiveBombDirection = forward + Vector( random->RandomFloat( -10, 10 ), random->RandomFloat( -10, 10 ), random->RandomFloat( -20, -10 ) ); |
|
} |
|
VectorNormalize( m_vecDiveBombDirection ); |
|
|
|
// Calculate a roll force. |
|
m_flDiveBombRollForce = random->RandomFloat( 20.0, 420.0 ); |
|
if ( random->RandomInt( 0, 1 ) ) |
|
{ |
|
m_flDiveBombRollForce *= -1; |
|
} |
|
|
|
DiveBombSoundThink(); |
|
|
|
m_nFlyMode = SCANNER_FLY_DIVE; |
|
TaskComplete(); |
|
break; |
|
} |
|
|
|
default: |
|
BaseClass::StartTask(pTask); |
|
break; |
|
} |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose: Override to split in two when attacked |
|
//------------------------------------------------------------------------------ |
|
int CNPC_BaseScanner::OnTakeDamage_Alive( const CTakeDamageInfo &info ) |
|
{ |
|
// Start smoking when we're nearly dead |
|
if ( m_iHealth < ( m_iMaxHealth - ( m_iMaxHealth / 4 ) ) ) |
|
{ |
|
StartSmokeTrail(); |
|
} |
|
|
|
return (BaseClass::OnTakeDamage_Alive( info )); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose: Override to split in two when attacked |
|
//------------------------------------------------------------------------------ |
|
int CNPC_BaseScanner::OnTakeDamage_Dying( const CTakeDamageInfo &info ) |
|
{ |
|
// do the damage |
|
m_iHealth -= info.GetDamage(); |
|
|
|
if ( m_iHealth < -40 ) |
|
{ |
|
Gib(); |
|
return 1; |
|
} |
|
|
|
return VPhysicsTakeDamage( info ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator ) |
|
{ |
|
if ( info.GetDamageType() & DMG_BULLET) |
|
{ |
|
g_pEffects->Ricochet(ptr->endpos,ptr->plane.normal); |
|
} |
|
|
|
BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Take damage from being thrown by a physcannon |
|
//----------------------------------------------------------------------------- |
|
#define SCANNER_SMASH_SPEED 250.0 // How fast a scanner must slam into something to take full damage |
|
void CNPC_BaseScanner::TakeDamageFromPhyscannon( CBasePlayer *pPlayer ) |
|
{ |
|
CTakeDamageInfo info; |
|
info.SetDamageType( DMG_GENERIC ); |
|
info.SetInflictor( this ); |
|
info.SetAttacker( pPlayer ); |
|
info.SetDamagePosition( GetAbsOrigin() ); |
|
info.SetDamageForce( Vector( 1.0, 1.0, 1.0 ) ); |
|
|
|
// Convert velocity into damage. |
|
Vector vel; |
|
VPhysicsGetObject()->GetVelocity( &vel, NULL ); |
|
float flSpeed = vel.Length(); |
|
|
|
float flFactor = flSpeed / SCANNER_SMASH_SPEED; |
|
|
|
// Clamp. Don't inflict negative damage or massive damage! |
|
flFactor = clamp( flFactor, 0.0f, 2.0f ); |
|
float flDamage = m_iMaxHealth * flFactor; |
|
|
|
#if 0 |
|
Msg("Doing %f damage for %f speed!\n", flDamage, flSpeed ); |
|
#endif |
|
|
|
info.SetDamage( flDamage ); |
|
TakeDamage( info ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Take damage from physics impacts |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::TakeDamageFromPhysicsImpact( int index, gamevcollisionevent_t *pEvent ) |
|
{ |
|
CBaseEntity *pHitEntity = pEvent->pEntities[!index]; |
|
|
|
// NOTE: Augment the normal impact energy scale here. |
|
float flDamageScale = PlayerHasMegaPhysCannon() ? 10.0f : 5.0f; |
|
|
|
// Scale by the mapmaker's energyscale |
|
flDamageScale *= m_impactEnergyScale; |
|
|
|
int damageType = 0; |
|
float damage = CalculateDefaultPhysicsDamage( index, pEvent, flDamageScale, true, damageType ); |
|
if ( damage == 0 ) |
|
return; |
|
|
|
Vector damagePos; |
|
pEvent->pInternalData->GetContactPoint( damagePos ); |
|
Vector damageForce = pEvent->postVelocity[index] * pEvent->pObjects[index]->GetMass(); |
|
if ( damageForce == vec3_origin ) |
|
{ |
|
// This can happen if this entity is motion disabled, and can't move. |
|
// Use the velocity of the entity that hit us instead. |
|
damageForce = pEvent->postVelocity[!index] * pEvent->pObjects[!index]->GetMass(); |
|
} |
|
|
|
// FIXME: this doesn't pass in who is responsible if some other entity "caused" this collision |
|
PhysCallbackDamage( this, CTakeDamageInfo( pHitEntity, pHitEntity, damageForce, damagePos, damage, damageType ), *pEvent, index ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Is the scanner being held? |
|
//----------------------------------------------------------------------------- |
|
bool CNPC_BaseScanner::IsHeldByPhyscannon( ) |
|
{ |
|
return VPhysicsGetObject() && (VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Physics impact |
|
//------------------------------------------------------------------------------ |
|
#define SCANNER_SMASH_TIME 0.75 // How long after being thrown from a physcannon that a manhack is eligible to die from impact |
|
void CNPC_BaseScanner::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent ) |
|
{ |
|
BaseClass::VPhysicsCollision( index, pEvent ); |
|
|
|
// Take no impact damage while being carried. |
|
if ( IsHeldByPhyscannon( ) ) |
|
return; |
|
|
|
CBasePlayer *pPlayer = HasPhysicsAttacker( SCANNER_SMASH_TIME ); |
|
if( pPlayer ) |
|
{ |
|
TakeDamageFromPhyscannon( pPlayer ); |
|
return; |
|
} |
|
|
|
// It also can take physics damage from things thrown by the player. |
|
int otherIndex = !index; |
|
CBaseEntity *pHitEntity = pEvent->pEntities[otherIndex]; |
|
if ( pHitEntity ) |
|
{ |
|
if ( pHitEntity->HasPhysicsAttacker( 0.5f ) ) |
|
{ |
|
// It can take physics damage from things thrown by the player. |
|
TakeDamageFromPhysicsImpact( index, pEvent ); |
|
} |
|
else if ( FClassnameIs( pHitEntity, "prop_combine_ball" ) ) |
|
{ |
|
// It also can take physics damage from a combine ball. |
|
TakeDamageFromPhysicsImpact( index, pEvent ); |
|
} |
|
} |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose: |
|
//------------------------------------------------------------------------------ |
|
void CNPC_BaseScanner::Gib( void ) |
|
{ |
|
if ( IsMarkedForDeletion() ) |
|
return; |
|
|
|
// Sparks |
|
for ( int i = 0; i < 4; i++ ) |
|
{ |
|
Vector sparkPos = GetAbsOrigin(); |
|
sparkPos.x += random->RandomFloat(-12,12); |
|
sparkPos.y += random->RandomFloat(-12,12); |
|
sparkPos.z += random->RandomFloat(-12,12); |
|
g_pEffects->Sparks(sparkPos); |
|
} |
|
|
|
// Light |
|
CBroadcastRecipientFilter filter; |
|
te->DynamicLight( filter, 0.0, &WorldSpaceCenter(), 255, 180, 100, 0, 100, 0.1, 0 ); |
|
|
|
// Cover the gib spawn |
|
ExplosionCreate( WorldSpaceCenter(), GetAbsAngles(), this, 64, 64, false ); |
|
|
|
// Turn off any smoke trail |
|
if ( m_pSmokeTrail ) |
|
{ |
|
m_pSmokeTrail->m_ParticleLifetime = 0; |
|
UTIL_Remove(m_pSmokeTrail); |
|
m_pSmokeTrail = NULL; |
|
} |
|
|
|
// FIXME: This is because we couldn't save/load the CTakeDamageInfo. |
|
// because it's midnight before the teamwide playtest. Real solution |
|
// is to add a datadesc to CTakeDamageInfo |
|
if ( m_KilledInfo.GetInflictor() ) |
|
{ |
|
BaseClass::Event_Killed( m_KilledInfo ); |
|
} |
|
|
|
UTIL_Remove(this); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *pPhysGunUser - |
|
// bPunting - |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) |
|
{ |
|
m_hPhysicsAttacker = pPhysGunUser; |
|
m_flLastPhysicsInfluenceTime = gpGlobals->curtime; |
|
|
|
if ( reason == PUNTED_BY_CANNON ) |
|
{ |
|
// There's about to be a massive change in velocity. |
|
// Think immediately to handle changes in m_vCurrentVelocity; |
|
SetNextThink( gpGlobals->curtime + 0.01f ); |
|
|
|
m_flEngineStallTime = gpGlobals->curtime + 2.0f; |
|
ScannerEmitSound( "DiveBomb" ); |
|
} |
|
else |
|
{ |
|
SetCondition( COND_SCANNER_GRABBED_BY_PHYSCANNON ); |
|
ClearCondition( COND_SCANNER_RELEASED_FROM_PHYSCANNON ); |
|
} |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *pPhysGunUser - |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) |
|
{ |
|
m_hPhysicsAttacker = pPhysGunUser; |
|
m_flLastPhysicsInfluenceTime = gpGlobals->curtime; |
|
|
|
ClearCondition( COND_SCANNER_GRABBED_BY_PHYSCANNON ); |
|
SetCondition( COND_SCANNER_RELEASED_FROM_PHYSCANNON ); |
|
|
|
if ( Reason == LAUNCHED_BY_CANNON ) |
|
{ |
|
m_flEngineStallTime = gpGlobals->curtime + 2.0f; |
|
|
|
// There's about to be a massive change in velocity. |
|
// Think immediately to handle changes in m_vCurrentVelocity; |
|
SetNextThink( gpGlobals->curtime + 0.01f ); |
|
ScannerEmitSound( "DiveBomb" ); |
|
} |
|
} |
|
|
|
|
|
//------------------------------------------------------------------------------ |
|
// Do we have a physics attacker? |
|
//------------------------------------------------------------------------------ |
|
CBasePlayer *CNPC_BaseScanner::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: |
|
//------------------------------------------------------------------------------ |
|
void CNPC_BaseScanner::StopLoopingSounds(void) |
|
{ |
|
CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); |
|
controller.SoundDestroy( m_pEngineSound ); |
|
m_pEngineSound = NULL; |
|
|
|
BaseClass::StopLoopingSounds(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : pInflictor - |
|
// pAttacker - |
|
// flDamage - |
|
// bitsDamageType - |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::Event_Killed( const CTakeDamageInfo &info ) |
|
{ |
|
// Copy off the takedamage info that killed me, since we're not going to call |
|
// up into the base class's Event_Killed() until we gib. (gibbing is ultimate death) |
|
m_KilledInfo = info; |
|
|
|
// Interrupt whatever schedule I'm on |
|
SetCondition(COND_SCHEDULE_DONE); |
|
|
|
// If I have an enemy and I'm up high, do a dive bomb (unless dissolved) |
|
if ( GetEnemy() != NULL && (info.GetDamageType() & DMG_DISSOLVE) == false ) |
|
{ |
|
Vector vecDelta = GetLocalOrigin() - GetEnemy()->GetLocalOrigin(); |
|
if ( ( vecDelta.z > 120 ) && ( vecDelta.Length() > 360 ) ) |
|
{ |
|
// If I'm divebombing, don't take any more damage. It will make Event_Killed() be called again. |
|
// This is especially bad if someone machineguns the divebombing scanner. |
|
AttackDivebomb(); |
|
return; |
|
} |
|
} |
|
|
|
Gib(); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose: |
|
//------------------------------------------------------------------------------ |
|
void CNPC_BaseScanner::AttackDivebomb( void ) |
|
{ |
|
ScannerEmitSound( "DiveBomb" ); |
|
|
|
m_takedamage = DAMAGE_NO; |
|
|
|
StartSmokeTrail(); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose: Checks to see if we hit anything while dive bombing. |
|
//------------------------------------------------------------------------------ |
|
void CNPC_BaseScanner::AttackDivebombCollide(float flInterval) |
|
{ |
|
// |
|
// Trace forward to see if I hit anything |
|
// |
|
Vector checkPos = GetAbsOrigin() + (GetCurrentVelocity() * flInterval); |
|
trace_t tr; |
|
CBaseEntity* pHitEntity = NULL; |
|
AI_TraceHull( GetAbsOrigin(), checkPos, GetHullMins(), GetHullMaxs(), MASK_SOLID, this, COLLISION_GROUP_NONE, &tr ); |
|
|
|
if (tr.m_pEnt) |
|
{ |
|
pHitEntity = tr.m_pEnt; |
|
|
|
// Did I hit an entity that isn't another scanner? |
|
if (pHitEntity && pHitEntity->Classify()!=CLASS_SCANNER) |
|
{ |
|
if ( !pHitEntity->ClassMatches("item_battery") ) |
|
{ |
|
if ( !pHitEntity->IsWorld() ) |
|
{ |
|
CTakeDamageInfo info( this, this, sk_scanner_dmg_dive.GetFloat(), DMG_CLUB ); |
|
CalculateMeleeDamageForce( &info, (tr.endpos - tr.startpos), tr.endpos ); |
|
pHitEntity->TakeDamage( info ); |
|
} |
|
Gib(); |
|
} |
|
} |
|
} |
|
|
|
if (tr.fraction != 1.0) |
|
{ |
|
// We've hit something so deflect our velocity based on the surface |
|
// norm of what we've hit |
|
if (flInterval > 0) |
|
{ |
|
float moveLen = (1.0 - tr.fraction)*(GetAbsOrigin() - checkPos).Length(); |
|
Vector vBounceVel = moveLen*tr.plane.normal/flInterval; |
|
|
|
// If I'm right over the ground don't push down |
|
if (vBounceVel.z < 0) |
|
{ |
|
float floorZ = GetFloorZ(GetAbsOrigin()); |
|
if (abs(GetAbsOrigin().z - floorZ) < 36) |
|
{ |
|
vBounceVel.z = 0; |
|
} |
|
} |
|
SetCurrentVelocity( GetCurrentVelocity() + vBounceVel ); |
|
} |
|
CBaseCombatCharacter* pBCC = ToBaseCombatCharacter( pHitEntity ); |
|
|
|
if (pBCC) |
|
{ |
|
// Spawn some extra blood where we hit |
|
SpawnBlood(tr.endpos, g_vecAttackDir, pBCC->BloodColor(), sk_scanner_dmg_dive.GetFloat()); |
|
} |
|
else |
|
{ |
|
if (!(m_spawnflags & SF_NPC_GAG)) |
|
{ |
|
// <<TEMP>> need better sound here... |
|
ScannerEmitSound( "Shoot" ); |
|
} |
|
// For sparks we must trace a line in the direction of the surface norm |
|
// that we hit. |
|
checkPos = GetAbsOrigin() - (tr.plane.normal * 24); |
|
|
|
AI_TraceLine( GetAbsOrigin(), checkPos,MASK_SOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); |
|
if (tr.fraction != 1.0) |
|
{ |
|
g_pEffects->Sparks( tr.endpos ); |
|
|
|
CBroadcastRecipientFilter filter; |
|
te->DynamicLight( filter, 0.0, |
|
&GetAbsOrigin(), 255, 180, 100, 0, 50, 0.1, 0 ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::PlayFlySound(void) |
|
{ |
|
if ( IsMarkedForDeletion() ) |
|
return; |
|
|
|
CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); |
|
|
|
//Setup the sound if we're not already |
|
if ( m_pEngineSound == NULL ) |
|
{ |
|
// Create the sound |
|
CPASAttenuationFilter filter( this ); |
|
|
|
m_pEngineSound = controller.SoundCreate( filter, entindex(), CHAN_STATIC, GetEngineSound(), ATTN_NORM ); |
|
|
|
Assert(m_pEngineSound); |
|
|
|
// Start the engine sound |
|
controller.Play( m_pEngineSound, 0.0f, 100.0f ); |
|
controller.SoundChangeVolume( m_pEngineSound, 1.0f, 2.0f ); |
|
} |
|
|
|
float speed = GetCurrentVelocity().Length(); |
|
float flVolume = 0.25f + (0.75f*(speed/GetMaxSpeed())); |
|
int iPitch = MIN( 255, 80 + (20*(speed/GetMaxSpeed())) ); |
|
|
|
//Update our pitch and volume based on our speed |
|
controller.SoundChangePitch( m_pEngineSound, iPitch, 0.1f ); |
|
controller.SoundChangeVolume( m_pEngineSound, flVolume, 0.1f ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::ScannerEmitSound( const char *pszSoundName ) |
|
{ |
|
CFmtStr snd; |
|
snd.sprintf("%s.%s", GetScannerSoundPrefix(), pszSoundName ); |
|
|
|
m_bHasSpoken = true; |
|
|
|
EmitSound( snd.Access() ); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose: |
|
//------------------------------------------------------------------------------ |
|
void CNPC_BaseScanner::SpeakSentence( int sentenceType ) |
|
{ |
|
if (sentenceType == SCANNER_SENTENCE_ATTENTION) |
|
{ |
|
ScannerEmitSound( "Attention" ); |
|
} |
|
else if (sentenceType == SCANNER_SENTENCE_HANDSUP) |
|
{ |
|
ScannerEmitSound( "Scan" ); |
|
} |
|
else if (sentenceType == SCANNER_SENTENCE_PROCEED) |
|
{ |
|
ScannerEmitSound( "Proceed" ); |
|
} |
|
else if (sentenceType == SCANNER_SENTENCE_CURIOUS) |
|
{ |
|
ScannerEmitSound( "Curious" ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::InputSetFlightSpeed(inputdata_t &inputdata) |
|
{ |
|
//FIXME: Currently unsupported |
|
|
|
/* |
|
m_flFlightSpeed = inputdata.value.Int(); |
|
m_bFlightSpeedOverridden = (m_flFlightSpeed > 0); |
|
*/ |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::StartSmokeTrail( void ) |
|
{ |
|
if ( m_pSmokeTrail != NULL ) |
|
return; |
|
|
|
m_pSmokeTrail = SmokeTrail::CreateSmokeTrail(); |
|
|
|
if ( m_pSmokeTrail ) |
|
{ |
|
m_pSmokeTrail->m_SpawnRate = 10; |
|
m_pSmokeTrail->m_ParticleLifetime = 1; |
|
m_pSmokeTrail->m_StartSize = 8; |
|
m_pSmokeTrail->m_EndSize = 50; |
|
m_pSmokeTrail->m_SpawnRadius = 10; |
|
m_pSmokeTrail->m_MinSpeed = 15; |
|
m_pSmokeTrail->m_MaxSpeed = 25; |
|
|
|
m_pSmokeTrail->m_StartColor.Init( 0.5f, 0.5f, 0.5f ); |
|
m_pSmokeTrail->m_EndColor.Init( 0, 0, 0 ); |
|
m_pSmokeTrail->SetLifetime( 500.0f ); |
|
m_pSmokeTrail->FollowEntity( this ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::BlendPhyscannonLaunchSpeed() |
|
{ |
|
// Blend out desired velocity when launched by the physcannon |
|
if (!VPhysicsGetObject()) |
|
return; |
|
|
|
if ( HasPhysicsAttacker( SCANNER_SMASH_TIME ) && !IsHeldByPhyscannon( ) ) |
|
{ |
|
Vector vecCurrentVelocity; |
|
VPhysicsGetObject()->GetVelocity( &vecCurrentVelocity, NULL ); |
|
float flLerpFactor = (gpGlobals->curtime - m_flLastPhysicsInfluenceTime) / SCANNER_SMASH_TIME; |
|
flLerpFactor = clamp( flLerpFactor, 0.0f, 1.0f ); |
|
flLerpFactor = SimpleSplineRemapVal( flLerpFactor, 0.0f, 1.0f, 0.0f, 1.0f ); |
|
flLerpFactor *= flLerpFactor; |
|
VectorLerp( vecCurrentVelocity, m_vCurrentVelocity, flLerpFactor, m_vCurrentVelocity ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::MoveExecute_Alive(float flInterval) |
|
{ |
|
// Amount of noise to add to flying |
|
float noiseScale = 3.0f; |
|
|
|
// ------------------------------------------- |
|
// Avoid obstacles, unless I'm dive bombing |
|
// ------------------------------------------- |
|
if (m_nFlyMode != SCANNER_FLY_DIVE) |
|
{ |
|
SetCurrentVelocity( GetCurrentVelocity() + VelocityToAvoidObstacles(flInterval) ); |
|
} |
|
// If I am dive bombing add more noise to my flying |
|
else |
|
{ |
|
AttackDivebombCollide(flInterval); |
|
noiseScale *= 4; |
|
} |
|
|
|
IPhysicsObject *pPhysics = VPhysicsGetObject(); |
|
|
|
if ( pPhysics && pPhysics->IsAsleep() ) |
|
{ |
|
pPhysics->Wake(); |
|
} |
|
|
|
// Add time-coherent noise to the current velocity so that it never looks bolted in place. |
|
AddNoiseToVelocity( noiseScale ); |
|
|
|
AdjustScannerVelocity(); |
|
|
|
float maxSpeed = GetEnemy() ? ( GetMaxSpeed() * 2.0f ) : GetMaxSpeed(); |
|
if ( m_nFlyMode == SCANNER_FLY_DIVE ) |
|
{ |
|
maxSpeed = -1; |
|
} |
|
|
|
// Limit fall speed |
|
LimitSpeed( maxSpeed ); |
|
|
|
// Blend out desired velocity when launched by the physcannon |
|
BlendPhyscannonLaunchSpeed(); |
|
|
|
// Update what we're looking at |
|
UpdateHead( flInterval ); |
|
|
|
// Control the tail based on our vertical travel |
|
float tailPerc = clamp( GetCurrentVelocity().z, -150, 250 ); |
|
tailPerc = SimpleSplineRemapVal( tailPerc, -150, 250, -25, 80 ); |
|
|
|
SetPoseParameter( m_nPoseTail, tailPerc ); |
|
|
|
// Spin the dynamo based upon our speed |
|
float flCurrentDynamo = GetPoseParameter( m_nPoseDynamo ); |
|
float speed = GetCurrentVelocity().Length(); |
|
float flDynamoSpeed = (maxSpeed > 0 ? speed / maxSpeed : 1.0) * 60; |
|
flCurrentDynamo -= flDynamoSpeed; |
|
if ( flCurrentDynamo < -180.0 ) |
|
{ |
|
flCurrentDynamo += 360.0; |
|
} |
|
SetPoseParameter( m_nPoseDynamo, flCurrentDynamo ); |
|
|
|
PlayFlySound(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Handles movement towards the last move target. |
|
// Input : flInterval - |
|
//----------------------------------------------------------------------------- |
|
bool CNPC_BaseScanner::OverridePathMove( CBaseEntity *pMoveTarget, float flInterval ) |
|
{ |
|
// Save our last patrolling direction |
|
Vector lastPatrolDir = GetNavigator()->GetCurWaypointPos() - GetAbsOrigin(); |
|
|
|
// Continue on our path |
|
if ( ProgressFlyPath( flInterval, pMoveTarget, (MASK_NPCSOLID|CONTENTS_WATER), false, 64 ) == AINPP_COMPLETE ) |
|
{ |
|
if ( IsCurSchedule( SCHED_SCANNER_PATROL ) ) |
|
{ |
|
m_vLastPatrolDir = lastPatrolDir; |
|
VectorNormalize(m_vLastPatrolDir); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : flInterval - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CNPC_BaseScanner::OverrideMove( float flInterval ) |
|
{ |
|
// ---------------------------------------------- |
|
// If dive bombing |
|
// ---------------------------------------------- |
|
if (m_nFlyMode == SCANNER_FLY_DIVE) |
|
{ |
|
MoveToDivebomb( flInterval ); |
|
} |
|
else |
|
{ |
|
Vector vMoveTargetPos(0,0,0); |
|
CBaseEntity *pMoveTarget = NULL; |
|
|
|
// The original line of code was, due to the accidental use of '|' instead of |
|
// '&', always true. Replacing with 'true' to suppress the warning without changing |
|
// the (long-standing) behavior. |
|
if ( true ) //!GetNavigator()->IsGoalActive() || ( GetNavigator()->GetCurWaypointFlags() | bits_WP_TO_PATHCORNER ) ) |
|
{ |
|
// Select move target |
|
if ( GetTarget() != NULL ) |
|
{ |
|
pMoveTarget = GetTarget(); |
|
} |
|
else if ( GetEnemy() != NULL ) |
|
{ |
|
pMoveTarget = GetEnemy(); |
|
} |
|
|
|
// Select move target position |
|
if ( GetEnemy() != NULL ) |
|
{ |
|
vMoveTargetPos = GetEnemy()->GetAbsOrigin(); |
|
} |
|
} |
|
else |
|
{ |
|
vMoveTargetPos = GetNavigator()->GetCurWaypointPos(); |
|
} |
|
|
|
ClearCondition( COND_SCANNER_FLY_CLEAR ); |
|
ClearCondition( COND_SCANNER_FLY_BLOCKED ); |
|
|
|
// See if we can fly there directly |
|
if ( pMoveTarget ) |
|
{ |
|
trace_t tr; |
|
AI_TraceHull( GetAbsOrigin(), vMoveTargetPos, GetHullMins(), GetHullMaxs(), MASK_NPCSOLID_BRUSHONLY, this, COLLISION_GROUP_NONE, &tr ); |
|
|
|
float fTargetDist = (1.0f-tr.fraction)*(GetAbsOrigin() - vMoveTargetPos).Length(); |
|
|
|
if ( ( tr.m_pEnt == pMoveTarget ) || ( fTargetDist < 50 ) ) |
|
{ |
|
if ( g_debug_basescanner.GetBool() ) |
|
{ |
|
NDebugOverlay::Line(GetLocalOrigin(), vMoveTargetPos, 0,255,0, true, 0); |
|
NDebugOverlay::Cross3D(tr.endpos,Vector(-5,-5,-5),Vector(5,5,5),0,255,0,true,0.1); |
|
} |
|
|
|
SetCondition( COND_SCANNER_FLY_CLEAR ); |
|
} |
|
else |
|
{ |
|
//HANDY DEBUG TOOL |
|
if ( g_debug_basescanner.GetBool() ) |
|
{ |
|
NDebugOverlay::Line(GetLocalOrigin(), vMoveTargetPos, 255,0,0, true, 0); |
|
NDebugOverlay::Cross3D(tr.endpos,Vector(-5,-5,-5),Vector(5,5,5),255,0,0,true,0.1); |
|
} |
|
|
|
SetCondition( COND_SCANNER_FLY_BLOCKED ); |
|
} |
|
} |
|
|
|
// If I have a route, keep it updated and move toward target |
|
if ( GetNavigator()->IsGoalActive() ) |
|
{ |
|
if ( OverridePathMove( pMoveTarget, flInterval ) ) |
|
{ |
|
BlendPhyscannonLaunchSpeed(); |
|
return true; |
|
} |
|
} |
|
// ---------------------------------------------- |
|
// If attacking |
|
// ---------------------------------------------- |
|
else if (m_nFlyMode == SCANNER_FLY_ATTACK) |
|
{ |
|
MoveToAttack( flInterval ); |
|
} |
|
// ----------------------------------------------------------------- |
|
// If I don't have a route, just decelerate |
|
// ----------------------------------------------------------------- |
|
else if (!GetNavigator()->IsGoalActive()) |
|
{ |
|
float myDecay = 9.5; |
|
Decelerate( flInterval, myDecay); |
|
} |
|
} |
|
|
|
MoveExecute_Alive( flInterval ); |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &goalPos - |
|
// &startPos - |
|
// idealRange - |
|
// idealHeight - |
|
// Output : Vector |
|
//----------------------------------------------------------------------------- |
|
Vector CNPC_BaseScanner::IdealGoalForMovement( const Vector &goalPos, const Vector &startPos, float idealRange, float idealHeightDiff ) |
|
{ |
|
Vector vMoveDir; |
|
|
|
if ( GetGoalDirection( &vMoveDir ) == false ) |
|
{ |
|
vMoveDir = ( goalPos - startPos ); |
|
vMoveDir.z = 0; |
|
VectorNormalize( vMoveDir ); |
|
} |
|
|
|
// Move up from the position by the desired amount |
|
Vector vIdealPos = goalPos + Vector( 0, 0, idealHeightDiff ) + ( vMoveDir * -idealRange ); |
|
|
|
// Trace down and make sure we can fit here |
|
trace_t tr; |
|
AI_TraceHull( vIdealPos, vIdealPos - Vector( 0, 0, MinGroundDist() ), GetHullMins(), GetHullMaxs(), MASK_NPCSOLID, this, COLLISION_GROUP_NONE, &tr ); |
|
|
|
// Move up otherwise |
|
if ( tr.fraction < 1.0f ) |
|
{ |
|
vIdealPos.z += ( MinGroundDist() * ( 1.0f - tr.fraction ) ); |
|
} |
|
|
|
//FIXME: We also need to make sure that we fit here at all, and if not, chose a new spot |
|
|
|
// Debug tools |
|
if ( g_debug_basescanner.GetBool() ) |
|
{ |
|
NDebugOverlay::Cross3D( goalPos, -Vector(8,8,8), Vector(8,8,8), 255, 255, 0, true, 0.1f ); |
|
NDebugOverlay::Cross3D( startPos, -Vector(8,8,8), Vector(8,8,8), 255, 0, 255, true, 0.1f ); |
|
NDebugOverlay::Cross3D( vIdealPos, -Vector(8,8,8), Vector(8,8,8), 255, 255, 255, true, 0.1f ); |
|
NDebugOverlay::Line( startPos, goalPos, 0, 255, 0, true, 0.1f ); |
|
|
|
NDebugOverlay::Cross3D( goalPos + ( vMoveDir * -idealRange ), -Vector(8,8,8), Vector(8,8,8), 255, 255, 255, true, 0.1f ); |
|
NDebugOverlay::Line( goalPos, goalPos + ( vMoveDir * -idealRange ), 255, 255, 0, true, 0.1f ); |
|
NDebugOverlay::Line( goalPos + ( vMoveDir * -idealRange ), vIdealPos, 255, 255, 0, true, 0.1f ); |
|
} |
|
|
|
return vIdealPos; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : flInterval - |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::MoveToAttack(float flInterval) |
|
{ |
|
if (GetEnemy() == NULL) |
|
return; |
|
|
|
if ( flInterval <= 0 ) |
|
return; |
|
|
|
Vector vTargetPos = GetEnemyLKP(); |
|
|
|
//float flDesiredDist = m_flAttackNearDist + ( ( m_flAttackFarDist - m_flAttackNearDist ) / 2 ); |
|
|
|
Vector idealPos = IdealGoalForMovement( vTargetPos, GetAbsOrigin(), GetGoalDistance(), m_flAttackNearDist ); |
|
|
|
MoveToTarget( flInterval, idealPos ); |
|
|
|
//FIXME: Re-implement? |
|
|
|
/* |
|
// --------------------------------------------------------- |
|
// Add evasion if I have taken damage recently |
|
// --------------------------------------------------------- |
|
if ((m_flLastDamageTime + SCANNER_EVADE_TIME) > gpGlobals->curtime) |
|
{ |
|
vFlyDirection = vFlyDirection + VelocityToEvade(GetEnemyCombatCharacterPointer()); |
|
} |
|
*/ |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Accelerates toward a given position. |
|
// Input : flInterval - Time interval over which to move. |
|
// vecMoveTarget - Position to move toward. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::MoveToTarget( float flInterval, const Vector &vecMoveTarget ) |
|
{ |
|
// Don't move if stalling |
|
if ( m_flEngineStallTime > gpGlobals->curtime ) |
|
return; |
|
|
|
// Look at our inspection target if we have one |
|
if ( GetEnemy() != NULL ) |
|
{ |
|
// Otherwise at our enemy |
|
TurnHeadToTarget( flInterval, GetEnemy()->EyePosition() ); |
|
} |
|
else |
|
{ |
|
// Otherwise face our motion direction |
|
TurnHeadToTarget( flInterval, vecMoveTarget ); |
|
} |
|
|
|
// ------------------------------------- |
|
// Move towards our target |
|
// ------------------------------------- |
|
float myAccel; |
|
float myZAccel = 400.0f; |
|
float myDecay = 0.15f; |
|
|
|
Vector vecCurrentDir; |
|
|
|
// Get the relationship between my current velocity and the way I want to be going. |
|
vecCurrentDir = GetCurrentVelocity(); |
|
VectorNormalize( vecCurrentDir ); |
|
|
|
Vector targetDir = vecMoveTarget - GetAbsOrigin(); |
|
float flDist = VectorNormalize(targetDir); |
|
|
|
float flDot; |
|
flDot = DotProduct( targetDir, vecCurrentDir ); |
|
|
|
if( flDot > 0.25 ) |
|
{ |
|
// If my target is in front of me, my flight model is a bit more accurate. |
|
myAccel = 250; |
|
} |
|
else |
|
{ |
|
// Have a harder time correcting my course if I'm currently flying away from my target. |
|
myAccel = 128; |
|
} |
|
|
|
if ( myAccel > flDist / flInterval ) |
|
{ |
|
myAccel = flDist / flInterval; |
|
} |
|
|
|
if ( myZAccel > flDist / flInterval ) |
|
{ |
|
myZAccel = flDist / flInterval; |
|
} |
|
|
|
MoveInDirection( flInterval, targetDir, myAccel, myZAccel, myDecay ); |
|
|
|
// calc relative banking targets |
|
Vector forward, right, up; |
|
GetVectors( &forward, &right, &up ); |
|
|
|
m_vCurrentBanking.x = targetDir.x; |
|
m_vCurrentBanking.z = 120.0f * DotProduct( right, targetDir ); |
|
m_vCurrentBanking.y = 0; |
|
|
|
float speedPerc = SimpleSplineRemapVal( GetCurrentVelocity().Length(), 0.0f, GetMaxSpeed(), 0.0f, 1.0f ); |
|
|
|
speedPerc = clamp( speedPerc, 0.0f, 1.0f ); |
|
|
|
m_vCurrentBanking *= speedPerc; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Danger sounds. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::DiveBombSoundThink() |
|
{ |
|
Vector vecPosition, vecVelocity; |
|
IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); |
|
|
|
if ( pPhysicsObject == NULL ) |
|
return; |
|
|
|
pPhysicsObject->GetPosition( &vecPosition, NULL ); |
|
pPhysicsObject->GetVelocity( &vecVelocity, NULL ); |
|
|
|
CBasePlayer *pPlayer = AI_GetSinglePlayer(); |
|
if ( pPlayer ) |
|
{ |
|
Vector vecDelta; |
|
VectorSubtract( pPlayer->GetAbsOrigin(), vecPosition, vecDelta ); |
|
VectorNormalize( vecDelta ); |
|
if ( DotProduct( vecDelta, vecVelocity ) > 0.5f ) |
|
{ |
|
Vector vecEndPoint; |
|
VectorMA( vecPosition, 2.0f * TICK_INTERVAL, vecVelocity, vecEndPoint ); |
|
float flDist = CalcDistanceToLineSegment( pPlayer->GetAbsOrigin(), vecPosition, vecEndPoint ); |
|
if ( flDist < 200.0f ) |
|
{ |
|
ScannerEmitSound( "DiveBombFlyby" ); |
|
SetContextThink( &CNPC_BaseScanner::DiveBombSoundThink, gpGlobals->curtime + 0.5f, s_pDiveBombSoundThinkContext ); |
|
return; |
|
} |
|
} |
|
} |
|
|
|
SetContextThink( &CNPC_BaseScanner::DiveBombSoundThink, gpGlobals->curtime + 2.0f * TICK_INTERVAL, s_pDiveBombSoundThinkContext ); |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : flInterval - |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::MoveToDivebomb(float flInterval) |
|
{ |
|
float myAccel = 1600; |
|
float myDecay = 0.05f; // decay current velocity to 10% in 1 second |
|
|
|
// Fly towards my enemy |
|
Vector vEnemyPos = GetEnemyLKP(); |
|
Vector vFlyDirection = vEnemyPos - GetLocalOrigin(); |
|
VectorNormalize( vFlyDirection ); |
|
|
|
// Set net velocity |
|
MoveInDirection( flInterval, m_vecDiveBombDirection, myAccel, myAccel, myDecay); |
|
|
|
// Spin out of control. |
|
Vector forward; |
|
VPhysicsGetObject()->LocalToWorldVector( &forward, Vector( 1.0, 0.0, 0.0 ) ); |
|
AngularImpulse torque = forward * m_flDiveBombRollForce; |
|
VPhysicsGetObject()->ApplyTorqueCenter( torque ); |
|
|
|
// BUGBUG: why Y axis and not Z? |
|
Vector up; |
|
VPhysicsGetObject()->LocalToWorldVector( &up, Vector( 0.0, 1.0, 0.0 ) ); |
|
VPhysicsGetObject()->ApplyForceCenter( up * 2000 ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
bool CNPC_BaseScanner::IsEnemyPlayerInSuit() |
|
{ |
|
if( GetEnemy() && GetEnemy()->IsPlayer() ) |
|
{ |
|
CHL2_Player *pPlayer = NULL; |
|
pPlayer = (CHL2_Player *)GetEnemy(); |
|
|
|
if( pPlayer && pPlayer->IsSuitEquipped() ) |
|
{ |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : float |
|
//----------------------------------------------------------------------------- |
|
float CNPC_BaseScanner::GetGoalDistance( void ) |
|
{ |
|
if ( m_flGoalOverrideDistance != 0.0f ) |
|
return m_flGoalOverrideDistance; |
|
|
|
switch ( m_nFlyMode ) |
|
{ |
|
case SCANNER_FLY_ATTACK: |
|
{ |
|
float goalDist = ( m_flAttackNearDist + ( ( m_flAttackFarDist - m_flAttackNearDist ) / 2 ) ); |
|
if( IsEnemyPlayerInSuit() ) |
|
{ |
|
goalDist *= 0.5; |
|
} |
|
return goalDist; |
|
} |
|
break; |
|
} |
|
|
|
return 128.0f; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &vOut - |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CNPC_BaseScanner::GetGoalDirection( Vector *vOut ) |
|
{ |
|
CBaseEntity *pTarget = GetTarget(); |
|
|
|
if ( pTarget == NULL ) |
|
return false; |
|
|
|
if ( FClassnameIs( pTarget, "info_hint_air" ) || FClassnameIs( pTarget, "info_target" ) ) |
|
{ |
|
AngleVectors( pTarget->GetAbsAngles(), vOut ); |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
Vector CNPC_BaseScanner::VelocityToEvade(CBaseCombatCharacter *pEnemy) |
|
{ |
|
if (pEnemy) |
|
{ |
|
// ----------------------------------------- |
|
// Keep out of enemy's shooting position |
|
// ----------------------------------------- |
|
Vector vEnemyFacing = pEnemy->BodyDirection2D( ); |
|
Vector vEnemyDir = pEnemy->EyePosition() - GetLocalOrigin(); |
|
VectorNormalize(vEnemyDir); |
|
float fDotPr = DotProduct(vEnemyFacing,vEnemyDir); |
|
|
|
if (fDotPr < -0.9) |
|
{ |
|
Vector vDirUp(0,0,1); |
|
Vector vDir; |
|
CrossProduct( vEnemyFacing, vDirUp, vDir); |
|
|
|
Vector crossProduct; |
|
CrossProduct(vEnemyFacing, vEnemyDir, crossProduct); |
|
if (crossProduct.y < 0) |
|
{ |
|
vDir = vDir * -1; |
|
} |
|
return (vDir); |
|
} |
|
else if (fDotPr < -0.85) |
|
{ |
|
Vector vDirUp(0,0,1); |
|
Vector vDir; |
|
CrossProduct( vEnemyFacing, vDirUp, vDir); |
|
|
|
Vector crossProduct; |
|
CrossProduct(vEnemyFacing, vEnemyDir, crossProduct); |
|
if (random->RandomInt(0,1)) |
|
{ |
|
vDir = vDir * -1; |
|
} |
|
return (vDir); |
|
} |
|
} |
|
return vec3_origin; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
int CNPC_BaseScanner::DrawDebugTextOverlays(void) |
|
{ |
|
int nOffset = BaseClass::DrawDebugTextOverlays(); |
|
|
|
if ( m_debugOverlays & OVERLAY_TEXT_BIT ) |
|
{ |
|
Vector vel; |
|
GetVelocity( &vel, NULL ); |
|
|
|
char tempstr[512]; |
|
Q_snprintf( tempstr, sizeof(tempstr), "speed (max): %.2f (%.2f)", vel.Length(), m_flSpeed ); |
|
EntityText( nOffset, tempstr, 0 ); |
|
nOffset++; |
|
} |
|
|
|
return nOffset; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : float |
|
//----------------------------------------------------------------------------- |
|
float CNPC_BaseScanner::GetHeadTurnRate( void ) |
|
{ |
|
if ( GetEnemy() ) |
|
return 800.0f; |
|
|
|
return 350.0f; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
inline CBaseEntity *CNPC_BaseScanner::EntityToWatch( void ) |
|
{ |
|
return ( GetTarget() != NULL ) ? GetTarget() : GetEnemy(); // Okay if NULL |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : flInterval - |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::UpdateHead( float flInterval ) |
|
{ |
|
float yaw = GetPoseParameter( m_nPoseFaceHoriz ); |
|
float pitch = GetPoseParameter( m_nPoseFaceVert ); |
|
|
|
CBaseEntity *pTarget = EntityToWatch(); |
|
|
|
Vector vLookPos; |
|
|
|
if ( !HasCondition( COND_IN_PVS ) || GetAttachment( "eyes", vLookPos ) == false ) |
|
{ |
|
vLookPos = EyePosition(); |
|
} |
|
|
|
if ( pTarget != NULL ) |
|
{ |
|
Vector lookDir = pTarget->EyePosition() - vLookPos; |
|
VectorNormalize( lookDir ); |
|
|
|
if ( DotProduct( lookDir, BodyDirection3D() ) < 0.0f ) |
|
{ |
|
SetPoseParameter( m_nPoseFaceHoriz, UTIL_Approach( 0, yaw, 10 ) ); |
|
SetPoseParameter( m_nPoseFaceVert, UTIL_Approach( 0, pitch, 10 ) ); |
|
|
|
return; |
|
} |
|
|
|
float facingYaw = VecToYaw( BodyDirection3D() ); |
|
float yawDiff = VecToYaw( lookDir ); |
|
yawDiff = UTIL_AngleDiff( yawDiff, facingYaw + yaw ); |
|
|
|
float facingPitch = UTIL_VecToPitch( BodyDirection3D() ); |
|
float pitchDiff = UTIL_VecToPitch( lookDir ); |
|
pitchDiff = UTIL_AngleDiff( pitchDiff, facingPitch + pitch ); |
|
|
|
SetPoseParameter( m_nPoseFaceHoriz, UTIL_Approach( yaw + yawDiff, yaw, 50 ) ); |
|
SetPoseParameter( m_nPoseFaceVert, UTIL_Approach( pitch + pitchDiff, pitch, 50 ) ); |
|
} |
|
else |
|
{ |
|
SetPoseParameter( m_nPoseFaceHoriz, UTIL_Approach( 0, yaw, 10 ) ); |
|
SetPoseParameter( m_nPoseFaceVert, UTIL_Approach( 0, pitch, 10 ) ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &linear - |
|
// &angular - |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::ClampMotorForces( Vector &linear, AngularImpulse &angular ) |
|
{ |
|
// limit reaction forces |
|
if ( m_nFlyMode != SCANNER_FLY_DIVE ) |
|
{ |
|
linear.x = clamp( linear.x, -500, 500 ); |
|
linear.y = clamp( linear.y, -500, 500 ); |
|
linear.z = clamp( linear.z, -500, 500 ); |
|
} |
|
|
|
// If we're dive bombing, we need to drop faster than normal |
|
if ( m_nFlyMode != SCANNER_FLY_DIVE ) |
|
{ |
|
// Add in weightlessness |
|
linear.z += 800; |
|
} |
|
|
|
angular.z = clamp( angular.z, -GetHeadTurnRate(), GetHeadTurnRate() ); |
|
if ( m_nFlyMode == SCANNER_FLY_DIVE ) |
|
{ |
|
// Disable pitch and roll motors while crashing. |
|
angular.x = 0; |
|
angular.y = 0; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : &inputdata - |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::InputSetDistanceOverride( inputdata_t &inputdata ) |
|
{ |
|
m_flGoalOverrideDistance = inputdata.value.Float(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Emit sounds specific to the NPC's state. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::AlertSound(void) |
|
{ |
|
ScannerEmitSound( "Alert" ); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Purpose: |
|
//------------------------------------------------------------------------------ |
|
void CNPC_BaseScanner::DeathSound( const CTakeDamageInfo &info ) |
|
{ |
|
ScannerEmitSound( "Die" ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Overridden so that scanners play battle sounds while fighting. |
|
// Output : Returns TRUE on success, FALSE on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CNPC_BaseScanner::ShouldPlayIdleSound( void ) |
|
{ |
|
if ( HasSpawnFlags( SF_NPC_GAG ) ) |
|
return false; |
|
|
|
if ( random->RandomInt( 0, 25 ) != 0 ) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Plays sounds while idle or in combat. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::IdleSound(void) |
|
{ |
|
if ( m_NPCState == NPC_STATE_COMBAT ) |
|
{ |
|
// dvs: the combat sounds should be related to what is happening, rather than random |
|
ScannerEmitSound( "Combat" ); |
|
} |
|
else |
|
{ |
|
ScannerEmitSound( "Idle" ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Plays a sound when hurt. |
|
//----------------------------------------------------------------------------- |
|
void CNPC_BaseScanner::PainSound( const CTakeDamageInfo &info ) |
|
{ |
|
ScannerEmitSound( "Pain" ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
float CNPC_BaseScanner::GetMaxSpeed() |
|
{ |
|
return SCANNER_MAX_SPEED; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
// Schedules |
|
// |
|
//----------------------------------------------------------------------------- |
|
|
|
AI_BEGIN_CUSTOM_NPC( npc_basescanner, CNPC_BaseScanner ) |
|
|
|
DECLARE_TASK( TASK_SCANNER_SET_FLY_PATROL ) |
|
DECLARE_TASK( TASK_SCANNER_SET_FLY_CHASE ) |
|
DECLARE_TASK( TASK_SCANNER_SET_FLY_ATTACK ) |
|
DECLARE_TASK( TASK_SCANNER_SET_FLY_DIVE ) |
|
|
|
DECLARE_CONDITION(COND_SCANNER_FLY_CLEAR) |
|
DECLARE_CONDITION(COND_SCANNER_FLY_BLOCKED) |
|
DECLARE_CONDITION(COND_SCANNER_RELEASED_FROM_PHYSCANNON) |
|
DECLARE_CONDITION(COND_SCANNER_GRABBED_BY_PHYSCANNON) |
|
|
|
//========================================================= |
|
// > SCHED_SCANNER_PATROL |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_SCANNER_PATROL, |
|
|
|
" Tasks" |
|
" TASK_SCANNER_SET_FLY_PATROL 0" |
|
" TASK_SET_TOLERANCE_DISTANCE 32" |
|
" TASK_SET_ROUTE_SEARCH_TIME 5" // Spend 5 seconds trying to build a path if stuck |
|
" TASK_GET_PATH_TO_RANDOM_NODE 2000" |
|
" TASK_RUN_PATH 0" |
|
" TASK_WAIT_FOR_MOVEMENT 0" |
|
"" |
|
" Interrupts" |
|
" COND_GIVE_WAY" |
|
" COND_NEW_ENEMY" |
|
" COND_SEE_ENEMY" |
|
" COND_SEE_FEAR" |
|
" COND_HEAR_COMBAT" |
|
" COND_HEAR_DANGER" |
|
" COND_HEAR_PLAYER" |
|
" COND_LIGHT_DAMAGE" |
|
" COND_HEAVY_DAMAGE" |
|
" COND_PROVOKED" |
|
" COND_SCANNER_GRABBED_BY_PHYSCANNON" |
|
) |
|
|
|
//========================================================= |
|
// > SCHED_SCANNER_ATTACK |
|
// |
|
// This task does nothing. Translate it in your derived |
|
// class to perform your attack. |
|
// |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_SCANNER_ATTACK, |
|
|
|
" Tasks" |
|
" TASK_SCANNER_SET_FLY_ATTACK 0" |
|
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" |
|
" TASK_WAIT 0.1" |
|
"" |
|
" Interrupts" |
|
" COND_TOO_FAR_TO_ATTACK" |
|
" COND_SCANNER_FLY_BLOCKED" |
|
" COND_NEW_ENEMY" |
|
" COND_SCANNER_GRABBED_BY_PHYSCANNON" |
|
) |
|
|
|
//========================================================= |
|
// > SCHED_SCANNER_ATTACK_HOVER |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_SCANNER_ATTACK_HOVER, |
|
|
|
" Tasks" |
|
" TASK_SCANNER_SET_FLY_ATTACK 0" |
|
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" |
|
" TASK_WAIT 0.1" |
|
"" |
|
" Interrupts" |
|
" COND_TOO_FAR_TO_ATTACK" |
|
" COND_SCANNER_FLY_BLOCKED" |
|
" COND_NEW_ENEMY" |
|
" COND_SCANNER_GRABBED_BY_PHYSCANNON" |
|
) |
|
|
|
//========================================================= |
|
// > SCHED_SCANNER_ATTACK_DIVEBOMB |
|
// |
|
// Only done when scanner is dead |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_SCANNER_ATTACK_DIVEBOMB, |
|
|
|
" Tasks" |
|
" TASK_SCANNER_SET_FLY_DIVE 0" |
|
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" |
|
" TASK_WAIT 10" |
|
"" |
|
" Interrupts" |
|
" COND_SCANNER_GRABBED_BY_PHYSCANNON" |
|
) |
|
|
|
//========================================================= |
|
// > SCHED_SCANNER_CHASE_ENEMY |
|
// |
|
// Different interrupts than normal chase enemy. |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_SCANNER_CHASE_ENEMY, |
|
|
|
" Tasks" |
|
" TASK_SCANNER_SET_FLY_CHASE 0" |
|
" TASK_SET_FAIL_SCHEDULE SCHEDULE:SCHED_SCANNER_PATROL" |
|
" TASK_SET_TOLERANCE_DISTANCE 120" |
|
" TASK_GET_PATH_TO_ENEMY 0" |
|
" TASK_RUN_PATH 0" |
|
" TASK_WAIT_FOR_MOVEMENT 0" |
|
"" |
|
"" |
|
" Interrupts" |
|
" COND_SCANNER_FLY_CLEAR" |
|
" COND_NEW_ENEMY" |
|
" COND_ENEMY_DEAD" |
|
" COND_LOST_ENEMY" |
|
" COND_SCANNER_GRABBED_BY_PHYSCANNON" |
|
) |
|
|
|
//========================================================= |
|
// > SCHED_SCANNER_CHASE_TARGET |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_SCANNER_CHASE_TARGET, |
|
|
|
" Tasks" |
|
" TASK_SCANNER_SET_FLY_CHASE 0" |
|
" TASK_SET_TOLERANCE_DISTANCE 64" |
|
" TASK_GET_PATH_TO_TARGET 0" //FIXME: This is wrong! |
|
" TASK_RUN_PATH 0" |
|
" TASK_WAIT_FOR_MOVEMENT 0" |
|
"" |
|
" Interrupts" |
|
" COND_SCANNER_FLY_CLEAR" |
|
" COND_NEW_ENEMY" |
|
" COND_SCANNER_GRABBED_BY_PHYSCANNON" |
|
) |
|
|
|
//========================================================= |
|
// > SCHED_SCANNER_FOLLOW_HOVER |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_SCANNER_FOLLOW_HOVER, |
|
|
|
" Tasks" |
|
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" |
|
" TASK_WAIT 0.1" |
|
"" |
|
" Interrupts" |
|
" COND_SCANNER_FLY_BLOCKED" |
|
" COND_SCANNER_GRABBED_BY_PHYSCANNON" |
|
) |
|
|
|
//========================================================= |
|
// > SCHED_SCANNER_HELD_BY_PHYSCANNON |
|
//========================================================= |
|
DEFINE_SCHEDULE |
|
( |
|
SCHED_SCANNER_HELD_BY_PHYSCANNON, |
|
|
|
" Tasks" |
|
" TASK_SET_ACTIVITY ACTIVITY:ACT_IDLE" |
|
" TASK_WAIT 5.0" |
|
"" |
|
" Interrupts" |
|
" COND_LIGHT_DAMAGE" |
|
" COND_HEAVY_DAMAGE" |
|
" COND_SCANNER_RELEASED_FROM_PHYSCANNON" |
|
) |
|
|
|
AI_END_CUSTOM_NPC()
|
|
|