diff options
Diffstat (limited to 'game/server/tf/bot_npc/bot_npc_minion.cpp')
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc_minion.cpp | 1239 |
1 files changed, 1239 insertions, 0 deletions
diff --git a/game/server/tf/bot_npc/bot_npc_minion.cpp b/game/server/tf/bot_npc/bot_npc_minion.cpp new file mode 100644 index 0000000..59f04a0 --- /dev/null +++ b/game/server/tf/bot_npc/bot_npc_minion.cpp @@ -0,0 +1,1239 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// bot_npc_minion.cpp +// Minions for the Boss +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef TF_RAID_MODE + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "tf_projectile_arrow.h" +#include "tf_weapon_grenade_pipebomb.h" +#include "tf_ammo_pack.h" +#include "nav_mesh/tf_nav_area.h" +#include "bot_npc_minion.h" +#include "NextBot/Path/NextBotChasePath.h" +#include "econ_wearable.h" +#include "team_control_point_master.h" +#include "particle_parse.h" +#include "nav_mesh/tf_path_follower.h" +#include "tf_obj_sentrygun.h" +#include "bot/map_entities/tf_spawner.h" + +#define MINION_LIGHT_ON 0 +#define MINION_LIGHT_OFF 1 + +ConVar tf_bot_npc_minion_health( "tf_bot_npc_minion_health", "60"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_speed( "tf_bot_npc_minion_speed", "250"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_acceleration( "tf_bot_npc_minion_acceleration", "500"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_horiz_damping( "tf_bot_npc_minion_horiz_damping", "2"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_vert_damping( "tf_bot_npc_minion_vert_damping", "1"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_stun_charge( "tf_bot_npc_minion_stun_charge", "0.4"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_ammo_count( "tf_bot_npc_minion_ammo_count", "100"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_debug( "tf_bot_npc_minion_debug", "0"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_notice_threat_range( "tf_bot_npc_minion_notice_threat_range", "500"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_stun_range( "tf_bot_npc_minion_stun_range", "100"/*,FCVAR_CHEAT */ ); +ConVar tf_bot_npc_minion_stun_charge_up_time( "tf_bot_npc_minion_stun_charge_up_time", "1.5"/*,FCVAR_CHEAT */ ); +ConVar tf_bot_npc_minion_stun_kill_time( "tf_bot_npc_minion_stun_kill_time", "7"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_invuln_duration( "tf_bot_npc_minion_invuln_duration", "3"/*, FCVAR_CHEAT*/ ); + + + +LINK_ENTITY_TO_CLASS( bot_npc_minion, CBotNPCMinion ); + +PRECACHE_REGISTER( bot_npc_minion ); + +IMPLEMENT_SERVERCLASS_ST( CBotNPCMinion, DT_BotNPCMinion ) + + SendPropEHandle( SENDINFO( m_stunTarget ) ), + +END_SEND_TABLE() + + +//----------------------------------------------------------------------------------------------------- +class CTFSpawnTemplateStunDrone : public CTFSpawnTemplate +{ +public: + virtual CBaseEntity *Instantiate( void ) const + { + return CreateEntityByName( "bot_npc_minion" ); + } +}; + +LINK_ENTITY_TO_CLASS( tf_template_stun_drone, CTFSpawnTemplateStunDrone ); + + + + +//----------------------------------------------------------------------------------------------------- +//----------------------------------------------------------------------------------------------------- +CBotNPCMinion::CBotNPCMinion() +{ + ALLOCATE_INTENTION_INTERFACE( CBotNPCMinion ); + + m_locomotor = new CNextBotFlyingLocomotion( this ); + m_body = new CBotNPCBody( this ); + m_vision = new CDisableVision( this ); + + m_eyeOffset = vec3_origin; + m_target = NULL; + m_stunTarget = NULL; + m_isAlert = false; +} + + +//----------------------------------------------------------------------------------------------------- +CBotNPCMinion::~CBotNPCMinion() +{ + DEALLOCATE_INTENTION_INTERFACE; + + if ( m_vision ) + delete m_vision; + + if ( m_locomotor ) + delete m_locomotor; + + if ( m_body ) + delete m_body; +} + +//----------------------------------------------------------------------------------------------------- +void CBotNPCMinion::Precache() +{ + BaseClass::Precache(); + + PrecacheModel( "models/props_swamp/bug_zapper.mdl" ); +// PrecacheModel( "models/combine_helicopter/helicopter_bomb01.mdl" ); +// PrecacheModel( "models/props_lights/spotlight001a.mdl" ); + + PrecacheScriptSound( "Minion.Ping.Roam" ); + PrecacheScriptSound( "Minion.Ping.Acquire" ); + PrecacheScriptSound( "Minion.Bounce" ); +// PrecacheScriptSound( "Minion.Explode" ); + PrecacheScriptSound( "Minion.ChargeUpStun" ); + PrecacheScriptSound( "Minion.Stun" ); + PrecacheScriptSound( "Minion.StunKill" ); + PrecacheScriptSound( "Minion.Building" ); + PrecacheScriptSound( "Minion.Recognize" ); + PrecacheScriptSound( "Minion.Notice" ); + + PrecacheParticleSystem( "cart_flashinglight" ); +} + + +//----------------------------------------------------------------------------------------------------- +void CBotNPCMinion::Spawn( void ) +{ + BaseClass::Spawn(); + + SetModel( "models/props_swamp/bug_zapper.mdl" ); + + int health = tf_bot_npc_minion_health.GetInt(); + SetHealth( health ); + SetMaxHealth( health ); + + ChangeTeam( TF_TEAM_RED ); + + // this flag lets flamethrowers deflect me + AddFlag( FL_GRENADE ); + + m_eyeOffset = Vector( 0, 0, 20.0f ); + m_lastKnownTargetPosition = vec3_origin; + m_isAlert = false; + + m_invulnTimer.Start( tf_bot_npc_minion_invuln_duration.GetFloat() ); +} + + +//--------------------------------------------------------------------------------------------- +ConVar tf_bot_npc_minion_dmg_mult_sentry( "tf_bot_npc_minion_dmg_mult_sentry", "0.5"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_dmg_mult_sniper( "tf_bot_npc_minion_dmg_mult_sniper", "2"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_dmg_mult_arrow( "tf_bot_npc_minion_dmg_mult_arrow", "3"/*, FCVAR_CHEAT*/ ); + +float MinionModifyDamage( const CTakeDamageInfo &info ) +{ + CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( info.GetWeapon() ); + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() ); + + if ( sentry ) + { + return info.GetDamage() * tf_bot_npc_minion_dmg_mult_sentry.GetFloat(); + } + else if ( pWeapon ) + { + switch( pWeapon->GetWeaponID() ) + { + case TF_WEAPON_SNIPERRIFLE: + case TF_WEAPON_SNIPERRIFLE_DECAP: + case TF_WEAPON_SNIPERRIFLE_CLASSIC: + return info.GetDamage() * tf_bot_npc_minion_dmg_mult_sniper.GetFloat(); + + case TF_WEAPON_COMPOUND_BOW: + return info.GetDamage() * tf_bot_npc_minion_dmg_mult_arrow.GetFloat(); + } + } + + // unmodified + return info.GetDamage(); +} + + +//--------------------------------------------------------------------------------------------- +int CBotNPCMinion::OnTakeDamage_Alive( const CTakeDamageInfo &rawInfo ) +{ + if ( !m_invulnTimer.IsElapsed() ) + { + // invulnerable for a moment after spawning + return 0; + } + + CTakeDamageInfo info = rawInfo; + + info.SetDamage( MinionModifyDamage( info ) ); + + BecomeAlert(); + + // fire event for client combat text, beep, etc. + IGameEvent *event = gameeventmanager->CreateEvent( "npc_hurt" ); + if ( event ) + { + event->SetInt( "entindex", entindex() ); + event->SetInt( "health", MAX( 0, GetHealth() ) ); + event->SetInt( "damageamount", info.GetDamage() ); + event->SetBool( "crit", ( info.GetDamageType() & DMG_CRITICAL ) ? true : false ); + + CTFPlayer *attackerPlayer = ToTFPlayer( info.GetAttacker() ); + if ( attackerPlayer ) + { + event->SetInt( "attacker_player", attackerPlayer->GetUserID() ); + + if ( attackerPlayer->GetActiveTFWeapon() ) + { + event->SetInt( "weaponid", attackerPlayer->GetActiveTFWeapon()->GetWeaponID() ); + } + else + { + event->SetInt( "weaponid", 0 ); + } + } + else + { + // hurt by world + event->SetInt( "attacker_player", 0 ); + event->SetInt( "weaponid", 0 ); + } + + gameeventmanager->FireEvent( event ); + } + + return BaseClass::OnTakeDamage_Alive( info ); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCMinion::Deflected( CBaseEntity *pDeflectedBy, Vector &vecDir ) +{ + BecomeAlert(); + + if ( pDeflectedBy ) + { + GetLocomotionInterface()->Deflect( pDeflectedBy ); + } +} + + +//--------------------------------------------------------------------------------------------- +unsigned int CBotNPCMinion::PhysicsSolidMaskForEntity( void ) const +{ + // Only collide with the other team + int teamContents = ( GetTeamNumber() == TF_TEAM_RED ) ? CONTENTS_BLUETEAM : CONTENTS_REDTEAM; + + return BaseClass::PhysicsSolidMaskForEntity() | teamContents; +} + + +//--------------------------------------------------------------------------------------------- +bool CBotNPCMinion::ShouldCollide( int collisionGroup, int contentsMask ) const +{ + if ( collisionGroup == COLLISION_GROUP_PLAYER_MOVEMENT ) + { + switch( GetTeamNumber() ) + { + case TF_TEAM_RED: + if ( !( contentsMask & CONTENTS_REDTEAM ) ) + return false; + break; + + case TF_TEAM_BLUE: + if ( !( contentsMask & CONTENTS_BLUETEAM ) ) + return false; + break; + } + } + + return BaseClass::ShouldCollide( collisionGroup, contentsMask ); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCMinion::BecomeAmmoPack( void ) +{ + int iPrimary = tf_bot_npc_minion_ammo_count.GetInt(); + int iSecondary = tf_bot_npc_minion_ammo_count.GetInt(); + int iMetal = tf_bot_npc_minion_ammo_count.GetInt(); + + // Create the ammo pack. + //CTFAmmoPack *pAmmoPack = CTFAmmoPack::Create( GetAbsOrigin(), GetAbsAngles(), NULL, "models/props_swamp/bug_zapper.mdl" ); + CTFAmmoPack *pAmmoPack = CTFAmmoPack::Create( GetAbsOrigin(), GetAbsAngles(), NULL, "models/items/ammopack_medium.mdl" ); + if ( pAmmoPack ) + { + Vector vel = GetAbsVelocity(); + pAmmoPack->SetInitialVelocity( vel ); + pAmmoPack->m_nSkin = MINION_LIGHT_OFF; + + // Give the ammo pack some health, so that trains can destroy it. + pAmmoPack->SetCollisionGroup( COLLISION_GROUP_DEBRIS ); + pAmmoPack->m_takedamage = DAMAGE_YES; + pAmmoPack->SetHealth( 900 ); + + pAmmoPack->SetBodygroup( 1, 1 ); + + DispatchSpawn( pAmmoPack ); + + // Fill up the ammo pack. + pAmmoPack->GiveAmmo( iPrimary, TF_AMMO_PRIMARY ); + pAmmoPack->GiveAmmo( iSecondary, TF_AMMO_SECONDARY ); + pAmmoPack->GiveAmmo( iMetal, TF_AMMO_METAL ); + } +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCMinion::BecomeAlert( void ) +{ + m_isAlert = true; + m_nSkin = MINION_LIGHT_ON; +} + + +//--------------------------------------------------------------------------------------------- +// +// Find the closest living player not already being targeted by another minion +// +CTFPlayer *CBotNPCMinion::FindTarget( void ) +{ + CUtlVector< CBotNPCMinion * > minionVector; + CBotNPCMinion *minion = NULL; + while( ( minion = (CBotNPCMinion *)gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL ) + { + minionVector.AddToTail( minion ); + } + + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS ); + + CTFPlayer *closeVictim = NULL; + float victimRangeSq = FLT_MAX; + + for( int i=0; i<playerVector.Count(); ++i ) + { + if ( !playerVector[i]->IsAlive() ) + continue; + + float rangeSq = GetRangeSquaredTo( playerVector[i] ); + if ( rangeSq < victimRangeSq ) + { + if ( playerVector[i]->m_Shared.IsStealthed() ) + { + continue; + } + + // is any other minion targeting this player? + bool isTargetAvailable = true; + for( int m=0; m<minionVector.Count(); ++m ) + { + CBotNPCMinion *peer = minionVector[m]; + + if ( peer != this && peer->HasTarget() && peer->GetTarget() == playerVector[i] ) + { + isTargetAvailable = false; + break; + } + } + + if ( isTargetAvailable ) + { + closeVictim = playerVector[i]; + victimRangeSq = rangeSq; + } + } + } + + return closeVictim; +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCMinion::UpdateTarget( void ) +{ + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS ); + + CTFPlayer *closestPlayer = NULL; + float closestRangeSq = IsAlert() ? FLT_MAX : ( tf_bot_npc_minion_notice_threat_range.GetFloat() * tf_bot_npc_minion_notice_threat_range.GetFloat() ); + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *player = playerVector[i]; + + float rangeSq = ( player->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); + if ( rangeSq < closestRangeSq ) + { + if ( IsLineOfSightClear( player, IGNORE_ACTORS ) ) + { + closestPlayer = player; + closestRangeSq = rangeSq; + } + } + } + + if ( closestPlayer ) + { + if ( closestPlayer != GetTarget() ) + { + EmitSound( "Minion.Notice" ); + SetTarget( closestPlayer ); + } + + m_lastKnownTargetPosition = closestPlayer->GetAbsOrigin(); + } + else + { + SetTarget( NULL ); + } +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCMinionHoldStunVictim : public Action< CBotNPCMinion > +{ +public: + virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ); + virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval ); + virtual void OnEnd( CBotNPCMinion *me, Action< CBotNPCMinion > *nextAction ); + + virtual const char *GetName( void ) const { return "HoldStunVictim"; } // return name of this action + +private: + CTFPathFollower m_path; + CountdownTimer m_restunTimer; + CountdownTimer m_killTimer; +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionHoldStunVictim::OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ) +{ + CTFPlayer *target = me->GetTarget(); + + if ( !target ) + { + return Done( "No target" ); + } + + me->StartStunEffects( target ); + + me->EmitSound( "Minion.Stun" ); + + m_restunTimer.Invalidate(); + m_killTimer.Start( tf_bot_npc_minion_stun_kill_time.GetFloat() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionHoldStunVictim::Update( CBotNPCMinion *me, float interval ) +{ + CTFPlayer *target = me->GetTarget(); + + if ( !target ) + { + return Done( "No target" ); + } + + me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() ); + + if ( me->IsRangeGreaterThan( target, tf_bot_npc_minion_stun_range.GetFloat() ) ) + { + return Done( "Target out of stun range" ); + } + + // if we've held them long enough, they die + if ( m_killTimer.IsElapsed() ) + { + me->EmitSound( "Minion.StunKill" ); + + CTakeDamageInfo info( me, me, 1000.0f, DMG_ENERGYBEAM, TF_DMG_CUSTOM_NONE ); + + Vector toTarget = target->WorldSpaceCenter() - me->WorldSpaceCenter(); + toTarget.z = 0.5f; + toTarget.NormalizeInPlace(); + + CalculateMeleeDamageForce( &info, toTarget, me->WorldSpaceCenter(), 1.0f ); + target->TakeDamage( info ); + + return Done( "My work here is done." ); + } + + // stun them! + // if they are ubered we overload and explode from the energy feedback + if ( target->m_Shared.InCond( TF_COND_INVULNERABLE ) ) + { + CTakeDamageInfo info( target, target, me->GetMaxHealth() + 10.0f, DMG_ENERGYBEAM, TF_DMG_CUSTOM_NONE ); + + Vector fromVictim = me->WorldSpaceCenter() - target->WorldSpaceCenter(); + fromVictim.NormalizeInPlace(); + + CalculateMeleeDamageForce( &info, fromVictim, me->WorldSpaceCenter(), 1.0f ); + me->TakeDamage( info ); + + return Done( "Destroyed by target's Uber" ); + } + + // periodically restart the stun effect + if ( m_restunTimer.IsElapsed() ) + { + const float stunTime = 1.0f; + const float speedReduction = 0.5f; + + int stunFlags = TF_STUN_LOSER_STATE | TF_STUN_MOVEMENT | TF_STUN_CONTROLS; + target->m_Shared.StunPlayer( stunTime + 0.5f, speedReduction, stunFlags, NULL ); + m_restunTimer.Start( stunTime ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCMinionHoldStunVictim::OnEnd( CBotNPCMinion *me, Action< CBotNPCMinion > *nextAction ) +{ + me->EndStunEffects(); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCMinionStartStunAttack : public Action< CBotNPCMinion > +{ +public: + virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ); + virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval ); + + virtual const char *GetName( void ) const { return "StartStunAttack"; } // return name of this action + +private: + CTFPathFollower m_path; + CountdownTimer m_stunTimer; +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionStartStunAttack::OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ) +{ + // light on + me->m_nSkin = MINION_LIGHT_ON; + + me->EmitSound( "Minion.ChargeUpStun" ); + m_stunTimer.Start( tf_bot_npc_minion_stun_charge_up_time.GetFloat() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionStartStunAttack::Update( CBotNPCMinion *me, float interval ) +{ + CTFPlayer *target = me->GetTarget(); + + if ( !target ) + { + return Done( "No target" ); + } + + me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() ); + + if ( me->IsRangeGreaterThan( target, tf_bot_npc_minion_stun_range.GetFloat() ) ) + { + return Done( "Target out of stun range" ); + } + + if ( m_stunTimer.IsElapsed() ) + { + // stun 'em + return ChangeTo( new CBotNPCMinionHoldStunVictim, "Stun successful" ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCMinionReadyToStun : public Action< CBotNPCMinion > +{ +public: + virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval ); + + virtual const char *GetName( void ) const { return "ReadyToStun"; } // return name of this action +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionReadyToStun::Update( CBotNPCMinion *me, float interval ) +{ + CTFPlayer *target = me->GetTarget(); + + if ( target ) + { + me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() ); + + if ( me->IsRangeLessThan( target, tf_bot_npc_minion_stun_range.GetFloat() ) ) + { + return SuspendFor( new CBotNPCMinionStartStunAttack, "Charging stun..." ); + } + } + + return Continue(); +} + + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCMinionApproachTarget : public Action< CBotNPCMinion > +{ +public: + virtual Action< CBotNPCMinion > *InitialContainedAction( CBotNPCMinion *me ); + + virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ); + virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval ); + virtual void OnEnd( CBotNPCMinion *me, Action< CBotNPCMinion > *nextAction ); + + virtual const char *GetName( void ) const { return "ApproachTarget"; } // return name of this action + +private: + CTFPathFollower m_path; + CountdownTimer m_initialStunTimer; + CountdownTimer m_chooseTargetTimer; + CountdownTimer m_pingTimer; +}; + + +//--------------------------------------------------------------------------------------------- +Action< CBotNPCMinion > *CBotNPCMinionApproachTarget::InitialContainedAction( CBotNPCMinion *me ) +{ + return new CBotNPCMinionReadyToStun; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionApproachTarget::OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ) +{ + if ( !me->HasTarget() ) + { + return Done( "No initial target" ); + } + + m_pingTimer.Start( 1.0f ); + + // light on + me->m_nSkin = MINION_LIGHT_ON; + + me->EmitSound( "Minion.Recognize" ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionApproachTarget::Update( CBotNPCMinion *me, float interval ) +{ + if ( !me->HasTarget() ) + { + me->GetLocomotionInterface()->FaceTowards( me->GetLastKnownTargetPosition() ); + + if ( ( me->GetAbsOrigin() - me->GetLastKnownTargetPosition() ).AsVector2D().IsLengthLessThan( 20.0f ) ) + { + // reached last know position of threat - we lost them + return Done( "Lost our target" ); + } + + me->EndStunEffects(); + } + + // approach + if ( !m_path.IsValid() || m_path.GetAge() > 1.0f ) + { + ShortestPathCost cost; + m_path.Compute( me, me->GetLastKnownTargetPosition(), cost ); + } + + me->GetLocomotionInterface()->SetDesiredSpeed( tf_bot_npc_minion_speed.GetFloat() ); + + if ( me->IsLineOfSightClear( me->GetLastKnownTargetPosition() ) ) + { + // move directly toward our goal + me->GetLocomotionInterface()->Approach( me->GetLastKnownTargetPosition() ); + } + else + { + m_path.Update( me ); + } + + if ( m_pingTimer.IsElapsed() ) + { + m_pingTimer.Reset(); + me->EmitSound( "Minion.Ping.Acquire" ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCMinionApproachTarget::OnEnd( CBotNPCMinion *me, Action< CBotNPCMinion > *nextAction ) +{ + me->EndStunEffects(); + me->m_nSkin = MINION_LIGHT_OFF; +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCMinionNotice : public Action< CBotNPCMinion > +{ +public: + virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ); + virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval ); + + virtual const char *GetName( void ) const { return "Notice"; } // return name of this action + +private: + CountdownTimer m_timer; +}; + +ConVar tf_bot_npc_minion_notice_duration( "tf_bot_npc_minion_notice_duration", "1"/*, FCVAR_CHEAT */ ); + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionNotice::OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ) +{ + m_timer.Start( tf_bot_npc_minion_notice_duration.GetFloat() ); + + me->BecomeAlert(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionNotice::Update( CBotNPCMinion *me, float interval ) +{ + if ( me->HasTarget() ) + { + me->GetLocomotionInterface()->FaceTowards( me->GetTarget()->WorldSpaceCenter() ); + } + + if ( m_timer.IsElapsed() ) + { + return ChangeTo( new CBotNPCMinionApproachTarget, "Acquiring target" ); + } + + return Continue(); +} + + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCMinionRoam : public Action< CBotNPCMinion > +{ +public: + virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ); + virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval ); + + virtual const char *GetName( void ) const { return "Roam"; } // return name of this action + +private: + CountdownTimer m_pingTimer; + CTFNavArea *m_goalArea; + CountdownTimer m_waitTimer; + + void UpdateGoal( CBotNPCMinion *me ); +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionRoam::OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ) +{ + m_goalArea = NULL; + m_pingTimer.Invalidate(); + m_waitTimer.Invalidate(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionRoam::Update( CBotNPCMinion *me, float interval ) +{ + if ( me->HasTarget() ) + { + return SuspendFor( new CBotNPCMinionNotice, "Target found..." ); + } + + // light off + me->m_nSkin = MINION_LIGHT_OFF; + + if ( m_goalArea == NULL || + me->GetLastKnownArea() == m_goalArea || + m_goalArea->IsOverlapping( me->WorldSpaceCenter() ) || + ( me->WorldSpaceCenter() - m_goalArea->GetCenter() ).AsVector2D().IsLengthLessThan( 20.0f ) ) + { + // at goal + if ( m_waitTimer.HasStarted() ) + { + if ( m_waitTimer.IsElapsed() ) + { + // time to find a new goal + UpdateGoal( me ); + m_waitTimer.Invalidate(); + } + } + else + { + m_waitTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + } + } + + if ( m_goalArea ) + { + me->GetLocomotionInterface()->SetDesiredSpeed( tf_bot_npc_minion_speed.GetFloat() ); + me->GetLocomotionInterface()->Approach( m_goalArea->GetCenter() ); + + if ( tf_bot_npc_minion_debug.GetBool() ) + { + NDebugOverlay::Line( me->WorldSpaceCenter(), m_goalArea->GetCenter(), 0, 255, 0, true, 0.1f ); + } + } + + if ( m_pingTimer.IsElapsed() ) + { + m_pingTimer.Start( RandomFloat( 0.9f, 1.1f ) * 3.0f ); + me->EmitSound( "Minion.Ping.Roam" ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCMinionRoam::UpdateGoal( CBotNPCMinion *me ) +{ + CNavArea *myArea = me->GetLastKnownArea(); + + if ( myArea ) + { + CUtlVector< CNavArea * > adjVector; + myArea->CollectAdjacentAreas( &adjVector ); + + if ( adjVector.Count() > 0 ) + { + m_goalArea = (CTFNavArea *)adjVector[ RandomInt( 0, adjVector.Count()-1 ) ]; + } + } + else + { + CBaseCombatCharacter *boss = me->GetOwnerEntity() ? me->GetOwnerEntity()->MyCombatCharacterPointer() : NULL; + if ( boss ) + { + m_goalArea = (CTFNavArea *)boss->GetLastKnownArea(); + } + } +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCMinionIdle : public Action< CBotNPCMinion > +{ +public: + virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ); + virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval ); + + virtual const char *GetName( void ) const { return "Idle"; } // return name of this action + +private: + CountdownTimer m_pingTimer; +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionIdle::OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ) +{ + m_pingTimer.Start( 3.0f ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCMinion > CBotNPCMinionIdle::Update( CBotNPCMinion *me, float interval ) +{ + if ( me->HasTarget() ) + { + return SuspendFor( new CBotNPCMinionNotice, "Target found..." ); + } + + // light off + me->m_nSkin = MINION_LIGHT_OFF; + + if ( m_pingTimer.IsElapsed() ) + { + m_pingTimer.Reset(); + me->EmitSound( "Minion.Ping.Roam" ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +ConVar tf_bot_npc_minion_init_vel( "tf_bot_npc_minion_init_vel", "250"/*, FCVAR_CHEAT*/ ); + + +class CBotNPCMinionBehavior : public Action< CBotNPCMinion > +{ +public: + virtual Action< CBotNPCMinion > *InitialContainedAction( CBotNPCMinion *me ) + { + if ( TFGameRules()->GetActiveBoss() ) + { + return new CBotNPCMinionRoam; + } + + // minions in the wild just hover in place + return new CBotNPCMinionIdle; + } + + virtual ActionResult< CBotNPCMinion > OnStart( CBotNPCMinion *me, Action< CBotNPCMinion > *priorAction ) + { + Vector initVelocity; + + float s, c; + SinCos( RandomFloat( -M_PI, M_PI ), &s, &c ); + + initVelocity.x = c * tf_bot_npc_minion_init_vel.GetFloat(); + initVelocity.y = s * tf_bot_npc_minion_init_vel.GetFloat(); + initVelocity.z = 0.0f; + + static_cast< CNextBotFlyingLocomotion * >( me->GetLocomotionInterface() )->SetVelocity( initVelocity ); + + return Continue(); + } + + virtual ActionResult< CBotNPCMinion > Update( CBotNPCMinion *me, float interval ) + { + me->UpdateTarget(); + + return Continue(); + } + + virtual EventDesiredResult< CBotNPCMinion > OnKilled( CBotNPCMinion *me, const CTakeDamageInfo &info ) + { + me->BecomeAmmoPack(); + + DispatchParticleEffect( "asplode_hoodoo_embers", me->GetAbsOrigin(), me->GetAbsAngles() ); + me->EmitSound( "Minion.Explode" ); + UTIL_Remove( me ); + + return TryDone(); + } + + virtual const char *GetName( void ) const { return "Behavior"; } // return name of this action +}; + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +IMPLEMENT_INTENTION_INTERFACE( CBotNPCMinion, CBotNPCMinionBehavior ); + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CNextBotFlyingLocomotion::CNextBotFlyingLocomotion( INextBot *bot ) : ILocomotion( bot ) +{ + Reset(); +} + + +//--------------------------------------------------------------------------------------------- +CNextBotFlyingLocomotion::~CNextBotFlyingLocomotion() +{ +} + + +//--------------------------------------------------------------------------------------------- +// (EXTEND) reset to initial state +void CNextBotFlyingLocomotion::Reset( void ) +{ + m_velocity = vec3_origin; + m_acceleration = vec3_origin; + m_desiredSpeed = 0.0f; + m_currentSpeed = 0.0f; + m_forward = vec3_origin; + m_desiredAltitude = 50.0f; +} + + +//--------------------------------------------------------------------------------------------- +void CNextBotFlyingLocomotion::MaintainAltitude( void ) +{ + CBaseCombatCharacter *me = GetBot()->GetEntity(); + + trace_t result; + //CTraceFilterSimple filter( me, COLLISION_GROUP_NONE ); + CTraceFilterSimpleClassnameList filter( me, COLLISION_GROUP_NONE ); + filter.AddClassnameToIgnore( "bot_npc_minion" ); + + // find ceiling + TraceHull( me->GetAbsOrigin(), me->GetAbsOrigin() + Vector( 0, 0, 1000.0f ), + me->WorldAlignMins(), me->WorldAlignMaxs(), + GetBot()->GetBodyInterface()->GetSolidMask(), &filter, &result ); + + float ceiling = result.endpos.z - me->GetAbsOrigin().z; + + // trace wider hull to account for nearby ledges we want to float over + TraceHull( me->GetAbsOrigin() + Vector( 0, 0, ceiling ), + me->GetAbsOrigin() + Vector( 0, 0, -1000.0f ), + Vector( 2.0f * me->WorldAlignMins().x, 2.0f * me->WorldAlignMins().y, me->WorldAlignMins().z ), + Vector( 2.0f * me->WorldAlignMaxs().x, 2.0f * me->WorldAlignMaxs().y, me->WorldAlignMaxs().z ), + GetBot()->GetBodyInterface()->GetSolidMask(), &filter, &result ); + + float groundZ = result.endpos.z; + + float currentAltitude = me->GetAbsOrigin().z - groundZ; + float error = m_desiredAltitude - currentAltitude; + float accelZ = clamp( error, -tf_bot_npc_minion_acceleration.GetFloat(), tf_bot_npc_minion_acceleration.GetFloat() ); + + m_acceleration.z += accelZ; +} + + +ConVar tf_bot_npc_minion_avoid_range( "tf_bot_npc_minion_avoid_range", "100"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_avoid_force( "tf_bot_npc_minion_avoid_force", "100"/*, FCVAR_CHEAT*/ ); + + +//--------------------------------------------------------------------------------------------- +// (EXTEND) update internal state +void CNextBotFlyingLocomotion::Update( void ) +{ + CBaseCombatCharacter *me = GetBot()->GetEntity(); + const float deltaT = GetUpdateInterval(); + + Vector pos = me->GetAbsOrigin(); + + // always maintain altitude, even if not trying to move (ie: no Approach call) + MaintainAltitude(); + + m_forward = m_velocity; + m_currentSpeed = m_forward.NormalizeInPlace(); + + Vector damping( tf_bot_npc_minion_horiz_damping.GetFloat(), tf_bot_npc_minion_horiz_damping.GetFloat(), tf_bot_npc_minion_vert_damping.GetFloat() ); + Vector totalAccel = m_acceleration - m_velocity * damping; + + // avoid other minions + CBaseEntity *minion = NULL; + while( ( minion = gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL ) + { + if ( me == minion ) + continue; + + Vector toPeer = minion->GetAbsOrigin() - me->GetAbsOrigin(); + toPeer.z = 0.0f; + float range = toPeer.NormalizeInPlace(); + + if ( range < tf_bot_npc_minion_avoid_range.GetFloat() ) + { + totalAccel += -tf_bot_npc_minion_avoid_force.GetFloat() * toPeer; + } + } + + m_velocity += totalAccel * deltaT; + me->SetAbsVelocity( m_velocity ); + + pos += m_velocity * deltaT; + + // check for collisions along move + trace_t result; + CTraceFilterSkipClassname filter( me, "bot_npc_minion", COLLISION_GROUP_NONE ); + Vector from = me->GetAbsOrigin(); + Vector to = pos; + Vector desiredGoal = to; + Vector resolvedGoal; + int recursionLimit = 3; + + int hitCount = 0; + Vector surfaceNormal = vec3_origin; + + bool didHitWorld = false; + + while( true ) + { + TraceHull( from, desiredGoal, me->WorldAlignMins(), me->WorldAlignMaxs(), GetBot()->GetBodyInterface()->GetSolidMask(), &filter, &result ); + + if ( !result.DidHit() ) + { + resolvedGoal = pos; + break; + } + + if ( result.DidHitWorld() ) + { + didHitWorld = true; + } + + ++hitCount; + surfaceNormal += result.plane.normal; + + // If we hit really close to our target, then stop + if ( !result.startsolid && desiredGoal.DistToSqr( result.endpos ) < 1.0f ) + { + resolvedGoal = result.endpos; + break; + } + + if ( result.startsolid ) + { + // stuck inside solid; don't move + resolvedGoal = me->GetAbsOrigin(); + break; + } + + if ( --recursionLimit <= 0 ) + { + // reached recursion limit, no more adjusting allowed + resolvedGoal = result.endpos; + break; + } + + // slide off of surface we hit + Vector fullMove = desiredGoal - from; + Vector leftToMove = fullMove * ( 1.0f - result.fraction ); + + float blocked = DotProduct( result.plane.normal, leftToMove ); + + Vector unconstrained = fullMove - blocked * result.plane.normal; + + // check for collisions along remainder of move + // But don't bother if we're not going to deflect much + Vector remainingMove = from + unconstrained; + if ( remainingMove.DistToSqr( result.endpos ) < 1.0f ) + { + resolvedGoal = result.endpos; + break; + } + + desiredGoal = remainingMove; + } + + if ( hitCount > 0 ) + { + surfaceNormal.NormalizeInPlace(); + + // bounce + m_velocity = m_velocity - 2.0f * DotProduct( m_velocity, surfaceNormal ) * surfaceNormal; + + if ( didHitWorld ) + { + me->EmitSound( "Minion.Bounce" ); + } + } + + GetBot()->GetEntity()->SetAbsOrigin( result.endpos ); + + m_acceleration = vec3_origin; +} + + +//--------------------------------------------------------------------------------------------- +// (EXTEND) move directly towards the given position +void CNextBotFlyingLocomotion::Approach( const Vector &goalPos, float goalWeight ) +{ + Vector flyGoal = goalPos; + flyGoal.z += m_desiredAltitude; + + Vector toGoal = flyGoal - GetBot()->GetEntity()->GetAbsOrigin(); + // altitude is handled in Update() + toGoal.z = 0.0f; + toGoal.NormalizeInPlace(); + + m_acceleration += tf_bot_npc_minion_acceleration.GetFloat() * toGoal; +} + + +//--------------------------------------------------------------------------------------------- +void CNextBotFlyingLocomotion::SetDesiredSpeed( float speed ) +{ + m_desiredSpeed = speed; +} + + +//--------------------------------------------------------------------------------------------- +float CNextBotFlyingLocomotion::GetDesiredSpeed( void ) const +{ + return m_desiredSpeed; +} + + +//--------------------------------------------------------------------------------------------- +void CNextBotFlyingLocomotion::SetDesiredAltitude( float height ) +{ + m_desiredAltitude = height; +} + + +//--------------------------------------------------------------------------------------------- +float CNextBotFlyingLocomotion::GetDesiredAltitude( void ) const +{ + return m_desiredAltitude; +} + + +ConVar tf_bot_npc_minion_deflect_range( "tf_bot_npc_minion_deflect_range", "300"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_deflect_force( "tf_bot_npc_minion_deflect_force", "2000"/*, FCVAR_CHEAT*/ ); + + +//--------------------------------------------------------------------------------------------- +void CNextBotFlyingLocomotion::Deflect( CBaseEntity *deflector ) +{ + if ( deflector ) + { + Vector fromDeflector = GetBot()->GetEntity()->WorldSpaceCenter() - deflector->EyePosition(); + float range = fromDeflector.NormalizeInPlace(); + + if ( range < tf_bot_npc_minion_deflect_range.GetFloat() ) + { + m_velocity += ( 1.0f - ( range / tf_bot_npc_minion_deflect_range.GetFloat() ) ) * tf_bot_npc_minion_deflect_force.GetFloat() * fromDeflector; + } + } +} + +#endif // TF_RAID_MODE |