diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /game/server/tf/bot_npc | |
| download | archived-source-engine-2018-hl2-src-master.tar.xz archived-source-engine-2018-hl2-src-master.zip | |
Diffstat (limited to 'game/server/tf/bot_npc')
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc.cpp | 3604 | ||||
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc.h | 541 | ||||
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc_archer.cpp | 402 | ||||
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc_archer.h | 79 | ||||
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc_body.cpp | 153 | ||||
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc_body.h | 64 | ||||
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc_decoy.cpp | 246 | ||||
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc_decoy.h | 80 | ||||
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc_mini.cpp | 101 | ||||
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc_mini.h | 83 | ||||
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc_minion.cpp | 1239 | ||||
| -rw-r--r-- | game/server/tf/bot_npc/bot_npc_minion.h | 199 |
12 files changed, 6791 insertions, 0 deletions
diff --git a/game/server/tf/bot_npc/bot_npc.cpp b/game/server/tf/bot_npc/bot_npc.cpp new file mode 100644 index 0000000..03d5bca --- /dev/null +++ b/game/server/tf/bot_npc/bot_npc.cpp @@ -0,0 +1,3604 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// bot_npc.cpp +// A NextBot non-player derived actor +// Michael Booth, November 2010 + +#include "cbase.h" + +#ifdef OBSOLETE_USE_BOSS_ALPHA + +#ifdef TF_RAID_MODE + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "tf_projectile_arrow.h" +#include "tf_projectile_rocket.h" +#include "tf_weapon_grenade_pipebomb.h" +#include "tf_ammo_pack.h" +#include "tf_obj_sentrygun.h" +#include "nav_mesh/tf_nav_area.h" +#include "bot_npc.h" +#include "NextBot/Path/NextBotChasePath.h" +#include "econ_wearable.h" +#include "team_control_point_master.h" +#include "particle_parse.h" +#include "CRagdollMagnet.h" +#include "nav_mesh/tf_path_follower.h" +#include "bot_npc_minion.h" +#include "player_vs_environment/monster_resource.h" +#include "bot/map_entities/tf_bot_generator.h" +#include "player_vs_environment/tf_population_manager.h" + +//#define USE_BOSS_SENTRY + + +ConVar tf_bot_npc_health( "tf_bot_npc_health", "100000"/*, FCVAR_CHEAT*/ ); // 50000 + +ConVar tf_bot_npc_speed( "tf_bot_npc_speed", "300"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_attack_range( "tf_bot_npc_attack_range", "300"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_melee_damage( "tf_bot_npc_melee_damage", "150"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_threat_tolerance( "tf_bot_npc_threat_tolerance", "100"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_shoot_interval( "tf_bot_npc_shoot_interval", "15"/*, FCVAR_CHEAT*/ ); // 2 +ConVar tf_bot_npc_aim_time( "tf_bot_npc_aim_time", "1"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_chase_range( "tf_bot_npc_chase_range", "300"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_grenade_launch_range( "tf_bot_npc_grenade_launch_range", "300"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_grenade_damage( "tf_bot_npc_grenade_damage", "25"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_minion_launch_count_initial( "tf_bot_npc_minion_launch_count_initial", "5"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_launch_count_increase_interval( "tf_bot_npc_minion_launch_count_increase_interval", "999999999"/*, FCVAR_CHEAT*/ ); // 30 +ConVar tf_bot_npc_minion_launch_initial_interval( "tf_bot_npc_minion_launch_initial_interval", "20"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_minion_launch_interval( "tf_bot_npc_minion_launch_interval", "30"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_chase_duration( "tf_bot_npc_chase_duration", "30"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_quit_range( "tf_bot_npc_quit_range", "2500"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_reaction_time( "tf_bot_npc_reaction_time", "0.5"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_charge_interval( "tf_bot_npc_charge_interval", "10"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_charge_pushaway_force( "tf_bot_npc_charge_pushaway_force", "500"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_charge_damage( "tf_bot_npc_charge_damage", "150"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_nuke_charge_time( "tf_bot_npc_nuke_charge_time", "5" ); +ConVar tf_bot_npc_nuke_interval( "tf_bot_npc_nuke_interval", "20" ); +ConVar tf_bot_npc_nuke_lethal_time( "tf_bot_npc_nuke_lethal_time", "999999999" ); // 300 + +ConVar tf_bot_npc_block_dps_react( "tf_bot_npc_block_dps_react", "150" ); + +ConVar tf_bot_npc_become_stunned_damage( "tf_bot_npc_become_stunned_damage", "500" ); +ConVar tf_bot_npc_stunned_injury_multiplier( "tf_bot_npc_stunned_injury_multiplier", "10" ); +ConVar tf_bot_npc_stunned_duration( "tf_bot_npc_stunned_duration", "5" ); +ConVar tf_bot_npc_head_radius( "tf_bot_npc_head_radius", "75" ); // 50 + +ConVar tf_bot_npc_stun_rocket_reflect_count( "tf_bot_npc_stun_rocket_reflect_count", "2"/*, FCVAR_CHEAT */ ); +ConVar tf_bot_npc_stun_rocket_reflect_duration( "tf_bot_npc_stun_rocket_reflect_duration", "1"/*, FCVAR_CHEAT */ ); + +ConVar tf_bot_npc_grenade_interval( "tf_bot_npc_grenade_interval", "10" ); + +ConVar tf_bot_npc_hate_taunt_cooldown( "tf_bot_npc_hate_taunt_cooldown", "10"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_debug_damage( "tf_bot_npc_debug_damage", "0"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_always_stun( "tf_bot_npc_always_stun", "0"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_min_nuke_after_stun_time( "tf_bot_npc_min_nuke_after_stun_time", "5" /*, FCVAR_CHEAT */ ); + + + +//----------------------------------------------------------------------------------------------------- +// The Bot NPC +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( bot_boss, CBotNPC ); + +PRECACHE_REGISTER( bot_boss ); + +IMPLEMENT_SERVERCLASS_ST( CBotNPC, DT_BotNPC ) + + SendPropEHandle( SENDINFO( m_laserTarget ) ), + SendPropBool( SENDINFO( m_isNuking ) ), + +END_SEND_TABLE() + + +//------------------------------------------------------------------------------ +void CBotNPC::InputSpawn( inputdata_t &inputdata ) +{ + DispatchSpawn( this ); +} + + +//----------------------------------------------------------------------------------------------------- +CBotNPC::CBotNPC() +{ + m_intention = new CBotNPCIntention( this ); + m_locomotor = new CBotNPCLocomotion( this ); + m_body = new CBotNPCBody( this ); + m_vision = new CBotNPCVision( this ); + + m_conditionFlags = 0; + m_laserTarget = NULL; + m_isNuking = false; + m_ageTimer.Invalidate(); + m_spawner = NULL; + ClearStunDamage(); +} + + +//----------------------------------------------------------------------------------------------------- +CBotNPC::~CBotNPC() +{ + if ( m_intention ) + delete m_intention; + + if ( m_locomotor ) + delete m_locomotor; + + if ( m_body ) + delete m_body; + + if ( m_vision ) + delete m_vision; +} + + +//----------------------------------------------------------------------------------------------------- +void CBotNPC::Precache() +{ + BaseClass::Precache(); + +#ifdef USE_BOSS_SENTRY + int model = PrecacheModel( "models/bots/boss_sentry/boss_sentry.mdl" ); +#else + int model = PrecacheModel( "models/bots/knight/knight.mdl" ); +#endif + + PrecacheGibsForModel( model ); + + PrecacheModel( "models/weapons/c_models/c_bigsword/c_bigsword.mdl" ); + PrecacheModel( "models/weapons/c_models/c_bigshield/c_bigshield.mdl" ); + PrecacheModel( "models/weapons/c_models/c_big_mean_mother_hubbard/c_big_mean.mdl" ); + + PrecacheScriptSound( "Weapon_Sword.Swing" ); + PrecacheScriptSound( "Weapon_Sword.HitFlesh" ); + PrecacheScriptSound( "Weapon_Sword.HitWorld" ); + PrecacheScriptSound( "DemoCharge.HitWorld" ); + PrecacheScriptSound( "TFPlayer.Pain" ); + PrecacheScriptSound( "Halloween.HeadlessBossAttack" ); + PrecacheScriptSound( "RobotBoss.StunStart" ); + PrecacheScriptSound( "RobotBoss.Stunned" ); + PrecacheScriptSound( "RobotBoss.StunRecover" ); + PrecacheScriptSound( "RobotBoss.Acquire" ); + PrecacheScriptSound( "RobotBoss.Vocalize" ); + PrecacheScriptSound( "RobotBoss.Footstep" ); + PrecacheScriptSound( "RobotBoss.LaunchGrenades" ); + PrecacheScriptSound( "RobotBoss.LaunchRockets" ); + PrecacheScriptSound( "RobotBoss.Hurt" ); + PrecacheScriptSound( "RobotBoss.Vulnerable" ); + PrecacheScriptSound( "RobotBoss.ChargeUpNukeAttack" ); + PrecacheScriptSound( "RobotBoss.NukeAttack" ); + PrecacheScriptSound( "RobotBoss.Scanning" ); + PrecacheScriptSound( "RobotBoss.ReinforcementsArrived" ); + PrecacheScriptSound( "Cart.Explode" ); + + PrecacheParticleSystem( "asplode_hoodoo_embers" ); + PrecacheParticleSystem( "charge_up" ); + + PrecacheArmorParts(); +} + + +//----------------------------------------------------------------------------------------------------- +void CBotNPC::PrecacheArmorParts( void ) +{ + CUtlBuffer fileBuffer( 4096, 1024*1024, CUtlBuffer::TEXT_BUFFER ); + + // filename is local to game dir for Steam, so we need to prepend game dir + char gamePath[256]; + engine->GetGameDir( gamePath, 256 ); + + char filename[256]; + Q_snprintf( filename, sizeof( filename ), "%s\\models\\bots\\knight\\armor_parts.txt", gamePath ); + + if ( !filesystem->ReadFile( filename, "MOD", fileBuffer ) ) + { + Warning( "Unable to read %s\n", filename ); + } + else + { + while( true ) + { + char partName[256]; + + if ( fileBuffer.Scanf( "%s", partName ) <= 0 ) + { + break; + } + + // Make sure we have a valid string before trying to precache it. + if ( Q_strlen( partName ) > 0 ) + { + PrecacheModel( partName ); + } + } + } +} + + +//----------------------------------------------------------------------------------------------------- +void CBotNPC::InstallArmorParts( void ) +{ + if ( IsMiniBoss() ) + return; + + CUtlBuffer fileBuffer( 4096, 1024*1024, CUtlBuffer::TEXT_BUFFER ); + + // filename is local to game dir for Steam, so we need to prepend game dir + char gamePath[256]; + engine->GetGameDir( gamePath, 256 ); + + char filename[256]; + Q_snprintf( filename, sizeof( filename ), "%s\\models\\bots\\knight\\armor_parts.txt", gamePath ); + + if ( !filesystem->ReadFile( filename, "MOD", fileBuffer ) ) + { + Warning( "Unable to read %s\n", filename ); + } + else + { + while( true ) + { + char partName[256]; + + if ( fileBuffer.Scanf( "%s", partName ) <= 0 ) + { + break; + } + + CBaseAnimating *part = (CBaseAnimating *)CreateEntityByName( "prop_dynamic" ); + if ( part ) + { + part->SetModel( partName ); + + // bonemerge into our model + part->FollowEntity( this, true ); + + m_armorPartVector.AddToTail( part ); + } + } + } +} + + +//----------------------------------------------------------------------------------------------------- +void CBotNPC::Spawn( void ) +{ + BaseClass::Spawn(); + +#ifdef USE_BOSS_SENTRY + SetModel( "models/bots/boss_sentry/boss_sentry.mdl" ); +#else + SetModel( "models/bots/knight/knight.mdl" ); +#endif + + InstallArmorParts(); + + ModifyMaxHealth( tf_bot_npc_health.GetInt() ); + + // show Boss' health meter on HUD + if ( g_pMonsterResource ) + { + g_pMonsterResource->SetBossHealthPercentage( 1.0f ); + } + + m_damagePoseParameter = -1; + m_conditionFlags = 0; + + // randomize initial check + m_nearestVisibleEnemy = NULL; + m_nearestVisibleEnemyTimer.Start( RandomFloat( 0.0f, tf_bot_npc_reaction_time.GetFloat() ) ); + + m_homePos = GetAbsOrigin(); + + m_currentDamagePerSecond = 0.0f; + m_lastDamagePerSecond = 0.0f; + + m_attackTarget = NULL; + m_attackTargetTimer.Invalidate(); + m_isAttackTargetLocked = false; + + m_nukeTimer.Start( tf_bot_npc_nuke_interval.GetFloat() ); + m_isNuking = false; + + m_grenadeTimer.Start( GetGrenadeInterval() ); + m_ageTimer.Start(); + + ChangeTeam( TF_TEAM_RED ); + + TFGameRules()->SetActiveBoss( this ); +} + + +//----------------------------------------------------------------------------------------------------- +ConVar tf_bot_npc_dmg_mult_sniper( "tf_bot_npc_dmg_mult_sniper", "1.5"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_dmg_mult_minigun( "tf_bot_npc_dmg_mult_minigun", "0.5"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_dmg_mult_flamethrower( "tf_bot_npc_dmg_mult_flamethrower", "1"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_dmg_mult_sentrygun( "tf_bot_npc_dmg_mult_sentrygun", "0.5"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_dmg_mult_grenade( "tf_bot_npc_dmg_mult_grenade", "2"/*, FCVAR_CHEAT*/ ); + + +float ModifyBossDamage( const CTakeDamageInfo &info ) +{ + CTFWeaponBase *pWeapon = dynamic_cast< CTFWeaponBase * >( info.GetWeapon() ); + + if ( pWeapon ) + { + switch( pWeapon->GetWeaponID() ) + { + case TF_WEAPON_SNIPERRIFLE: + case TF_WEAPON_SNIPERRIFLE_DECAP: + case TF_WEAPON_SNIPERRIFLE_CLASSIC: + case TF_WEAPON_COMPOUND_BOW: + return info.GetDamage() * tf_bot_npc_dmg_mult_sniper.GetFloat(); + + case TF_WEAPON_MINIGUN: + return info.GetDamage() * tf_bot_npc_dmg_mult_minigun.GetFloat(); + + case TF_WEAPON_FLAMETHROWER: + return info.GetDamage() * tf_bot_npc_dmg_mult_flamethrower.GetFloat(); + + case TF_WEAPON_SENTRY_BULLET: + return info.GetDamage() * tf_bot_npc_dmg_mult_sentrygun.GetFloat(); + + case TF_WEAPON_GRENADE_DEMOMAN: + return info.GetDamage() * tf_bot_npc_dmg_mult_grenade.GetFloat(); + } + } + + // unmodified + return info.GetDamage(); +} + + +//----------------------------------------------------------------------------------------------------- +int CBotNPC::OnTakeDamage_Alive( const CTakeDamageInfo &rawInfo ) +{ + CTakeDamageInfo info = rawInfo; + + // don't take damage from myself + if ( info.GetAttacker() == this ) + { + return 0; + } + + if ( IsInCondition( INVULNERABLE ) ) + { + return 0; + } + + if ( IsInCondition( SHIELDED ) ) + { + // no damage from the front + CBaseEntity *inflictor = info.GetInflictor(); + if ( inflictor ) + { + Vector myForward; + GetVectors( &myForward, NULL, NULL ); + + Vector themForward; + inflictor->GetVectors( &themForward, NULL, NULL ); + + if ( DotProduct( themForward, myForward ) < -0.7071f ) + { + // blocked by my shield + EmitSound( "FX_RicochetSound.Ricochet" ); + DispatchParticleEffect( "asplode_hoodoo_embers", info.GetDamagePosition(), GetAbsAngles() ); + + return 0; + } + } + } + + + // weapon-specific damage modification + info.SetDamage( ModifyBossDamage( info ) ); + + + if ( IsInCondition( VULNERABLE_TO_STUN ) ) + { + // Heavies can't deal stun damage (too high DPS) + //CTFPlayer *playerAttacker = ToTFPlayer( info.GetAttacker() ); + + if ( true ) // !playerAttacker ) || !playerAttacker->IsPlayerClass( TF_CLASS_HEAVYWEAPONS ) ) + { + // track head damage when vulnerable + Vector headPos; + QAngle headAngles; + if ( GetAttachment( "head", headPos, headAngles ) ) + { + Vector damagePos = info.GetDamagePosition(); + +/* + const trace_t &pTrace = CBaseEntity::GetTouchTrace(); + damagePos = pTrace.endpos; +*/ + +/* + CBaseEntity *inflictor = info.GetInflictor(); + if ( inflictor ) + { + damagePos = inflictor->GetAbsOrigin() + 3.0f * gpGlobals->frametime * inflictor->GetAbsVelocity(); + } +*/ + + if ( tf_bot_npc_debug_damage.GetBool() ) + { + NDebugOverlay::Cross3D( headPos, 5.0f, 255, 0, 0, true, 5.0f ); + NDebugOverlay::Cross3D( damagePos, 5.0f, 0, 255, 0, true, 5.0f ); + NDebugOverlay::Line( damagePos, headPos, 255, 255, 0, true, 5.0f ); + } + + bool isHeadHit = ( damagePos - headPos ).IsLengthLessThan( tf_bot_npc_head_radius.GetFloat() ); + + if ( isHeadHit ) + { + // hit the head + AccumulateStunDamage( info.GetDamage() ); + DispatchParticleEffect( "asplode_hoodoo_embers", info.GetDamagePosition(), GetAbsAngles() ); + + if ( tf_bot_npc_debug_damage.GetBool() ) + { + DevMsg( "Stun dmg = %f\n", GetStunDamage() ); + NDebugOverlay::Circle( headPos, tf_bot_npc_head_radius.GetFloat(), 255, 0, 0, 255, true, 5.0f ); + } + } + else if ( tf_bot_npc_debug_damage.GetBool() ) + { + NDebugOverlay::Circle( headPos, tf_bot_npc_head_radius.GetFloat(), 255, 255, 0, 255, true, 5.0f ); + } + } + } + } + + // take extra damage when stunned + if ( IsInCondition( STUNNED ) ) + { + info.SetDamage( info.GetDamage() * tf_bot_npc_stunned_injury_multiplier.GetFloat() ); + + if ( m_ouchTimer.IsElapsed() ) + { + m_ouchTimer.Start( 1.0f ); + EmitSound( "RobotBoss.Hurt" ); + } + } + else if ( info.GetDamageType() & DMG_CRITICAL ) + { + // do the critical damage increase + info.SetDamage( info.GetDamage() * TF_DAMAGE_CRIT_MULTIPLIER ); + } + + + // keep a list of everyone who hurt me, and when + if ( info.GetAttacker() && info.GetAttacker()->MyCombatCharacterPointer() && !InSameTeam( info.GetAttacker() ) ) + { + CBaseCombatCharacter *attacker = info.GetAttacker()->MyCombatCharacterPointer(); + + // sentry guns are first class attackers + if ( info.GetInflictor() ) + { + CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() ); + if ( sentry ) + { + attacker = sentry; + } + } + + RememberAttacker( attacker, info.GetDamage(), ( info.GetDamageType() & DMG_CRITICAL ) ? true : false ); + + CTFPlayer *playerAttacker = ToTFPlayer( attacker ); + if ( playerAttacker ) + { + for( int i=0; i<playerAttacker->m_Shared.GetNumHealers(); ++i ) + { + CTFPlayer *medic = ToTFPlayer( playerAttacker->m_Shared.GetHealerByIndex( i ) ); + if ( medic ) + { + // medics healing my attacker are also considered attackers + RememberAttacker( medic, 0, 0 ); + } + } + } + + // if we don't have an attack target yet, we do now + if ( !HasAttackTarget() ) + { + SetAttackTarget( attacker ); + } + } + + EmitSound( "TFPlayer.Pain" ); + + // 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 ); + } + + int result = BaseClass::OnTakeDamage_Alive( info ); + + if ( g_pMonsterResource ) + { + g_pMonsterResource->SetBossHealthPercentage( (float)GetHealth() / (float)GetMaxHealth() ); + } + + return result; +} + + +//--------------------------------------------------------------------------------------------- +// Returns true if we're in a condition that means we can't start another action +bool CBotNPC::IsBusy( void ) const +{ + return IsInCondition( (Condition)( CHARGING | STUNNED | VULNERABLE_TO_STUN | BUSY ) ); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPC::RememberAttacker( CBaseCombatCharacter *attacker, float damage, bool wasCritical ) +{ + AttackerInfo attackerInfo; + + attackerInfo.m_attacker = attacker; + attackerInfo.m_timestamp = gpGlobals->curtime; + attackerInfo.m_damage = damage; + attackerInfo.m_wasCritical = wasCritical; + + m_attackerVector.AddToHead( attackerInfo ); +} + + +//---------------------------------------------------------------------------------- +CTFPlayer *CBotNPC::GetClosestMinionPrisoner( void ) +{ + CUtlVector< CBotNPCMinion * > minionVector; + CBotNPCMinion *minion = NULL; + while( ( minion = (CBotNPCMinion *)gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL ) + { + minionVector.AddToTail( minion ); + } + + CTFPlayer *closeCapture = NULL; + float captureRangeSq = FLT_MAX; + + for( int m=0; m<minionVector.Count(); ++m ) + { + minion = minionVector[m]; + + if ( minion->HasTarget() ) + { + CTFPlayer *victim = minion->GetTarget(); + if ( victim->m_Shared.InCond( TF_COND_STUNNED ) ) + { + // they've got one! + float rangeSq = GetRangeSquaredTo( victim ); + if ( rangeSq < captureRangeSq ) + { + closeCapture = victim; + captureRangeSq = rangeSq; + } + } + } + } + + return closeCapture; +} + + +//---------------------------------------------------------------------------------- +bool CBotNPC::IsPrisonerOfMinion( CBaseCombatCharacter *victim ) +{ + if ( !victim->IsPlayer() ) + { + return false; + } + + CUtlVector< CBotNPCMinion * > minionVector; + CBotNPCMinion *minion = NULL; + while( ( minion = (CBotNPCMinion *)gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL ) + { + minionVector.AddToTail( minion ); + } + + for( int m=0; m<minionVector.Count(); ++m ) + { + minion = minionVector[m]; + + if ( minion->HasTarget() && minion->GetTarget() == victim ) + { + if ( minion->GetTarget()->m_Shared.InCond( TF_COND_STUNNED ) ) + { + return true; + } + } + } + + return false; +} + + +//---------------------------------------------------------------------------------- +void CBotNPC::UpdateDamagePerSecond( void ) +{ + m_lastDamagePerSecond = m_currentDamagePerSecond; + + m_currentDamagePerSecond = 0.0f; + + const float windowDuration = 10.0f; // 5.0f; + int i; + + m_threatVector.RemoveAll(); + + for( i=0; i<m_attackerVector.Count(); ++i ) + { + float age = gpGlobals->curtime - m_attackerVector[i].m_timestamp; + + if ( age > windowDuration ) + { + // too old + break; + } + + float decayedDamage = ( ( windowDuration - age ) / windowDuration ) * m_attackerVector[i].m_damage; + + m_currentDamagePerSecond += decayedDamage; + + CBaseCombatCharacter *attacker = m_attackerVector[i].m_attacker; + + if ( attacker && attacker->IsAlive() ) + { + int j; + for( j=0; j<m_threatVector.Count(); ++j ) + { + if ( m_threatVector[j].m_who == attacker ) + { + m_threatVector[j].m_threat += decayedDamage; + break; + } + } + + if ( j >= m_threatVector.Count() ) + { + // new threat + ThreatInfo threat; + threat.m_who = attacker; + threat.m_threat = decayedDamage; + m_threatVector.AddToTail( threat ); + } + } + } + +// if ( m_currentDamagePerSecond > 0.0001f ) +// { +// DevMsg( "%3.2f: dps = %3.2f\n", gpGlobals->curtime, m_currentDamagePerSecond ); +// } +} + + +//---------------------------------------------------------------------------------- +const CBotNPC::ThreatInfo *CBotNPC::GetMaxThreat( void ) const +{ + int maxThreatIndex = -1; + + for( int i=0; i<m_threatVector.Count(); ++i ) + { + if ( maxThreatIndex < 0 || m_threatVector[i].m_threat > m_threatVector[ maxThreatIndex ].m_threat ) + { + maxThreatIndex = i; + } + } + + if ( maxThreatIndex < 0 ) + { + // no threat yet + return NULL; + } + + return &m_threatVector[ maxThreatIndex ]; +} + + +//---------------------------------------------------------------------------------- +const CBotNPC::ThreatInfo *CBotNPC::GetThreat( CBaseCombatCharacter *who ) const +{ + for( int i=0; i<m_threatVector.Count(); ++i ) + { + if ( m_threatVector[i].m_who == who ) + { + return &m_threatVector[i]; + } + } + + return NULL; +} + + +//---------------------------------------------------------------------------------- +void CBotNPC::UpdateAttackTarget( void ) +{ + if ( m_isAttackTargetLocked && HasAttackTarget() ) + { + return; + } + + // who is most dangerous to me at the moment + const ThreatInfo *maxThreat = GetMaxThreat(); + + if ( !maxThreat ) + { + // nobody is hurting me at the moment + + if ( HasAttackTarget() ) + { + // stay focused on current target + return; + } + + // we have no current target, either + + // if my minions have captured someone, go get them + CTFPlayer *closeCapture = GetClosestMinionPrisoner(); + if ( closeCapture ) + { + SetAttackTarget( closeCapture ); + return; + } + + // if we see an enemy, attack them + CBaseCombatCharacter *visible = GetNearestVisibleEnemy(); + if ( visible ) + { + SetAttackTarget( visible ); + } + + return; + } + + // we are under attack, if we don't have a target, attack the highest threat + if ( !HasAttackTarget() ) + { + SetAttackTarget( maxThreat->m_who ); + return; + } + + if ( IsAttackTarget( maxThreat->m_who ) ) + { + // our current target is still dealing the most damage to us + return; + } + + // switch to new threat if is is more dangerous + const ThreatInfo *attackTargetThreat = GetThreat( GetAttackTarget() ); + + if ( !attackTargetThreat || maxThreat->m_threat > attackTargetThreat->m_threat + tf_bot_npc_threat_tolerance.GetFloat() ) + { + // change threats + SetAttackTarget( maxThreat->m_who ); + } +} + + +//---------------------------------------------------------------------------------- +void CBotNPC::RemoveCondition( Condition c ) +{ + if ( c == STUNNED ) + { + // reset the accumulator + ClearStunDamage(); + } + + m_conditionFlags &= ~c; +} + + +//---------------------------------------------------------------------------------- +void CBotNPC::SwingAxe( void ) +{ + if ( !IsSwingingAxe() ) + { + AddGesture( ACT_MP_ATTACK_STAND_ITEM1 ); + m_axeSwingTimer.Start( 0.58f ); + EmitSound( "Weapon_Sword.Swing" ); + } +} + + +//---------------------------------------------------------------------------------- +void CBotNPC::UpdateAxeSwing( void ) +{ + if ( !m_axeSwingTimer.HasStarted() ) + { + return; + } + + // continue axe swing + if ( !m_axeSwingTimer.IsElapsed() ) + { + return; + } + + // moment of impact - did axe swing hit? + m_axeSwingTimer.Invalidate(); + + CBaseCombatCharacter *victim = GetAttackTarget(); + + if ( victim ) + { + Vector forward; + GetVectors( &forward, NULL, NULL ); + + Vector toVictim = victim->WorldSpaceCenter() - WorldSpaceCenter(); + toVictim.NormalizeInPlace(); + + if ( DotProduct( forward, toVictim ) > 0.7071f ) + { + if ( IsRangeLessThan( victim, 0.9f * tf_bot_npc_attack_range.GetFloat() ) ) + { + if ( IsLineOfSightClear( victim ) ) + { + // CHOP! + CTakeDamageInfo info( this, this, tf_bot_npc_melee_damage.GetFloat(), DMG_SLASH, TF_DMG_CUSTOM_NONE ); + CalculateMeleeDamageForce( &info, toVictim, WorldSpaceCenter(), 1.0f ); + victim->TakeDamage( info ); + EmitSound( "Weapon_Sword.HitFlesh" ); + return; + } + } + } + } + + EmitSound( "Weapon_Sword.HitWorld" ); +} + + +//---------------------------------------------------------------------------------- +bool CBotNPC::IsSwingingAxe( void ) const +{ + return const_cast< CBotNPC * >( this )->IsPlayingGesture( ACT_MP_ATTACK_STAND_ITEM1 ); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPC::Update( void ) +{ + BaseClass::Update(); + + UpdateNearestVisibleEnemy(); + UpdateAxeSwing(); + UpdateDamagePerSecond(); + UpdateAttackTarget(); + + if ( m_damagePoseParameter < 0 ) + { + m_damagePoseParameter = LookupPoseParameter( "damage" ); + } + + if ( m_damagePoseParameter >= 0 ) + { + SetPoseParameter( m_damagePoseParameter, 1.0f - ( (float)GetHealth() / (float)GetMaxHealth() ) ); + } + + // chase down players who taunt me + if ( m_hateTauntTimer.IsElapsed() ) + { + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS ); + + for( int i=0; i<playerVector.Count(); ++i ) + { + if ( playerVector[i]->IsTaunting() ) + { + m_hateTauntTimer.Start( tf_bot_npc_hate_taunt_cooldown.GetFloat() ); + + if ( IsLineOfSightClear( playerVector[i], IGNORE_ACTORS ) ) + { + // the taunter becomes our new attack target + SetAttackTarget( playerVector[i], tf_bot_npc_hate_taunt_cooldown.GetFloat() ); + } + } + } + } +} + + +//--------------------------------------------------------------------------------------------- +bool CBotNPC::IsPotentiallyChaseable( CTFPlayer *victim ) +{ + if ( !victim ) + { + return false; + } + + if ( !victim->IsAlive() ) + { + // victim is dead - pick a new one + return false; + } + + CTFNavArea *victimArea = (CTFNavArea *)victim->GetLastKnownArea(); + if ( !victimArea || victimArea->HasAttributeTF( TF_NAV_SPAWN_ROOM_BLUE | TF_NAV_SPAWN_ROOM_RED ) ) + { + // unreachable - pick a new victim + return false; + } + + if ( victim->GetGroundEntity() != NULL ) + { + Vector victimAreaPos; + victimArea->GetClosestPointOnArea( victim->GetAbsOrigin(), &victimAreaPos ); + if ( ( victim->GetAbsOrigin() - victimAreaPos ).AsVector2D().IsLengthGreaterThan( 50.0f ) ) + { + // off the mesh and unreachable - pick a new victim + return false; + } + } + + if ( victim->m_Shared.IsInvulnerable() ) + { + // invulnerable - pick a new victim + return false; + } + + Vector toHome = m_homePos - victim->GetAbsOrigin(); + if ( toHome.IsLengthGreaterThan( tf_bot_npc_quit_range.GetFloat() ) ) + { + // too far from home - pick a new victim + return false; + } + + return true; +} + + +//--------------------------------------------------------------------------------------------- +bool CBotNPC::IsIgnored( CTFPlayer *player ) const +{ + if ( player->m_Shared.IsStealthed() ) + { + if ( player->m_Shared.GetPercentInvisible() < 0.75f ) + { + // spy is partially cloaked, and therefore attracts our attention + return false; + } + + if ( player->m_Shared.InCond( TF_COND_BURNING ) || + player->m_Shared.InCond( TF_COND_URINE ) || + player->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) || + player->m_Shared.InCond( TF_COND_BLEEDING ) ) + { + // always notice players with these conditions + return false; + } + + // invisible! + return true; + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPC::UpdateNearestVisibleEnemy( void ) +{ + if ( !m_nearestVisibleEnemyTimer.IsElapsed() ) + { + return; + } + + m_nearestVisibleEnemyTimer.Start( tf_bot_npc_reaction_time.GetFloat() ); + + // collect everyone + CUtlVector< CTFPlayer * > playerVector; + //CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + Vector myForward; + GetVectors( &myForward, NULL, NULL ); + + m_nearestVisibleEnemy = NULL; + float victimRangeSq = FLT_MAX; + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *victim = playerVector[i]; + + if ( IsIgnored( victim ) ) + { + continue; + } + + float rangeSq = GetRangeSquaredTo( playerVector[i] ); + if ( rangeSq < victimRangeSq ) + { + // FOV check + Vector to = playerVector[i]->WorldSpaceCenter() - WorldSpaceCenter(); + to.NormalizeInPlace(); + + if ( DotProduct( to, myForward ) > -0.7071f ) + { + if ( IsLineOfSightClear( playerVector[i] ) ) + { + m_nearestVisibleEnemy = playerVector[i]; + victimRangeSq = rangeSq; + } + } + } + } +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPC::SetAttackTarget( CBaseCombatCharacter *target, float duration ) +{ + if ( target && m_attackTarget != NULL && m_attackTarget->IsAlive() && m_attackTargetTimer.HasStarted() && !m_attackTargetTimer.IsElapsed() ) + { + // can't switch away from our still valid target yet + return; + } + + if ( m_attackTarget != target ) + { + if ( target ) + { + EmitSound( "RobotBoss.Acquire" ); + AddGesture( ACT_MP_GESTURE_FLINCH_CHEST ); + } + + TFGameRules()->SetIT( m_attackTarget ); + + m_attackTarget = target; + } + + if ( duration > 0.0f ) + { + m_attackTargetTimer.Start( duration ); + } + else + { + m_attackTargetTimer.Invalidate(); + } +} + + +//--------------------------------------------------------------------------------------------- +CBaseCombatCharacter *CBotNPC::GetAttackTarget( void ) const +{ + if ( m_attackTarget != NULL && m_attackTarget->IsAlive() ) + { + return m_attackTarget; + } + + return NULL; +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPC::Break( void ) +{ + CPVSFilter filter( GetAbsOrigin() ); + UserMessageBegin( filter, "BreakModel" ); + WRITE_SHORT( GetModelIndex() ); + WRITE_VEC3COORD( GetAbsOrigin() ); + WRITE_ANGLES( GetAbsAngles() ); + WRITE_SHORT( GetSkin() ); + MessageEnd(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPC::CollectPlayersStandingOnMe( CUtlVector< CTFPlayer * > *playerVector ) +{ + CUtlVector< CTFPlayer * > allPlayerVector; + CollectPlayers( &allPlayerVector, TEAM_ANY, COLLECT_ONLY_LIVING_PLAYERS ); + + for( int i=0; i<allPlayerVector.Count(); ++i ) + { + CTFPlayer *player = allPlayerVector[i]; + + if ( player->GetGroundEntity() == this ) + { + playerVector->AddToTail( player ); + } + } +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCStunned : public Action< CBotNPC > +{ +public: + CBotNPCStunned( float duration, Action< CBotNPC > *nextAction = NULL ); + + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + virtual void OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ); + + virtual EventDesiredResult< CBotNPC > OnInjured( CBotNPC *me, const CTakeDamageInfo &info ); + + virtual const char *GetName( void ) const { return "Stunned"; } // return name of this action + +private: + CountdownTimer m_timer; + enum StunStateType + { + BECOMING_STUNNED, + STUNNED, + RECOVERING + } + m_state; + int m_layerUsed; + + Action< CBotNPC > *m_nextAction; +}; + + +//--------------------------------------------------------------------------------------------- +CBotNPCStunned::CBotNPCStunned( float duration, Action< CBotNPC > *nextAction ) +{ + m_timer.Start( duration ); + m_nextAction = nextAction; +} + + +//--------------------------------------------------------------------------------------------- +ConVar tf_bot_npc_stun_ammo_count( "tf_bot_npc_stun_ammo_count", "3"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_stun_ammo_amount( "tf_bot_npc_stun_ammo_amount", "100"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_stun_ammo_velocity( "tf_bot_npc_stun_ammo_velocity", "100"/*, FCVAR_CHEAT*/ ); + +void TossAmmoPack( CBotNPC *me ) +{ + int iPrimary = tf_bot_npc_stun_ammo_amount.GetInt(); + int iSecondary = tf_bot_npc_stun_ammo_amount.GetInt(); + int iMetal = tf_bot_npc_stun_ammo_amount.GetInt(); + + // Create the ammo pack. + CTFAmmoPack *pAmmoPack = CTFAmmoPack::Create( me->GetAbsOrigin(), me->GetAbsAngles(), NULL, "models/items/ammopack_medium.mdl" ); + if ( pAmmoPack ) + { +/* + Vector vel; + + vel.x = RandomFloat( -1.0f, 1.0f ) * tf_bot_npc_stun_ammo_velocity.GetFloat(); + vel.y = RandomFloat( -1.0f, 1.0f ) * tf_bot_npc_stun_ammo_velocity.GetFloat(); + vel.z = tf_bot_npc_stun_ammo_velocity.GetFloat(); + + pAmmoPack->SetInitialVelocity( vel ); +*/ + pAmmoPack->m_nSkin = 0; + + // 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 ); + + pAmmoPack->ApplyLocalAngularVelocityImpulse( AngularImpulse( 600, random->RandomInt( -1200, 1200 ), 0 ) ); + + 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 ); + } +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCStunned::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + // start animation + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_MELEE ); + m_layerUsed = me->AddLayeredSequence( me->LookupSequence( "PRIMARY_Stun_begin" ), 0 ); + m_state = BECOMING_STUNNED; + + m_timer.Reset(); + + me->AddCondition( CBotNPC::STUNNED ); + me->EmitSound( "RobotBoss.StunStart" ); + + // throw out some ammo + for( int i=0; i<tf_bot_npc_stun_ammo_count.GetInt(); ++i ) + { + TossAmmoPack( me ); + } + + me->m_outputOnStunned.FireOutput( me, me ); + + // relay the event to the map logic + CTFSpawnerBoss *spawner = me->GetSpawner(); + if ( spawner ) + { + spawner->OnBotStunned( me ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCStunned::Update( CBotNPC *me, float interval ) +{ + switch( m_state ) + { + case BECOMING_STUNNED: + if ( me->IsSequenceFinished() ) + { + me->FastRemoveLayer( m_layerUsed ); + + m_state = STUNNED; + m_layerUsed = me->AddLayeredSequence( me->LookupSequence( "PRIMARY_stun_middle" ), 0 ); + me->SetLayerLooping( m_layerUsed, true ); + me->EmitSound( "RobotBoss.Stunned" ); + } + break; + + case STUNNED: + if ( m_timer.IsElapsed() ) + { + me->FastRemoveLayer( m_layerUsed ); + + m_state = RECOVERING; + m_layerUsed = me->AddLayeredSequence( me->LookupSequence( "PRIMARY_stun_end" ), 0 ); + me->StopSound( "RobotBoss.Stunned" ); + me->EmitSound( "RobotBoss.StunRecover" ); + } + break; + + case RECOVERING: + if ( me->IsSequenceFinished() ) + { + me->FastRemoveLayer( m_layerUsed ); + + if ( m_nextAction ) + { + return ChangeTo( m_nextAction, "Stun finished" ); + } + + return Done( "Stun finished" ); + } + break; + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBotNPC > CBotNPCStunned::OnInjured( CBotNPC *me, const CTakeDamageInfo &info ) +{ + return TryToSustain( RESULT_CRITICAL ); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCStunned::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) +{ + me->RemoveCondition( CBotNPC::STUNNED ); + + if ( me->HasAbility( CBotNPC::CAN_ENRAGE ) ) + { + // being stunned makes the boss ANGRY! + me->AddCondition( CBotNPC::ENRAGED ); + } + + // make sure the boss attacks at least once before he starts a nuke + if ( me->GetNukeTimer()->GetRemainingTime() < tf_bot_npc_min_nuke_after_stun_time.GetFloat() ) + { + me->GetNukeTimer()->Start( tf_bot_npc_min_nuke_after_stun_time.GetFloat() ); + } +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCBigJump : public Action< CBotNPC > +{ +public: + CBotNPCBigJump( const Vector &destination, Action< CBotNPC > *nextAction = NULL ); + + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + virtual void OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ); + + virtual EventDesiredResult< CBotNPC > OnInjured( CBotNPC *me, const CTakeDamageInfo &info ); + + virtual const char *GetName( void ) const { return "Jump"; } // return name of this action + +private: + enum StunStateType + { + JUMPING_UP, + FLOATING_UP, + FALLING_DOWN + } + m_state; + + CountdownTimer m_timer; + Vector m_destination; + + Action< CBotNPC > *m_nextAction; +}; + + +//--------------------------------------------------------------------------------------------- +CBotNPCBigJump::CBotNPCBigJump( const Vector &destination, Action< CBotNPC > *nextAction ) +{ + m_destination = destination; + m_nextAction = nextAction; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCBigJump::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + // start animation + me->GetBodyInterface()->StartActivity( ACT_MP_JUMP_START_MELEE ); + m_state = JUMPING_UP; + m_timer.Start( 3.0f ); + + // disconnect us from the ground + me->GetLocomotionInterface()->Jump(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCBigJump::Update( CBotNPC *me, float interval ) +{ + // animation state + switch( m_state ) + { + case JUMPING_UP: + if ( me->IsSequenceFinished() ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_JUMP_FLOAT_MELEE ); + m_state = FLOATING_UP; + } + break; + } + + // movement + switch( m_state ) + { + case JUMPING_UP: + case FLOATING_UP: + me->GetLocomotionInterface()->SetVelocity( Vector( 0, 0, 1200.0f ) ); + + if ( m_timer.IsElapsed() ) + { + m_state = FALLING_DOWN; + + // move so we fall on our destination point + me->SetAbsOrigin( m_destination + Vector( 0, 0, 1300.0f ) ); + me->GetLocomotionInterface()->SetVelocity( vec3_origin ); + } + break; + + case FALLING_DOWN: + if ( me->GetLocomotionInterface()->IsOnGround() ) + { + me->AddGesture( ACT_MP_JUMP_LAND_MELEE ); + + if ( m_nextAction ) + { + return ChangeTo( m_nextAction, "Finished jump" ); + } + + return Done( "Finished jump" ); + } + break; + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBotNPC > CBotNPCBigJump::OnInjured( CBotNPC *me, const CTakeDamageInfo &info ) +{ + return TryToSustain( RESULT_CRITICAL ); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCBigJump::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) +{ +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCLaunchMinions : public Action< CBotNPC > +{ +public: + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + + // if anything interrupts this action, abort it + virtual ActionResult< CBotNPC > OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction ) { return Done(); } + + virtual const char *GetName( void ) const { return "LaunchMinions"; } // return name of this action + +private: + CountdownTimer m_timer; + int m_minionsLeft; + + bool SpawnMinion( CBotNPC *me ); +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCLaunchMinions::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + // start animation + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_SECONDARY ); + + me->AddGestureSequence( me->LookupSequence( "taunt01" ) ); + + m_timer.Start( 4.0f ); + + int bonus = (int)( me->GetAge() / tf_bot_npc_minion_launch_count_increase_interval.GetFloat() ); + m_minionsLeft = tf_bot_npc_minion_launch_count_initial.GetInt() + bonus; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +bool CBotNPCLaunchMinions::SpawnMinion( CBotNPC *me ) +{ + Vector spawnSpot = me->WorldSpaceCenter(); + + Vector headPos; + QAngle headAngles; + if ( me->GetAttachment( "head", headPos, headAngles ) ) + { + spawnSpot = headPos + RandomVector( -10.0f, 10.0f ); + } + + CBaseCombatCharacter *minion = static_cast< CBaseCombatCharacter * >( CreateEntityByName( "bot_npc_minion" ) ); + if ( minion ) + { + minion->SetAbsAngles( me->GetAbsAngles() ); + minion->SetAbsOrigin( spawnSpot ); + minion->SetOwnerEntity( me ); + + DispatchSpawn( minion ); + + return true; + } + + return false; +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCLaunchMinions::Update( CBotNPC *me, float interval ) +{ + CBaseCombatCharacter *target = me->GetAttackTarget(); + + if ( target ) + { + me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() ); + } + + if ( m_timer.IsElapsed() ) + { + while( m_minionsLeft-- ) + { + SpawnMinion( me ); + } + + return Done(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCNukeAttack : public Action< CBotNPC > +{ +public: + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + virtual void OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ); + + virtual EventDesiredResult< CBotNPC > OnInjured( CBotNPC *me, const CTakeDamageInfo &info ); + + virtual const char *GetName( void ) const { return "NukeAttack"; } // return name of this action + +private: + CountdownTimer m_shakeTimer; + CountdownTimer m_chargeUpTimer; +}; + +ConVar tf_bot_npc_nuke_damage( "tf_bot_npc_nuke_damage", "75"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_nuke_max_remaining_health( "tf_bot_npc_nuke_max_remaining_health", "60"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_nuke_afterburn_time( "tf_bot_npc_nuke_afterburn_time", "5"/*, FCVAR_CHEAT*/ ); + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCNukeAttack::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_MP_JUMP_FLOAT_LOSERSTATE ); + me->StartNukeEffect(); + + me->EmitSound( "RobotBoss.ChargeUpNukeAttack" ); + me->AddCondition( CBotNPC::VULNERABLE_TO_STUN ); + + m_chargeUpTimer.Start( tf_bot_npc_nuke_charge_time.GetFloat() ); + m_shakeTimer.Start( 0.25f ); + + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCNukeAttack::Update( CBotNPC *me, float interval ) +{ + float stunRatio = me->GetStunDamage() / me->GetBecomeStunnedDamage(); + + if ( me->HasAbility( CBotNPC::CAN_BE_STUNNED ) && stunRatio >= 1.0f ) + { + return ChangeTo( new CBotNPCStunned( tf_bot_npc_stunned_duration.GetFloat() ), "They got me" ); + } + + // update the client's HUD + if ( g_pMonsterResource ) + { + g_pMonsterResource->SetBossStunPercentage( 1.0f - stunRatio ); + } + + if ( m_shakeTimer.IsElapsed() ) + { + m_shakeTimer.Reset(); + UTIL_ScreenShake( me->GetAbsOrigin(), 15.0f, 5.0f, 1.0f, 3000.0f, SHAKE_START ); + } + + if ( m_chargeUpTimer.IsElapsed() ) + { + // BLAST! + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + me->EmitSound( "RobotBoss.NukeAttack" ); + + CUtlVector< CBaseCombatCharacter * > victimVector; + + int i; + + // players + for ( i=0; i<playerVector.Count(); ++i ) + { + CBasePlayer *player = playerVector[i]; + + if ( player && player->IsAlive() && player->GetTeamNumber() == TF_TEAM_BLUE ) + { + victimVector.AddToTail( player ); + } + } + + // objects + CTFTeam *team = GetGlobalTFTeam( TF_TEAM_BLUE ); + if ( team ) + { + for ( i=0; i<team->GetNumObjects(); ++i ) + { + CBaseObject *object = team->GetObject( i ); + if ( object ) + { + victimVector.AddToTail( object ); + } + } + } + +#ifdef SKIPME + team = GetGlobalTFTeam( TF_TEAM_RED ); + if ( team ) + { + for ( i=0; i<team->GetNumObjects(); ++i ) + { + CBaseObject *object = team->GetObject( i ); + if ( object ) + { + victimVector.AddToTail( object ); + } + } + } + + // non-player bots + CUtlVector< INextBot * > botVector; + TheNextBots().CollectAllBots( &botVector ); + for( i=0; i<botVector.Count(); ++i ) + { + CBaseCombatCharacter *bot = botVector[i]->GetEntity(); + + if ( !bot->IsPlayer() && bot->IsAlive() ) + { + victimVector.AddToTail( bot ); + } + } +#endif // SKIPME + + for( int i=0; i<victimVector.Count(); ++i ) + { + CBaseCombatCharacter *victim = victimVector[i]; + + if ( me->IsSelf( victim ) ) + continue; + + if ( me->IsLineOfSightClear( victim ) ) + { + Vector toVictim = victim->WorldSpaceCenter() - me->WorldSpaceCenter(); + toVictim.NormalizeInPlace(); + + float damage = tf_bot_npc_nuke_damage.GetFloat(); + + if ( me->GetAge() > tf_bot_npc_nuke_lethal_time.GetFloat() ) + { + // nuke is now lethal + damage = 999.9f; + } + else if ( tf_bot_npc_nuke_max_remaining_health.GetFloat() >= 0.0f ) + { + // nuke slams everyone's health to this + if ( victim->GetHealth() > tf_bot_npc_nuke_max_remaining_health.GetFloat() ) + { + damage = victim->GetHealth() - tf_bot_npc_nuke_max_remaining_health.GetFloat(); + } + } + + CTakeDamageInfo info( me, me, damage, DMG_ENERGYBEAM, TF_DMG_CUSTOM_NONE ); + CalculateMeleeDamageForce( &info, toVictim, me->WorldSpaceCenter(), 1.0f ); + victim->TakeDamage( info ); + + if ( victim->IsPlayer() ) + { + CTFPlayer *playerVictim = ToTFPlayer( victim ); + + // catch them on fire (unless they are a Pyro) + if ( !playerVictim->IsPlayerClass( TF_CLASS_PYRO ) ) + { + playerVictim->m_Shared.Burn( me, tf_bot_npc_nuke_afterburn_time.GetFloat() ); + } + + color32 colorHit = { 255, 255, 255, 255 }; + UTIL_ScreenFade( victim, colorHit, 1.0f, 0.1f, FFADE_IN ); + } + } + } + + return Done(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCNukeAttack::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) +{ + me->RemoveCondition( CBotNPC::VULNERABLE_TO_STUN ); + me->StopNukeEffect(); + me->ClearStunDamage(); + me->GetNukeTimer()->Start( tf_bot_npc_nuke_interval.GetFloat() ); + + if ( g_pMonsterResource ) + { + g_pMonsterResource->HideBossStunMeter(); + } +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBotNPC > CBotNPCNukeAttack::OnInjured( CBotNPC *me, const CTakeDamageInfo &info ) +{ + return TryToSustain( RESULT_CRITICAL ); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCLaunchRockets : public Action< CBotNPC > +{ +public: + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + virtual void OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ); + + // if anything interrupts this action, abort it + virtual ActionResult< CBotNPC > OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction ) { return Done(); } + + virtual const char *GetName( void ) const { return "LaunchRockets"; } // return name of this action + +private: + CountdownTimer m_timer; + + CountdownTimer m_launchTimer; + int m_rocketsLeft; + + int m_animLayer; + + CHandle< CBaseCombatCharacter > m_target; + Vector m_lastTargetPosition; +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCLaunchRockets::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + // start animation + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_SECONDARY ); + + m_animLayer = me->AddLayeredSequence( me->LookupSequence( "taunt02" ), 0 ); + + m_timer.Start( 1.0f ); + + m_rocketsLeft = me->GetRocketLaunchCount(); + + me->AddCondition( CBotNPC::BUSY ); + me->LockAttackTarget(); + + me->EmitSound( "RobotBoss.LaunchRockets" ); + + if ( me->GetAttackTarget() == NULL ) + { + return Done( "No target" ); + } + + m_target = me->GetAttackTarget(); + m_lastTargetPosition = m_target->WorldSpaceCenter(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCLaunchRockets::Update( CBotNPC *me, float interval ) +{ + if ( m_target != NULL ) + { + m_lastTargetPosition = m_target->WorldSpaceCenter(); + } + + me->GetLocomotionInterface()->FaceTowards( m_lastTargetPosition ); + + if ( m_timer.IsElapsed() && m_launchTimer.IsElapsed() ) + { + if ( !m_rocketsLeft ) + { + return Done(); + } + + --m_rocketsLeft; + m_launchTimer.Start( me->GetRocketInterval() ); + + QAngle launchAngles = me->GetAbsAngles(); + + if ( m_target == NULL ) + { + Vector to = m_lastTargetPosition - me->WorldSpaceCenter(); + VectorAngles( to, launchAngles ); + } + else + { + float range = me->GetRangeTo( m_target->EyePosition() ); + + const float rocketSpeed = me->GetRocketAimError() * 1100.0f; // 2000.0f; // 1100.0f; nerfing accuracy + float flightTime = range / rocketSpeed; + + Vector aimSpot = m_target->EyePosition() + m_target->GetAbsVelocity() * flightTime; + + Vector to = aimSpot - me->WorldSpaceCenter(); + VectorAngles( to, launchAngles ); + } + + CTFProjectile_Rocket *pRocket = CTFProjectile_Rocket::Create( me, me->WorldSpaceCenter(), launchAngles, me, me ); + if ( pRocket ) + { + if ( me->IsInCondition( CBotNPC::ENRAGED ) ) + { + pRocket->SetCritical( true ); + pRocket->EmitSound( "Weapon_RPG.SingleCrit" ); + } + else + { + me->EmitSound( me->GetRocketSoundEffect() ); + } + + pRocket->SetDamage( me->GetRocketDamage() ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCLaunchRockets::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) +{ + me->RemoveCondition( CBotNPC::ENRAGED ); + me->RemoveCondition( CBotNPC::BUSY ); + me->FastRemoveLayer( m_animLayer ); + me->UnlockAttackTarget(); +} + + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCRush : public Action< CBotNPC > +{ +public: + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + virtual void OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ); + + // if anything interrupts this action, abort it + virtual ActionResult< CBotNPC > OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction ) { return Done(); } + + virtual EventDesiredResult< CBotNPC > OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result = NULL ); + + virtual const char *GetName( void ) const { return "Rush"; } // return name of this action + +private: + CountdownTimer m_timer; + Vector m_chargeOrigin; + float m_maxAttainedSpeed; + float m_lastSpeed; + bool m_didHitVictim; +}; + + +//--------------------------------------------------------------------------------------------- +void PushawayPlayer( CTFPlayer *victim, const Vector &pushOrigin, float pushForce ) +{ + if ( !victim ) + return; + + if ( victim->GetFlags() & FL_ONGROUND ) + { + // launching into the air + victim->SetAbsVelocity( vec3_origin ); + + const float stunTime = 0.5f; + victim->m_Shared.StunPlayer( stunTime, 1.0, TF_STUN_MOVEMENT ); + + victim->ApplyPunchImpulseX( RandomInt( 10, 15 ) ); + victim->SpeakConceptIfAllowed( MP_CONCEPT_DEFLECTED, "projectile:0,victim:1" ); + } + + victim->RemoveFlag( FL_ONGROUND ); + + Vector toVictim = victim->WorldSpaceCenter() - pushOrigin; + toVictim.z = 0.0f; + toVictim.NormalizeInPlace(); + toVictim.z = 1.0f; + + victim->ApplyAbsVelocityImpulse( pushForce * toVictim ); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCRush::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + m_timer.Start( 1.5f ); + m_chargeOrigin = me->GetAbsOrigin(); + m_maxAttainedSpeed = 0.0f; + m_lastSpeed = 0.0f; + m_didHitVictim = false; + + me->AddCondition( CBotNPC::CHARGING ); + me->AddCondition( CBotNPC::SHIELDED ); + + me->EmitSound( "Halloween.HeadlessBossAttack" ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCRush::Update( CBotNPC *me, float interval ) +{ + // pushaway/hit nearby players + CUtlVector< CTFPlayer * > playerVector; + CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS ); + CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS ); + + Vector chargeVector = me->GetAbsOrigin() - m_chargeOrigin; + chargeVector.NormalizeInPlace(); + + const float chargeRadius = 150.0f; + + for( int i=0; i<playerVector.Count(); ++i ) + { + CTFPlayer *victim = playerVector[i]; + + if ( me->IsRangeGreaterThan( victim, chargeRadius ) ) + continue; + + Vector closestPointOnChargePath; + CalcClosestPointOnLine( victim->GetAbsOrigin(), m_chargeOrigin, me->GetAbsOrigin(), closestPointOnChargePath ); + + Vector fromChargePath = victim->GetAbsOrigin() - closestPointOnChargePath; + float range = fromChargePath.NormalizeInPlace(); + + if ( range >= chargeRadius ) + continue; + + if ( !me->IsLineOfSightClear( victim ) ) + continue; + + float nearness = 1.0f - ( range / chargeRadius ); + + // push 'em + float pushForce = tf_bot_npc_charge_pushaway_force.GetFloat() * nearness; + PushawayPlayer( victim, closestPointOnChargePath, pushForce ); + + // crunch 'em + CTakeDamageInfo info( me, me, tf_bot_npc_charge_damage.GetFloat() * nearness, DMG_CRUSH, TF_DMG_CUSTOM_NONE ); + + CalculateMeleeDamageForce( &info, fromChargePath, closestPointOnChargePath, 1.0f ); + + victim->TakeDamage( info ); + + color32 color = { 255, 0, 0, 255 }; + UTIL_ScreenFade( victim, color, 0.5f, 0.1f, FFADE_IN ); + + if ( nearness > 0.5f ) + { + m_didHitVictim = true; + } + } + + float speed = me->GetLocomotionInterface()->GetVelocity().Length(); + m_maxAttainedSpeed = MAX( m_maxAttainedSpeed, speed ); + + if ( m_timer.IsElapsed() ) + { + return ChangeTo( new CBotNPCLaunchRockets, "Finished charge" ); + } + else + { + // chaaarge! + me->GetLocomotionInterface()->Run(); + + Vector forward; + me->GetVectors( &forward, NULL, NULL ); + me->GetLocomotionInterface()->Approach( 100.0f * forward + me->GetLocomotionInterface()->GetFeet() ); + + if ( !m_didHitVictim && m_maxAttainedSpeed > 350.0f && speed - m_lastSpeed < -200.0f ) + { + // abrupt slowdown = bonk! + return ChangeTo( new CBotNPCStunned( 3.0f, new CBotNPCLaunchRockets ), "Smacked into the world" ); + } + } + + // animation + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_CROUCHWALK_PRIMARY ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_CROUCHWALK_PRIMARY ); + } + + m_lastSpeed = speed; + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCRush::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) +{ + me->RemoveCondition( CBotNPC::SHIELDED ); + me->RemoveCondition( CBotNPC::CHARGING ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBotNPC > CBotNPCRush::OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result ) +{ + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCBlock : public Action< CBotNPC > +{ +public: + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + virtual void OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ); + + virtual ActionResult< CBotNPC > OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction ); + + virtual const char *GetName( void ) const { return "Block"; } // return name of this action + +private: + CountdownTimer m_timer; +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCBlock::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + // start animation + me->SetSequence( me->LookupSequence( "marketing_pose_001" ) ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + + m_timer.Start( 3.0f ); + + me->AddCondition( CBotNPC::SHIELDED ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCBlock::Update( CBotNPC *me, float interval ) +{ + if ( m_timer.IsElapsed() ) + { + return Done(); + } + + if ( me->GetAttackTarget() ) + { + me->GetLocomotionInterface()->FaceTowards( me->GetAttackTarget()->WorldSpaceCenter() ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCBlock::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) +{ + me->RemoveCondition( CBotNPC::SHIELDED ); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCBlock::OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction ) +{ + return Done(); +} + + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCLaunchGrenades : public Action< CBotNPC > +{ +public: + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + virtual void OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ); + + // if anything interrupts this action, abort it + virtual ActionResult< CBotNPC > OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction ) { return Done(); } + + virtual const char *GetName( void ) const { return "LaunchGrenades"; } // return name of this action + +private: + CountdownTimer m_timer; + CountdownTimer m_detonateTimer; + CUtlVector< CHandle< CTFGrenadePipebombProjectile > > m_grenadeVector; + void LaunchGrenade( CBotNPC *me, const Vector &launchVel, CTFWeaponInfo *weaponInfo ); + void LaunchGrenadeRings( CBotNPC *me ); + void LaunchGrenadeSpokes( CBotNPC *me ); + int m_animLayer; +}; + +ConVar tf_bot_npc_grenade_ring_min_horiz_vel( "tf_bot_npc_grenade_ring_min_horiz_vel", "100"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_grenade_ring_max_horiz_vel( "tf_bot_npc_grenade_ring_max_horiz_vel", "350"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_grenade_vert_vel( "tf_bot_npc_grenade_vert_vel", "750"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_grenade_det_time( "tf_bot_npc_grenade_det_time", "3"/*, FCVAR_CHEAT*/ ); + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCLaunchGrenades::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_SECONDARY ); + m_animLayer = me->AddLayeredSequence( me->LookupSequence( "gesture_melee_cheer" ), 0 ); + + m_timer.Start( 1.0f ); + m_detonateTimer.Invalidate(); + me->AddCondition( CBotNPC::BUSY ); + me->GetGrenadeTimer()->Start( me->GetGrenadeInterval() ); + + me->EmitSound( "RobotBoss.LaunchGrenades" ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCLaunchGrenades::LaunchGrenade( CBotNPC *me, const Vector &launchVel, CTFWeaponInfo *weaponInfo ) +{ + CTFGrenadePipebombProjectile *pProjectile = CTFGrenadePipebombProjectile::Create( me->WorldSpaceCenter(), vec3_angle, launchVel, + AngularImpulse( 600, random->RandomInt( -1200, 1200 ), 0 ), + me, *weaponInfo, TF_PROJECTILE_PIPEBOMB_REMOTE, 1 ); + if ( pProjectile ) + { + pProjectile->SetLauncher( me ); + pProjectile->SetDamage( tf_bot_npc_grenade_damage.GetFloat() ); + + if ( me->IsInCondition( CBotNPC::ENRAGED ) ) + { + pProjectile->SetCritical( true ); + } + + m_grenadeVector.AddToTail( pProjectile ); + } +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCLaunchGrenades::LaunchGrenadeRings( CBotNPC *me ) +{ + const char *weaponAlias = WeaponIdToAlias( TF_WEAPON_GRENADELAUNCHER ); + if ( !weaponAlias ) + return; + + WEAPON_FILE_INFO_HANDLE weaponInfoHandle = LookupWeaponInfoSlot( weaponAlias ); + if ( weaponInfoHandle == GetInvalidWeaponInfoHandle() ) + return; + + CTFWeaponInfo *weaponInfo = static_cast< CTFWeaponInfo * >( GetFileWeaponInfoFromHandle( weaponInfoHandle ) ); + + QAngle myAngles = me->EyeAngles(); + + // create rings of stickies + float deltaVel = tf_bot_npc_grenade_ring_max_horiz_vel.GetFloat() - tf_bot_npc_grenade_ring_min_horiz_vel.GetFloat(); + const int ringCount = 2; + for( int r=0; r<ringCount; ++r ) + { + float u = (float)r/(float)(ringCount-1); + + float horizVel = tf_bot_npc_grenade_ring_min_horiz_vel.GetFloat() + u * deltaVel; + + float angleDelta = 10.0f + 20.0f * ( 1.0f - u ); + + for( float angle=0.0f; angle<360.0f; angle += angleDelta ) + { + Vector forward; + AngleVectors( myAngles, &forward ); + + Vector vecVelocity( horizVel * forward.x, horizVel * forward.y, tf_bot_npc_grenade_vert_vel.GetFloat() ); + + LaunchGrenade( me, vecVelocity, weaponInfo ); + + myAngles.y += angleDelta; + } + } +} + + +ConVar tf_bot_npc_grenade_spoke_angle( "tf_bot_npc_grenade_spoke_angle", "45"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_grenade_spoke_count( "tf_bot_npc_grenade_spoke_count", "15"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_grenade_spoke_min_horiz_vel( "tf_bot_npc_grenade_spoke_min_horiz_vel", "100"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_grenade_spoke_max_horiz_vel( "tf_bot_npc_grenade_spoke_max_horiz_vel", "750"/*, FCVAR_CHEAT*/ ); + + +//--------------------------------------------------------------------------------------------- +void CBotNPCLaunchGrenades::LaunchGrenadeSpokes( CBotNPC *me ) +{ + const char *weaponAlias = WeaponIdToAlias( TF_WEAPON_GRENADELAUNCHER ); + if ( !weaponAlias ) + return; + + WEAPON_FILE_INFO_HANDLE weaponInfoHandle = LookupWeaponInfoSlot( weaponAlias ); + if ( weaponInfoHandle == GetInvalidWeaponInfoHandle() ) + return; + + CTFWeaponInfo *weaponInfo = static_cast< CTFWeaponInfo * >( GetFileWeaponInfoFromHandle( weaponInfoHandle ) ); + + // create spokes of stickies + float deltaVel = tf_bot_npc_grenade_spoke_max_horiz_vel.GetFloat() - tf_bot_npc_grenade_spoke_min_horiz_vel.GetFloat(); + float angleDelta = tf_bot_npc_grenade_spoke_angle.GetFloat(); + QAngle myAngles = me->EyeAngles(); + + for( float angle=0.0f; angle<360.0f; angle += angleDelta ) + { + Vector forward; + AngleVectors( myAngles, &forward ); + + int spokeCount = tf_bot_npc_grenade_spoke_count.GetInt(); + + for( int i=0; i<spokeCount; ++i ) + { + float u = (float)i/(float)(spokeCount-1); + + float horizVel = tf_bot_npc_grenade_spoke_min_horiz_vel.GetFloat() + u * deltaVel; + + Vector vecVelocity( horizVel * forward.x, horizVel * forward.y, tf_bot_npc_grenade_vert_vel.GetFloat() ); + + LaunchGrenade( me, vecVelocity, weaponInfo ); + } + + myAngles.y += angleDelta; + } +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCLaunchGrenades::Update( CBotNPC *me, float interval ) +{ + QAngle myAngles = me->EyeAngles(); + + if ( m_timer.HasStarted() && m_timer.IsElapsed() ) + { + m_timer.Invalidate(); + + if ( RandomInt( 0, 100 ) < 50 ) + { + LaunchGrenadeRings( me ); + } + else + { + LaunchGrenadeSpokes( me ); + } + + me->EmitSound( "Weapon_Grenade_Normal.Single" ); + + m_detonateTimer.Start( tf_bot_npc_grenade_det_time.GetFloat() ); + } + + if ( m_detonateTimer.HasStarted() && m_detonateTimer.IsElapsed() ) + { + // detonate the stickies + for( int i=0; i<m_grenadeVector.Count(); ++i ) + { + if ( m_grenadeVector[i] ) + { + m_grenadeVector[i]->Detonate(); + } + } + + return Done(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCLaunchGrenades::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) +{ + // fizzle any outstanding stickies + for( int i=0; i<m_grenadeVector.Count(); ++i ) + { + if ( m_grenadeVector[i] ) + { + m_grenadeVector[i]->Fizzle(); + m_grenadeVector[i]->Detonate(); + } + } + + me->RemoveCondition( CBotNPC::ENRAGED ); + me->RemoveCondition( CBotNPC::BUSY ); + me->FastRemoveLayer( m_animLayer ); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCShootCrossbow : public Action< CBotNPC > +{ +public: + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + + // if anything interrupts this action, abort it + virtual ActionResult< CBotNPC > OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction ) { return Done(); } + + virtual const char *GetName( void ) const { return "ShootCrossbow"; } // return name of this action + +private: + CountdownTimer m_timer; +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCShootCrossbow::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_SECONDARY ); + m_timer.Start( tf_bot_npc_aim_time.GetFloat() ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCShootCrossbow::Update( CBotNPC *me, float interval ) +{ + CBaseCombatCharacter *target = me->GetAttackTarget(); + + if ( !target ) + { + return Done( "No target" ); + } + + me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() ); + + if ( m_timer.IsElapsed() ) + { + // fire bolt + const float arrowSpeed = 4000.0f; + const float arrowGravity = 0.0f; // railgun + + Vector muzzleOrigin; + QAngle muzzleAngles; + if ( me->GetWeapon()->GetAttachment( "muzzle", muzzleOrigin, muzzleAngles ) == false ) + { + return Done( "No muzzle attachment!" ); + } + + // lead target + float range = me->GetRangeTo( target->EyePosition() ); + float flightTime = range / arrowSpeed; + + Vector aimSpot = target->EyePosition() + target->GetAbsVelocity() * flightTime; + + Vector to = aimSpot - muzzleOrigin; + VectorAngles( to, muzzleAngles ); + + CTFProjectile_Arrow *arrow = CTFProjectile_Arrow::Create( muzzleOrigin, muzzleAngles, arrowSpeed, arrowGravity, TF_PROJECTILE_ARROW, me, me ); + if ( arrow ) + { + arrow->SetLauncher( me ); + arrow->SetCritical( true ); + + // set damage to 5 points more than our target's max health so a Medic can save us + // arrow->SetDamage( ( target->GetMaxHealth() + 5.0f ) / TF_DAMAGE_CRIT_MULTIPLIER ); + arrow->SetDamage( 200.0f ); + + me->EmitSound( "Weapon_CompoundBow.Single" ); + } + + return Done(); + } + + return Continue(); +} + + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCLostVictim : public Action< CBotNPC > +{ +public: + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + virtual void OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ); + + virtual const char *GetName( void ) const { return "LostVictim"; } // return name of this action + +private: + CountdownTimer m_timer; + float m_headTurn; + int m_headYawPoseParameter; +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCLostVictim::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + m_headTurn = 0.0f; + m_headYawPoseParameter = me->LookupPoseParameter( "body_yaw" ); + + m_timer.Start( RandomFloat( 3.0f, 5.0f ) ); + + me->EmitSound( "RobotBoss.Scanning" ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCLostVictim::Update( CBotNPC *me, float interval ) +{ + if ( m_timer.IsElapsed() ) + { + return Done( "Giving up" ); + } + + CBaseCombatCharacter *target = me->GetAttackTarget(); + if ( target ) + { + if ( me->IsLineOfSightClear( target ) || me->IsPrisonerOfMinion( target ) ) + { + me->EmitSound( "RobotBoss.Acquire" ); + me->AddGesture( ACT_MP_GESTURE_FLINCH_CHEST ); + return Done( "Ah hah!" ); + } + } + + const float rate = M_PI / 3.0f; + m_headTurn += rate * interval; + + float s, c; + SinCos( m_headTurn, &s, &c ); + + me->SetPoseParameter( m_headYawPoseParameter, 40.0f * s ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCLostVictim::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) +{ + me->SetPoseParameter( m_headYawPoseParameter, 0 ); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCChaseVictim : public Action< CBotNPC > +{ +public: + CBotNPCChaseVictim( CBaseCombatCharacter *chaseTarget ); + + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + virtual void OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ); + + virtual EventDesiredResult< CBotNPC > OnStuck( CBotNPC *me ); + virtual EventDesiredResult< CBotNPC > OnMoveToSuccess( CBotNPC *me, const Path *path ); + virtual EventDesiredResult< CBotNPC > OnMoveToFailure( CBotNPC *me, const Path *path, MoveToFailureType reason ); + + virtual const char *GetName( void ) const { return "ChaseVictim"; } // return name of this action + +private: + CTFPathFollower m_path; + IntervalTimer m_visibleTimer; + CHandle< CBaseCombatCharacter > m_lastTarget; + + CHandle< CBaseCombatCharacter > m_chaseTarget; + Vector m_lastKnownTargetSpot; +}; + + +//--------------------------------------------------------------------------------------------- +CBotNPCChaseVictim::CBotNPCChaseVictim( CBaseCombatCharacter *chaseTarget ) +{ + m_chaseTarget = chaseTarget; + m_lastKnownTargetSpot = chaseTarget->GetAbsOrigin(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCChaseVictim::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + if ( m_chaseTarget == NULL ) + { + return Done( "Target is NULL" ); + } + + m_lastKnownTargetSpot = m_chaseTarget->GetAbsOrigin(); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCChaseVictim::Update( CBotNPC *me, float interval ) +{ + if ( m_chaseTarget == NULL || !m_chaseTarget->IsAlive() ) + { + return ChangeTo( new CBotNPCLostVictim, "No victim" ); + } + + if ( m_chaseTarget != me->GetAttackTarget() ) + { + return Done( "Changing targets" ); + } + + Vector moveGoal = m_chaseTarget->GetAbsOrigin(); + + if ( me->IsLineOfSightClear( m_chaseTarget ) ) + { + if ( !m_visibleTimer.HasStarted() ) + { + m_visibleTimer.Start(); + } + + if ( me->HasAbility( CBotNPC::CAN_NUKE ) && me->GetNukeTimer()->IsElapsed() ) + { + return SuspendFor( new CBotNPCNukeAttack, "Nuking!" ); + } + + m_lastKnownTargetSpot = m_chaseTarget->GetAbsOrigin(); + + if ( me->HasAbility( CBotNPC::CAN_LAUNCH_STICKIES ) ) + { + if ( ( me->GetGrenadeTimer()->IsElapsed() && me->IsRangeLessThan( m_chaseTarget, tf_bot_npc_grenade_launch_range.GetFloat() ) ) || + me->IsInCondition( CBotNPC::ENRAGED ) ) + { + return SuspendFor( new CBotNPCLaunchGrenades, "Target is close (or I am enraged) - grenades!" ); + } + } + + // chase into line of sight a bit so they can't immediately get behind cover again + if ( me->HasAbility( CBotNPC::CAN_FIRE_ROCKETS ) ) + { + if ( m_visibleTimer.IsGreaterThen( 1.0f ) || + me->IsRangeLessThan( m_chaseTarget, tf_bot_npc_chase_range.GetFloat() ) ) + { + return SuspendFor( new CBotNPCLaunchRockets, "Fire!" ); + } + } + + if ( me->IsRangeLessThan( m_chaseTarget, 150.0f ) ) + { + // too close - stand still + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_MELEE ); + } + + return Continue(); + } + } + else + { + m_visibleTimer.Invalidate(); + + // move to where we last saw our target + moveGoal = m_lastKnownTargetSpot; + + if ( me->IsRangeLessThan( m_lastKnownTargetSpot, 20.0f ) ) + { + // reached spot where we last saw our victim - give up + me->SetAttackTarget( NULL ); + + return ChangeTo( new CBotNPCLostVictim, "I lost my chase victim" ); + } + } + + + // move into sight of target + if ( m_path.GetAge() > 1.0f ) + { + CBotNPCPathCost cost( me ); + m_path.Compute( me, moveGoal, cost ); + } + + me->GetLocomotionInterface()->Run(); + m_path.Update( me ); + + // play running animation + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBotNPC > CBotNPCChaseVictim::OnMoveToSuccess( CBotNPC *me, const Path *path ) +{ + return TryDone( RESULT_CRITICAL, "Reached move goal" ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBotNPC > CBotNPCChaseVictim::OnMoveToFailure( CBotNPC *me, const Path *path, MoveToFailureType reason ) +{ + return TryDone( RESULT_CRITICAL, "Path follow failed" ); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCChaseVictim::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) +{ +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBotNPC > CBotNPCChaseVictim::OnStuck( CBotNPC *me ) +{ + // we're stuck - just warp to the our next path goal + if ( m_path.GetCurrentGoal() ) + { + me->SetAbsOrigin( m_path.GetCurrentGoal()->pos + Vector( 0, 0, 10.0f ) ); + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCLaserBlast : public Action< CBotNPC > +{ +public: + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + virtual void OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ); + + virtual ActionResult< CBotNPC > OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction ); + + virtual EventDesiredResult< CBotNPC > OnStuck( CBotNPC *me ); + + virtual const char *GetName( void ) const { return "LaserBlast"; } // return name of this action + +private: + CTFPathFollower m_path; + CountdownTimer m_laserTimer; + IntervalTimer m_visibleTimer; + CHandle< CBaseCombatCharacter > m_lastTarget; +}; + +ConVar tf_bot_npc_laser_damage_rate( "tf_bot_npc_laser_damage_rate", "40"/*, FCVAR_CHEAT*/ ); // 20 +ConVar tf_bot_npc_laser_damage_gain_rate( "tf_bot_npc_laser_damage_gain_rate", "0"/*, FCVAR_CHEAT*/ ); // 0 +ConVar tf_bot_npc_laser_damage_ignite_threshold( "tf_bot_npc_laser_damage_ignite_threshold", "999"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_laser_damage_ignite_time( "tf_bot_npc_laser_damage_ignite_time", "3"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_laser_afterburn_time( "tf_bot_npc_laser_afterburn_time", "10"/*, FCVAR_CHEAT*/ ); +ConVar tf_bot_npc_laser_damage_building_multiplier( "tf_bot_npc_laser_damage_building_multiplier", "4"/*, FCVAR_CHEAT*/ ); + +ConVar tf_bot_npc_laser_duration( "tf_bot_npc_laser_duration", "8"/*, FCVAR_CHEAT*/ ); + + +//---------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCLaserBlast::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + m_laserTimer.Start( tf_bot_npc_laser_duration.GetFloat() ); + m_visibleTimer.Invalidate(); + m_lastTarget = NULL; + + return Continue(); +} + + +//---------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCLaserBlast::Update( CBotNPC *me, float interval ) +{ + CBaseCombatCharacter *target = me->GetAttackTarget(); + + if ( !target ) + { + return Done( "No victim" ); + } + + if ( me->HasAbility( CBotNPC::CAN_NUKE ) && me->GetNukeTimer()->IsElapsed() ) + { + return ChangeTo( new CBotNPCNukeAttack, "Nuking!" ); + } + + if ( target != m_lastTarget ) + { + // new target, reset laser + m_laserTimer.Reset(); + m_lastTarget = target; + } + + if ( me->HasAbility( CBotNPC::CAN_FIRE_ROCKETS ) && m_laserTimer.IsElapsed() ) + { + // laser not effective - try rockets! + return ChangeTo( new CBotNPCLaunchRockets, "Launching Rockets!" ); + } + + if ( me->IsLineOfSightClear( target ) ) + { + if ( !m_visibleTimer.HasStarted() ) + { + m_visibleTimer.Start(); + } + + me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() ); + + // blast 'em + me->SetLaserTarget( target ); + + float damage = tf_bot_npc_laser_damage_rate.GetFloat() + m_laserTimer.GetElapsedTime() * tf_bot_npc_laser_damage_gain_rate.GetFloat(); + + // lasers do extra damage to buildings + if ( target->IsBaseObject() ) + { + damage *= tf_bot_npc_laser_damage_building_multiplier.GetFloat(); + } + + CTakeDamageInfo info( me, me, damage * interval, DMG_ENERGYBEAM, TF_DMG_CUSTOM_NONE ); + + Vector toVictim = target->WorldSpaceCenter() - me->EyePosition(); + toVictim.NormalizeInPlace(); + + CalculateMeleeDamageForce( &info, toVictim, me->EyePosition(), 1.0f ); + target->TakeDamage( info ); + + if ( target->IsPlayer() && damage > tf_bot_npc_laser_damage_ignite_threshold.GetFloat() ) + { + ToTFPlayer( target )->m_Shared.Burn( me, tf_bot_npc_laser_afterburn_time.GetFloat() ); + } + + if ( target->IsPlayer() && m_laserTimer.GetElapsedTime() > tf_bot_npc_laser_damage_ignite_time.GetFloat() ) + { + ToTFPlayer( target )->m_Shared.Burn( me, tf_bot_npc_laser_afterburn_time.GetFloat() ); + } + + // me->EmitSound( "Weapon_Sword.HitFlesh" ); + + if ( !me->IsPlayingGesture( ACT_MP_GESTURE_FLINCH_CHEST ) ) + { + me->AddGesture( ACT_MP_GESTURE_FLINCH_CHEST ); + } + } + else + { + me->SetLaserTarget( NULL ); + m_laserTimer.Reset(); + m_visibleTimer.Invalidate(); + } + + // chase into line of sight a bit so they can't immediately get behind cover again + if ( !m_visibleTimer.HasStarted() || m_visibleTimer.IsLessThen( 1.0f ) ) + { + // don't get too close to avoid penetration/stuck issues + if ( me->IsRangeGreaterThan( target, 100.0f ) ) + { + // move into sight of target + if ( m_path.GetAge() > 1.0f ) + { + CBotNPCPathCost cost( me ); + m_path.Compute( me, target, cost ); + } + + me->GetLocomotionInterface()->Run(); + m_path.Update( me ); + } + } + + if ( me->GetLocomotionInterface()->IsAttemptingToMove() ) + { + // play running animation + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + } + } + else + { + // standing still + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_ITEM1 ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCLaserBlast::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) +{ + me->SetLaserTarget( NULL ); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCLaserBlast::OnSuspend( CBotNPC *me, Action< CBotNPC > *interruptingAction ) +{ + return Done(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBotNPC > CBotNPCLaserBlast::OnStuck( CBotNPC *me ) +{ + // we're stuck - just warp to the our next path goal + if ( m_path.GetCurrentGoal() ) + { + me->SetAbsOrigin( m_path.GetCurrentGoal()->pos + Vector( 0, 0, 10.0f ) ); + } + + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCAttack : public Action< CBotNPC > +{ +public: + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + + virtual ActionResult< CBotNPC > OnResume( CBotNPC *me, Action< CBotNPC > *interruptingAction ); + + virtual EventDesiredResult< CBotNPC > OnStuck( CBotNPC *me ); + virtual EventDesiredResult< CBotNPC > OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result = NULL ); + + virtual const char *GetName( void ) const { return "Attack"; } // return name of this action + +private: + CTFPathFollower m_path; + + CountdownTimer m_chargeTimer; + + CHandle< CTFPlayer > m_closestVisible; + CountdownTimer m_attackThrottleTimer; + + void ValidateChaseVictim( CBotNPC *me ); + + CountdownTimer m_attackTargetFocusTimer; +}; + + +//---------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCAttack::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + m_attackThrottleTimer.Invalidate(); + + m_closestVisible = NULL; + + m_attackTargetFocusTimer.Invalidate(); + + m_chargeTimer.Invalidate(); + + return Continue(); +} + + +//---------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCAttack::Update( CBotNPC *me, float interval ) +{ + if ( !me->IsAlive() ) + { + return Done(); + } + + CBaseCombatCharacter *target = me->GetAttackTarget(); + + if ( !target ) + { + return Done( "No victim" ); + } + + me->GetLocomotionInterface()->FaceTowards( target->WorldSpaceCenter() ); + + // swing our axe at our attack target if they are in range + if ( !me->IsSwingingAxe() ) + { + if ( me->IsRangeLessThan( target, tf_bot_npc_attack_range.GetFloat() ) ) + { + me->SwingAxe(); + } + } + + if ( !me->IsSwingingAxe() ) + { + if ( m_chargeTimer.IsElapsed() && me->IsLookingTowards( target->WorldSpaceCenter(), 0.9f ) ) + { + m_chargeTimer.Start( tf_bot_npc_charge_interval.GetFloat() ); + return SuspendFor( new CBotNPCRush, "Chaaarge!" ); + } + + if ( me->GetReceivedDamagePerSecond() > tf_bot_npc_block_dps_react.GetFloat() && + target->IsPlayer() && + ToTFPlayer( target )->GetTimeSinceWeaponFired() < 1.0f ) + { + return SuspendFor( new CBotNPCBlock, "Blocking" ); + } + } + + // chase after our victim + const float standAndSwingRange = 0.5f * tf_bot_npc_attack_range.GetFloat(); + + if ( me->IsRangeGreaterThan( target, standAndSwingRange ) || !me->IsLineOfSightClear( target ) ) + { + if ( m_path.GetAge() > 1.0f ) + { + CBotNPCPathCost cost( me ); + m_path.Compute( me, target, cost ); + } + + m_path.Update( me ); + } + + if ( me->GetLocomotionInterface()->IsAttemptingToMove() ) + { + // play running animation + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + } + } + else + { + // standing still + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_ITEM1 ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 ); + } + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCAttack::OnResume( CBotNPC *me, Action< CBotNPC > *interruptingAction ) +{ + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBotNPC > CBotNPCAttack::OnStuck( CBotNPC *me ) +{ + // we're stuck - just warp to the our next path goal + if ( m_path.GetCurrentGoal() ) + { + me->SetAbsOrigin( m_path.GetCurrentGoal()->pos + Vector( 0, 0, 10.0f ) ); + } + + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBotNPC > CBotNPCAttack::OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result ) +{ + return TryContinue( RESULT_TRY ); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCGuardSpot : public Action< CBotNPC > +{ +public: + //----------------------------------------------------------------------------------------------------- + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) + { + m_path.SetMinLookAheadDistance( 300.0f ); + + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 ); + me->SetHomePosition( me->GetAbsOrigin() ); + + m_lookAtSpot = vec3_origin; + + return Continue(); + } + + //----------------------------------------------------------------------------------------------------- + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ) + { + CBaseCombatCharacter *target = me->GetAttackTarget(); + if ( target ) + { + if ( me->IsLineOfSightClear( target ) || me->IsPrisonerOfMinion( target ) ) + { + return SuspendFor( new CBotNPCChaseVictim( me->GetAttackTarget() ), "Get 'em!" ); + } + } + + CBaseCombatCharacter *visible = me->GetNearestVisibleEnemy(); + if ( visible ) + { + // look at visible victim out of range + me->GetLocomotionInterface()->FaceTowards( visible->WorldSpaceCenter() ); + } + + const float atHomeRange = 50.0f; + if ( me->IsRangeGreaterThan( me->GetHomePosition(), atHomeRange ) ) + { + if ( m_path.GetAge() > 3.0f ) + { + CBotNPCPathCost cost( me ); + if ( m_path.Compute( me, me->GetHomePosition(), cost ) == false ) + { + // can't reach guard post - just jump there for now + me->Teleport( &me->GetHomePosition(), NULL, NULL ); + } + } + + m_path.Update( me ); + } + else + { + // on guard spot - look around + if ( m_lookTimer.IsElapsed() ) + { + m_lookTimer.Start( RandomFloat( 1.0f, 2.0f ) ); + + CTFNavArea *myArea = (CTFNavArea *)me->GetLastKnownArea(); + if ( myArea ) + { + const CUtlVector< CTFNavArea * > &invasionAreaVector = myArea->GetEnemyInvasionAreaVector( TF_TEAM_RED ); + + if ( invasionAreaVector.Count() > 0 ) + { + // try to not look directly at walls + const float minGazeRange = 300.0f; + const int retryCount = 20.0f; + for( int r=0; r<retryCount; ++r ) + { + int which = RandomInt( 0, invasionAreaVector.Count()-1 ); + Vector gazeSpot = invasionAreaVector[ which ]->GetRandomPoint() + Vector( 0, 0, 0.75f * HumanHeight ); + + if ( me->IsRangeGreaterThan( gazeSpot, minGazeRange ) && me->GetVisionInterface()->IsLineOfSightClear( gazeSpot ) ) + { + // use maxLookInterval so these looks override body aiming from path following + m_lookAtSpot = gazeSpot; + break; + } + } + } + } + } + + me->GetLocomotionInterface()->FaceTowards( m_lookAtSpot ); + } + + if ( me->GetLocomotionInterface()->IsAttemptingToMove() ) + { + // play running animation + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_RUN_MELEE ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_MELEE ); + } + } + else + { + // standing still + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_ITEM1 ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM1 ); + } + } + + return Continue(); + } + + //----------------------------------------------------------------------------------------------------- + virtual EventDesiredResult< CBotNPC > OnInjured( CBotNPC *me, const CTakeDamageInfo &info ) + { + CTFPlayer *attacker = ToTFPlayer( info.GetAttacker() ); + + if ( me->HasAbility( CBotNPC::CAN_BE_STUNNED ) && attacker ) + { + if ( tf_bot_npc_always_stun.GetBool() ) + { + return TrySuspendFor( new CBotNPCStunned( tf_bot_npc_stunned_duration.GetFloat() ), RESULT_CRITICAL, "CVar force stunned" ); + } + + + bool isDeflectedRocket = false; + CTFBaseRocket *pBaseRocket = dynamic_cast< CTFBaseRocket * >( info.GetInflictor() ); + if ( pBaseRocket && pBaseRocket->GetDeflected() ) + { + isDeflectedRocket = true; + } + + const float hardHit = 50.0f; + bool isPotentialStunHit = info.GetDamage() > hardHit || isDeflectedRocket; + + if ( m_headStunTimer.IsElapsed() && isPotentialStunHit ) + { + Vector headPos; + QAngle headAngles; + if ( me->GetAttachment( "head", headPos, headAngles ) ) + { + if ( ( info.GetDamagePosition() - headPos ).IsLengthLessThan( tf_bot_npc_head_radius.GetFloat() ) ) + { + // hit head + + // deflecting consecutive Boss' rockets into his head == stun + if ( isDeflectedRocket ) + { + if ( !m_consecutiveRocketTimer.HasStarted() || // first rocket hit + m_consecutiveRocketTimer.IsElapsed() ) // too much time between hits - treat as first hit + { + m_consecutiveRocketTimer.Start( tf_bot_npc_stun_rocket_reflect_duration.GetFloat() ); + m_consecutiveRockets = 1; + } + else + { + // successive rocket hit + if ( ++m_consecutiveRockets >= tf_bot_npc_stun_rocket_reflect_count.GetInt() ) + { + return TrySuspendFor( new CBotNPCStunned( tf_bot_npc_stunned_duration.GetFloat() ), RESULT_CRITICAL, "My own rockets reflected into my head!" ); + } + } + + me->EmitSound( "RobotBoss.Vulnerable" ); + } + + // look for hard hits from above + Vector toAttacker = attacker->EyePosition() - headPos; + toAttacker.NormalizeInPlace(); + + if ( toAttacker.z > 0.9f ) + { + // just got hit in the head from an attacker above me - stun + m_headStunTimer.Start( 20.0f ); + + return TrySuspendFor( new CBotNPCStunned( tf_bot_npc_stunned_duration.GetFloat() ), RESULT_CRITICAL, "Hard head hit from above!" ); + } + } + } + } + } + + return TryContinue(); + } + + //----------------------------------------------------------------------------------------------------- + virtual const char *GetName( void ) const { return "GuardSpot"; } // return name of this action + +private: + CTFPathFollower m_path; + CountdownTimer m_lookTimer; + Vector m_lookAtSpot; + CountdownTimer m_headStunTimer; + + CountdownTimer m_consecutiveRocketTimer; + int m_consecutiveRockets; +}; + + +//--------------------------------------------------------------------------------------------- +ConVar tf_bot_npc_get_off_me_duration( "tf_bot_npc_get_off_me_duration", "3"/*, FCVAR_CHEAT */ ); + +ActionResult< CBotNPC > CBotNPCGetOffMe::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + me->AddGestureSequence( me->LookupSequence( "gesture_melee_help" ) ); + m_timer.Start( 0.5f ); + + me->AddCondition( CBotNPC::BUSY ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCGetOffMe::Update( CBotNPC *me, float interval ) +{ + if ( m_timer.IsElapsed() ) + { + // blast players off of my head + CUtlVector< CTFPlayer * > onMeVector; + me->CollectPlayersStandingOnMe( &onMeVector ); + + Vector headPos; + QAngle headAngles; + if ( me->GetAttachment( "head", headPos, headAngles ) ) + { + for( int i=0; i<onMeVector.Count(); ++i ) + { + // push 'em off + PushawayPlayer( onMeVector[i], headPos, tf_bot_npc_charge_pushaway_force.GetFloat() ); + } + } + + me->EmitSound( "Weapon_FlameThrower.AirBurstAttack" ); + + return Done(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCGetOffMe::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) +{ + me->RemoveCondition( CBotNPC::BUSY ); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCWaitForPlayers : public Action< CBotNPC > +{ +public: + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + virtual void OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ); + + virtual EventDesiredResult< CBotNPC > OnInjured( CBotNPC *me, const CTakeDamageInfo &info ); + virtual EventDesiredResult< CBotNPC > OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result = NULL ); + + virtual const char *GetName( void ) const { return "WaitForPlayers"; } // return name of this action +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCWaitForPlayers::OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) +{ + me->AddCondition( CBotNPC::BUSY ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPC > CBotNPCWaitForPlayers::Update( CBotNPC *me, float interval ) +{ + CBaseCombatCharacter *target = me->GetAttackTarget(); + if ( target ) + { + return ChangeTo( new CBotNPCGuardSpot, "I see you..." ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCWaitForPlayers::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) +{ + me->RemoveCondition( CBotNPC::BUSY ); + + me->GetNukeTimer()->Start( tf_bot_npc_nuke_interval.GetFloat() ); + me->GetGrenadeTimer()->Reset(); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBotNPC > CBotNPCWaitForPlayers::OnInjured( CBotNPC *me, const CTakeDamageInfo &info ) +{ + return TryChangeTo( new CBotNPCGuardSpot, RESULT_CRITICAL, "Ouch!" ); +} + + +//--------------------------------------------------------------------------------------------- +EventDesiredResult< CBotNPC > CBotNPCWaitForPlayers::OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result ) +{ + if ( other && other->IsPlayer() ) + { + return TryChangeTo( new CBotNPCGuardSpot, RESULT_CRITICAL, "Don't touch me" ); + } + + return TryContinue(); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCTacticalMonitor : public Action< CBotNPC > +{ +public: + virtual Action< CBotNPC > *InitialContainedAction( CBotNPC *me ) + { + if ( TFGameRules()->IsBossBattleMode() ) + { + return new CBotNPCWaitForPlayers; + } + + return NULL; + } + + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ) + { + m_getOffMeTimer.Invalidate(); + + return Continue(); + } + + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ) + { + // HACK: If we fell off the ledge, jump back +/* + if ( me->GetLocomotionInterface()->IsOnGround() && + me->GetAbsOrigin().z < me->GetHomePosition().z - 200.0f ) + { + return SuspendFor( new CBotNPCBigJump( me->GetHomePosition(), new CBotNPCLaunchRockets ), "Jumping home" ); + } +*/ + + if ( !m_getOffMeTimer.HasStarted() ) + { + CUtlVector< CTFPlayer * > onMeVector; + me->CollectPlayersStandingOnMe( &onMeVector ); + + if ( onMeVector.Count() ) + { + // someone is standing on me - push them off soon + m_getOffMeTimer.Start( tf_bot_npc_get_off_me_duration.GetFloat() ); + } + } + else if ( m_getOffMeTimer.IsElapsed() ) + { + if ( !me->IsBusy() ) + { + m_getOffMeTimer.Invalidate(); + + // if someone is still on me, push them off + CUtlVector< CTFPlayer * > onMeVector; + me->CollectPlayersStandingOnMe( &onMeVector ); + if ( onMeVector.Count() ) + { + return SuspendFor( new CBotNPCGetOffMe, "Get offa me!" ); + } + } + } + + return Continue(); + } + + virtual const char *GetName( void ) const { return "TacticalMonitor"; } // return name of this action + +private: + CountdownTimer m_backOffCooldownTimer; + + CountdownTimer m_getOffMeTimer; +}; + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCBehavior : public Action< CBotNPC > +{ +public: + virtual Action< CBotNPC > *InitialContainedAction( CBotNPC *me ) + { + return new CBotNPCTacticalMonitor; + } + + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ) + { + if ( m_vocalTimer.IsElapsed() ) + { + m_vocalTimer.Start( RandomFloat( 3.0f, 5.0f ) ); + + if ( !me->IsBusy() ) + { + me->EmitSound( "RobotBoss.Vocalize" ); + } + } + + return Continue(); + } + + virtual EventDesiredResult< CBotNPC > OnKilled( CBotNPC *me, const CTakeDamageInfo &info ) + { + // relay the event to the map logic + CTFSpawnerBoss *spawner = me->GetSpawner(); + if ( spawner ) + { + spawner->OnBotKilled( me ); + } + + // Calculate death force + Vector forceVector = me->CalcDamageForceVector( info ); + + // See if there's a ragdoll magnet that should influence our force. + CRagdollMagnet *magnet = CRagdollMagnet::FindBestMagnet( me ); + if ( magnet ) + { + forceVector += magnet->GetForceVector( me ); + } + + if ( me->IsMiniBoss() ) + { + me->EmitSound( "Cart.Explode" ); + me->BecomeRagdoll( info, forceVector ); + + if ( g_pMonsterResource ) + { + g_pMonsterResource->HideBossHealthMeter(); + } + } + else + { + // full end-of-game boss + UTIL_Remove( me ); + + if ( TFGameRules()->IsBossBattleMode() ) + { + // check that ALL bosses are dead + bool isBossBattleWon = true; + + CBotNPC *boss = NULL; + while( ( boss = (CBotNPC *)gEntList.FindEntityByClassname( boss, "bot_boss" ) ) != NULL ) + { + if ( !me->IsSelf( boss ) && boss->IsAlive() && !boss->IsMiniBoss() ) + { + isBossBattleWon = false; + } + } + + if ( isBossBattleWon ) + { + TFGameRules()->SetWinningTeam( TF_TEAM_BLUE, WINREASON_OPPONENTS_DEAD ); + + if ( g_pMonsterResource ) + { + g_pMonsterResource->HideBossHealthMeter(); + } + } + } + } + + return TryDone(); + } + + virtual EventDesiredResult< CBotNPC > OnContact( CBotNPC *me, CBaseEntity *other, CGameTrace *result = NULL ) + { + return TryContinue(); + } + + virtual const char *GetName( void ) const { return "Behavior"; } // return name of this action + +private: + CountdownTimer m_vocalTimer; +}; + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CBotNPCIntention::CBotNPCIntention( CBotNPC *me ) : IIntention( me ) +{ + m_behavior = new Behavior< CBotNPC >( new CBotNPCBehavior ); +} + +CBotNPCIntention::~CBotNPCIntention() +{ + delete m_behavior; +} + +void CBotNPCIntention::Reset( void ) +{ + delete m_behavior; + m_behavior = new Behavior< CBotNPC >( new CBotNPCBehavior ); +} + +void CBotNPCIntention::Update( void ) +{ + m_behavior->Update( static_cast< CBotNPC * >( GetBot() ), GetUpdateInterval() ); +} + +// is the a place we can be? +QueryResultType CBotNPCIntention::IsPositionAllowed( const INextBot *meBot, const Vector &pos ) const +{ + return ANSWER_YES; +} + + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +CBotNPCLocomotion::CBotNPCLocomotion( INextBot *bot ) : NextBotGroundLocomotion( bot ) +{ + CBotNPC *me = (CBotNPC *)GetBot()->GetEntity(); + + m_runSpeed = me->GetMoveSpeed(); +} + + +//--------------------------------------------------------------------------------------------- +float CBotNPCLocomotion::GetRunSpeed( void ) const +{ + CBotNPC *me = (CBotNPC *)GetBot()->GetEntity(); + + return me->IsInCondition( CBotNPC::CHARGING ) ? 1000.0f : m_runSpeed; +} + + +//--------------------------------------------------------------------------------------------- +// if delta Z is greater than this, we have to jump to get up +float CBotNPCLocomotion::GetStepHeight( void ) const +{ + return 18.0f; +} + + +//--------------------------------------------------------------------------------------------- +// return maximum height of a jump +float CBotNPCLocomotion::GetMaxJumpHeight( void ) const +{ + return 18.0f; +} + + +//--------------------------------------------------------------------------------------------- +// Return true to completely ignore this entity (may not be in sight when this is called) +bool CBotNPCVision::IsIgnored( CBaseEntity *subject ) const +{ + if ( subject->IsPlayer() ) + { + CTFPlayer *enemy = static_cast< CTFPlayer * >( subject ); + + if ( enemy->m_Shared.InCond( TF_COND_BURNING ) || + enemy->m_Shared.InCond( TF_COND_URINE ) || + enemy->m_Shared.InCond( TF_COND_STEALTHED_BLINK ) || + enemy->m_Shared.InCond( TF_COND_BLEEDING ) ) + { + // always notice players with these conditions + return false; + } + + if ( enemy->m_Shared.IsStealthed() ) + { + if ( enemy->m_Shared.GetPercentInvisible() < 0.75f ) + { + // spy is partially cloaked, and therefore attracts our attention + return false; + } + + // invisible! + return true; + } + + if ( enemy->IsPlacingSapper() ) + { + return false; + } + + if ( enemy->m_Shared.InCond( TF_COND_DISGUISING ) ) + { + return false; + } + + if ( enemy->m_Shared.InCond( TF_COND_DISGUISED ) && enemy->m_Shared.GetDisguiseTeam() == GetBot()->GetEntity()->GetTeamNumber() ) + { + // spy is disguised as a member of my team + return true; + } + } + + return false; +} + +#endif // TF_RAID_MODE + +#endif // OBSOLETE_USE_BOSS_ALPHA diff --git a/game/server/tf/bot_npc/bot_npc.h b/game/server/tf/bot_npc/bot_npc.h new file mode 100644 index 0000000..b3ad2e7 --- /dev/null +++ b/game/server/tf/bot_npc/bot_npc.h @@ -0,0 +1,541 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// bot_npc.h +// A NextBot non-player derived actor +// Michael Booth, November 2010 + +#ifndef BOT_NPC_H +#define BOT_NPC_H + +#ifdef OBSOLETE_USE_BOSS_ALPHA + +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "NextBotGroundLocomotion.h" +#include "Path/NextBotPathFollow.h" +#include "bot_npc_body.h" +#include "bot/map_entities/tf_spawner_boss.h" + +class CTFPlayer; +class CBotNPC; + + +//---------------------------------------------------------------------------- +class CBotNPCLocomotion : public NextBotGroundLocomotion +{ +public: + CBotNPCLocomotion( INextBot *bot ); + virtual ~CBotNPCLocomotion() { } + + virtual float GetRunSpeed( void ) const; // get maximum running speed + virtual float GetStepHeight( void ) const; // if delta Z is greater than this, we have to jump to get up + virtual float GetMaxJumpHeight( void ) const; // return maximum height of a jump + + virtual float GetMaxAcceleration( void ) const + { + return 2500.0f; + } + +private: + float m_runSpeed; +}; + + +//---------------------------------------------------------------------------- +class CBotNPCIntention : public IIntention +{ +public: + CBotNPCIntention( CBotNPC *me ); + virtual ~CBotNPCIntention(); + + virtual void Reset( void ); + virtual void Update( void ); + + virtual QueryResultType IsPositionAllowed( const INextBot *me, const Vector &pos ) const; // is the a place we can be? + + virtual INextBotEventResponder *FirstContainedResponder( void ) const { return m_behavior; } + virtual INextBotEventResponder *NextContainedResponder( INextBotEventResponder *current ) const { return NULL; } + +private: + Behavior< CBotNPC > *m_behavior; +}; + + +//---------------------------------------------------------------------------- +class CBotNPCVision : public IVision +{ +public: + CBotNPCVision( INextBot *bot ) : IVision( bot ) + { + } + + virtual ~CBotNPCVision() { } + + virtual bool IsIgnored( CBaseEntity *subject ) const; // return true to completely ignore this entity (may not be in sight when this is called) +}; + + +//---------------------------------------------------------------------------- +class CBotNPCWeapon : public CBaseAnimating +{ +public: + CBotNPCWeapon( CBotNPC *owner ) + { + m_owner = owner; + } + + virtual ~CBotNPCWeapon() { } + + virtual void StartAttack( void ) { } + virtual void Update( void ) { } + +private: + CHandle< CBotNPC > m_owner; +}; + + +//---------------------------------------------------------------------------- +class CBotNPCWeapon_Axe : public CBotNPCWeapon +{ +public: + DECLARE_CLASS( CBotNPCWeapon_Axe, CBotNPCWeapon ); + + CBotNPCWeapon_Axe( CBotNPC *owner ); + virtual ~CBotNPCWeapon_Axe() { } + + virtual void StartAttack( void ); + virtual void Update( void ); +}; + + +//---------------------------------------------------------------------------- +//---------------------------------------------------------------------------- +class CBotNPCGetOffMe : public Action< CBotNPC > +{ +public: + virtual ActionResult< CBotNPC > OnStart( CBotNPC *me, Action< CBotNPC > *priorAction ); + virtual ActionResult< CBotNPC > Update( CBotNPC *me, float interval ); + virtual void OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ); + + virtual const char *GetName( void ) const { return "GetOffMe"; } // return name of this action + +private: + CountdownTimer m_timer; +}; + + + +//---------------------------------------------------------------------------- +//---------------------------------------------------------------------------- +class CBotNPC : public NextBotCombatCharacter +{ +public: + DECLARE_CLASS( CBotNPC, NextBotCombatCharacter ); + DECLARE_SERVERCLASS(); + + CBotNPC(); + virtual ~CBotNPC(); + + virtual void Precache(); + virtual void Spawn( void ); + + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + + // INextBot + virtual CBotNPCIntention *GetIntentionInterface( void ) const { return m_intention; } + virtual CBotNPCLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; } + virtual CBotNPCBody *GetBodyInterface( void ) const { return m_body; } + virtual CBotNPCVision *GetVisionInterface( void ) const { return m_vision; } + + virtual void Update( void ); + + virtual bool IsPotentiallyChaseable( CTFPlayer *victim ); + + void SetSpawner( CTFSpawnerBoss *spawner ); // remember the spawner that created us + CTFSpawnerBoss *GetSpawner( void ) const; // return the spawner that created us + + void Break( void ); // bust into gibs + + struct AttackerInfo + { + CHandle< CBaseCombatCharacter > m_attacker; + float m_timestamp; + float m_damage; + bool m_wasCritical; + }; + const CUtlVector< AttackerInfo > &GetAttackerVector( void ) const; + void RememberAttacker( CBaseCombatCharacter *attacker, float damage, bool wasCritical ); + + struct ThreatInfo + { + CHandle< CBaseCombatCharacter > m_who; + float m_threat; + }; + + const ThreatInfo *GetMaxThreat( void ) const; + const ThreatInfo *GetThreat( CBaseCombatCharacter *who ) const; + + void SwingAxe( void ); + void UpdateAxeSwing( void ); + bool IsSwingingAxe( void ) const; + + + //---------------------------------- + enum Ability + { + CAN_BE_STUNNED = 0x01, + CAN_NUKE = 0x02, + CAN_ENRAGE = 0x04, + CAN_FIRE_ROCKETS = 0x08, + CAN_LAUNCH_STICKIES = 0x10, + CAN_LAUNCH_MINIONS = 0x20, + }; + virtual bool HasAbility( Ability ability ) const; + + virtual bool IsMiniBoss( void ) const { return false; } + + virtual float GetMoveSpeed( void ) const { return 300.0f; } + + virtual int GetRocketLaunchCount( void ) const { return 5; } + virtual float GetRocketDamage( void ) const { return 25.0f; } + virtual float GetRocketAimError( void ) const { return 1.81f; } + virtual float GetRocketInterval( void ) const { return 0.3f; } + virtual const char *GetRocketSoundEffect( void ) const { return "Weapon_RPG.Single"; } + + virtual float GetGrenadeInterval( void ) const { return 10.0f; } + + virtual float GetBecomeStunnedDamage( void ) const { return 500.0f; } + + + //---------------------------------- + enum Condition + { + SHIELDED = 0x01, + CHARGING = 0x02, + STUNNED = 0x04, + INVULNERABLE = 0x08, + VULNERABLE_TO_STUN = 0x10, + BUSY = 0x20, + ENRAGED = 0x40, + }; + + bool IsBusy( void ) const; // returns true if we're in a condition that means we can't start another action + + void AddCondition( Condition c ); + void RemoveCondition( Condition c ); + bool IsInCondition( Condition c ) const; + + bool IsAttackTarget( CBaseCombatCharacter *target ) const; + bool HasAttackTarget( void ) const; + void SetAttackTarget( CBaseCombatCharacter *target, float duration = 0.0f ); + CBaseCombatCharacter *GetAttackTarget( void ) const; + void LockAttackTarget( void ); // don't allow target to change until it is unlocked or the target is destroyed + void UnlockAttackTarget( void ); + + CBaseCombatCharacter *GetNearestVisibleEnemy( void ) const; + + void SetHomePosition( const Vector &pos ); + const Vector &GetHomePosition( void ) const; + + CBaseAnimating *GetWeapon( void ) const; + CBaseAnimating *GetShield( void ) const; + + CountdownTimer *GetNukeTimer( void ); + CountdownTimer *GetGrenadeTimer( void ); + + float GetReceivedDamagePerSecond( void ) const; + float GetReceivedDamagePerSecondDelta( void ) const; + + void SetLaserTarget( CBaseEntity *target ); + CBaseEntity *GetLaserTarget( void ) const; + + void ClearStunDamage( void ); + void AccumulateStunDamage( float damage ); + float GetStunDamage( void ) const; + + CTFPlayer *GetClosestMinionPrisoner( void ); + bool IsPrisonerOfMinion( CBaseCombatCharacter *victim ); + + void StartNukeEffect( void ); + void StopNukeEffect( void ); + + float GetAge( void ) const; // how long have we been alive + + void CollectPlayersStandingOnMe( CUtlVector< CTFPlayer * > *playerVector ); + + // Entity I/O + void InputSpawn( inputdata_t &inputdata ); + COutputEvent m_outputOnStunned; // fired the boss becomes stunned + +private: + CBotNPCIntention *m_intention; + CBotNPCLocomotion *m_locomotor; + CBotNPCBody *m_body; + CBotNPCVision *m_vision; + + CHandle< CTFSpawnerBoss > m_spawner; + + CBaseAnimating *m_axe; + CBaseAnimating *m_shield; + + void PrecacheArmorParts( void ); + void InstallArmorParts( void ); + CUtlVector< CBaseAnimating * > m_armorPartVector; + + CountdownTimer m_axeSwingTimer; + CountdownTimer m_attackTimer; + CountdownTimer m_nukeTimer; + CountdownTimer m_grenadeTimer; + CountdownTimer m_ouchTimer; + CountdownTimer m_hateTauntTimer; + + CNetworkHandle( CBaseEntity, m_laserTarget ); + CNetworkVar( bool, m_isNuking ); + + CHandle< CBaseCombatCharacter > m_nearestVisibleEnemy; + void UpdateNearestVisibleEnemy( void ); + CountdownTimer m_nearestVisibleEnemyTimer; + + CUtlVector< AttackerInfo > m_attackerVector; // list of everyone who injured me, and when + CUtlVector< ThreatInfo > m_threatVector; // list of attackers and their current damage/second on me + + float m_currentDamagePerSecond; + float m_lastDamagePerSecond; + void UpdateDamagePerSecond( void ); + + CHandle< CBaseCombatCharacter > m_attackTarget; + CountdownTimer m_attackTargetTimer; + bool m_isAttackTargetLocked; + void UpdateAttackTarget( void ); + + int m_damagePoseParameter; + + bool m_isShielded; + Vector m_homePos; + + bool IsIgnored( CTFPlayer *player ) const; + + unsigned int m_conditionFlags; + + float m_stunDamage; + + IntervalTimer m_ageTimer; +}; + + +inline bool CBotNPC::HasAbility( Ability ability ) const +{ + const int myAbilities = CAN_BE_STUNNED | CAN_NUKE | CAN_ENRAGE | CAN_FIRE_ROCKETS | CAN_LAUNCH_STICKIES | CAN_LAUNCH_MINIONS; + + return myAbilities & ability ? true : false; +} + +inline void CBotNPC::SetSpawner( CTFSpawnerBoss *spawner ) +{ + m_spawner = spawner; +} + +inline CTFSpawnerBoss *CBotNPC::GetSpawner( void ) const +{ + return m_spawner; +} + +inline bool CBotNPC::IsAttackTarget( CBaseCombatCharacter *target ) const +{ + if ( HasAttackTarget() ) + { + return ( m_attackTarget == target ) ? true : false; + } + return false; +} + +inline bool CBotNPC::HasAttackTarget( void ) const +{ + return ( m_attackTarget == NULL || !m_attackTarget->IsAlive() ) ? false : true; +} + +inline void CBotNPC::LockAttackTarget( void ) +{ + m_isAttackTargetLocked = HasAttackTarget(); +} + +inline void CBotNPC::UnlockAttackTarget( void ) +{ + m_isAttackTargetLocked = false; +} + +inline float CBotNPC::GetAge( void ) const +{ + return m_ageTimer.GetElapsedTime(); +} + +inline void CBotNPC::StartNukeEffect( void ) +{ + m_isNuking = true; +} + +inline void CBotNPC::StopNukeEffect( void ) +{ + m_isNuking = false; +} + +inline void CBotNPC::ClearStunDamage( void ) +{ + m_stunDamage = 0.0f; +} + +inline void CBotNPC::AccumulateStunDamage( float damage ) +{ + m_stunDamage += damage; +} + +inline float CBotNPC::GetStunDamage( void ) const +{ + return m_stunDamage; +} + +inline void CBotNPC::SetLaserTarget( CBaseEntity *target ) +{ + m_laserTarget = target; +} + +inline CBaseEntity *CBotNPC::GetLaserTarget( void ) const +{ + return m_laserTarget; +} + +inline float CBotNPC::GetReceivedDamagePerSecond( void ) const +{ + return m_currentDamagePerSecond; +} + +inline float CBotNPC::GetReceivedDamagePerSecondDelta( void ) const +{ + return m_currentDamagePerSecond - m_lastDamagePerSecond; +} + +inline CountdownTimer *CBotNPC::GetNukeTimer( void ) +{ + return &m_nukeTimer; +} + +inline CountdownTimer *CBotNPC::GetGrenadeTimer( void ) +{ + return &m_grenadeTimer; +} + +inline CBaseAnimating *CBotNPC::GetWeapon( void ) const +{ + return m_axe; +} + +inline CBaseAnimating *CBotNPC::GetShield( void ) const +{ + return m_shield; +} + +inline void CBotNPC::SetHomePosition( const Vector &pos ) +{ + m_homePos = pos; +} + +inline const Vector &CBotNPC::GetHomePosition( void ) const +{ + return m_homePos; +} + +inline CBaseCombatCharacter *CBotNPC::GetNearestVisibleEnemy( void ) const +{ + return m_nearestVisibleEnemy; +} + +inline void CBotNPC::AddCondition( Condition c ) +{ + m_conditionFlags |= c; +} + +inline bool CBotNPC::IsInCondition( Condition c ) const +{ + return ( m_conditionFlags & c ) ? true : false; +} + +inline const CUtlVector< CBotNPC::AttackerInfo > &CBotNPC::GetAttackerVector( void ) const +{ + return m_attackerVector; +} + + +//-------------------------------------------------------------------------------------------------------------- +class CBotNPCPathCost : public IPathCost +{ +public: + CBotNPCPathCost( CBotNPC *me ) + { + m_me = me; + } + + // return the cost (weighted distance between) of moving from "fromArea" to "area", or -1 if the move is not allowed + virtual float operator()( CNavArea *area, CNavArea *fromArea, const CNavLadder *ladder, const CFuncElevator *elevator, float length ) const + { + if ( fromArea == NULL ) + { + // first area in path, no cost + return 0.0f; + } + else + { + if ( !m_me->GetLocomotionInterface()->IsAreaTraversable( area ) ) + { + // our locomotor says we can't move here + return -1.0f; + } + + // compute distance traveled along path so far + float dist; + + if ( ladder ) + { + dist = ladder->m_length; + } + else if ( length > 0.0 ) + { + // optimization to avoid recomputing length + dist = length; + } + else + { + dist = ( area->GetCenter() - fromArea->GetCenter() ).Length(); + } + + float cost = dist + fromArea->GetCostSoFar(); + + // check height change + float deltaZ = fromArea->ComputeAdjacentConnectionHeightChange( area ); + if ( deltaZ >= m_me->GetLocomotionInterface()->GetStepHeight() ) + { + if ( deltaZ >= m_me->GetLocomotionInterface()->GetMaxJumpHeight() ) + { + // too high to reach + return -1.0f; + } + + // jumping is slower than flat ground + const float jumpPenalty = 5.0f; + cost += jumpPenalty * dist; + } + else if ( deltaZ < -m_me->GetLocomotionInterface()->GetDeathDropHeight() ) + { + // too far to drop + return -1.0f; + } + + return cost; + } + } + + CBotNPC *m_me; +}; + + +#endif // #ifdef OBSOLETE_USE_BOSS_ALPHA + +#endif // BOT_NPC_H diff --git a/game/server/tf/bot_npc/bot_npc_archer.cpp b/game/server/tf/bot_npc/bot_npc_archer.cpp new file mode 100644 index 0000000..592def4 --- /dev/null +++ b/game/server/tf/bot_npc/bot_npc_archer.cpp @@ -0,0 +1,402 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// bot_npc_archer.cpp +// A NextBot non-player derived archer +// Michael Booth, November 2010 + +#include "cbase.h" + +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "tf_projectile_arrow.h" +#include "tf_weapon_grenade_pipebomb.h" +#include "nav_mesh/tf_nav_area.h" +#include "bot_npc_archer.h" +#include "NextBot/Path/NextBotChasePath.h" +#include "econ_wearable.h" +#include "team_control_point_master.h" +#include "particle_parse.h" +#include "CRagdollMagnet.h" +#include "NextBot/Behavior/BehaviorMoveTo.h" + +ConVar tf_bot_npc_archer_health( "tf_bot_npc_archer_health", "100", FCVAR_CHEAT ); + +ConVar tf_bot_npc_archer_speed( "tf_bot_npc_archer_speed", "100", FCVAR_CHEAT ); + +ConVar tf_bot_npc_archer_shoot_interval( "tf_bot_npc_archer_shoot_interval", "2", FCVAR_CHEAT ); // 2 +ConVar tf_bot_npc_archer_arrow_damage( "tf_bot_npc_archer_arrow_damage", "75", FCVAR_CHEAT ); + + +//----------------------------------------------------------------- +// The Bot NPC +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( bot_npc_archer, CBotNPCArcher ); + +PRECACHE_REGISTER( bot_npc_archer ); + + +//----------------------------------------------------------------------------------------------------- +CBotNPCArcher::CBotNPCArcher() +{ + ALLOCATE_INTENTION_INTERFACE( CBotNPCArcher ); + + m_locomotor = new NextBotGroundLocomotion( this ); + m_body = new CBotNPCBody( this ); + + m_eyeOffset = vec3_origin; + m_homePos = vec3_origin; +} + + +//----------------------------------------------------------------------------------------------------- +CBotNPCArcher::~CBotNPCArcher() +{ + DEALLOCATE_INTENTION_INTERFACE; + + if ( m_locomotor ) + delete m_locomotor; + + if ( m_body ) + delete m_body; +} + +//----------------------------------------------------------------------------------------------------- +void CBotNPCArcher::Precache() +{ + BaseClass::Precache(); + + PrecacheModel( "models/player/sniper.mdl" ); + PrecacheModel( "models/weapons/c_models/c_bow/c_bow.mdl" ); +} + + +//----------------------------------------------------------------------------------------------------- +void CBotNPCArcher::Spawn( void ) +{ + BaseClass::Spawn(); + + SetModel( "models/player/sniper.mdl" ); + + m_bow = (CBaseAnimating *)CreateEntityByName( "prop_dynamic" ); + if ( m_bow ) + { + m_bow->SetModel( "models/weapons/c_models/c_bow/c_bow.mdl" ); + + // bonemerge into our model + m_bow->FollowEntity( this, true ); + } + + int health = tf_bot_npc_archer_health.GetInt(); + SetHealth( health ); + SetMaxHealth( health ); + + ChangeTeam( TF_TEAM_RED ); + + Vector headPos; + QAngle headAngles; + if ( GetAttachment( "head", headPos, headAngles ) ) + { + m_eyeOffset = headPos - GetAbsOrigin(); + } + + m_homePos = GetAbsOrigin(); +} + + +//--------------------------------------------------------------------------------------------- +unsigned int CBotNPCArcher::PhysicsSolidMaskForEntity( void ) const +{ + // Only collide with the other team + int teamContents = ( GetTeamNumber() == TF_TEAM_RED ) ? CONTENTS_BLUETEAM : CONTENTS_REDTEAM; + + return BaseClass::PhysicsSolidMaskForEntity() | teamContents; +} + + +//--------------------------------------------------------------------------------------------- +bool CBotNPCArcher::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 ); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCArcherSurrender : public Action< CBotNPCArcher > +{ +public: + virtual ActionResult< CBotNPCArcher > OnStart( CBotNPCArcher *me, Action< CBotNPCArcher > *priorAction ); + virtual const char *GetName( void ) const { return "Surrender"; } // return name of this action +}; + + +inline ActionResult< CBotNPCArcher > CBotNPCArcherSurrender::OnStart( CBotNPCArcher *me, Action< CBotNPCArcher > *priorAction ) +{ + CBaseAnimating *bow = me->GetBow(); + if ( bow ) + { + bow->AddEffects( EF_NODRAW ); + } + + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_LOSERSTATE ); + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCArcherShootBow : public Action< CBotNPCArcher > +{ +public: + CBotNPCArcherShootBow( CTFPlayer *target ) + { + m_target = target; + } + + virtual ActionResult< CBotNPCArcher > OnStart( CBotNPCArcher *me, Action< CBotNPCArcher > *priorAction ); + virtual ActionResult< CBotNPCArcher > Update( CBotNPCArcher *me, float interval ); + + virtual const char *GetName( void ) const { return "ShootBow"; } // return name of this action + +private: + CHandle< CTFPlayer > m_target; +}; + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCArcher > CBotNPCArcherShootBow::OnStart( CBotNPCArcher *me, Action< CBotNPCArcher > *priorAction ) +{ + if ( !m_target ) + { + return Done( "No target" ); + } + + me->GetLocomotionInterface()->FaceTowards( m_target->WorldSpaceCenter() ); + me->AddGesture( ACT_MP_ATTACK_STAND_ITEM2 ); + + // fire arrow + const float arrowSpeed = 2000.0f; + const float arrowGravity = 0.2f; + + Vector muzzleOrigin; + QAngle muzzleAngles; + if ( me->GetBow()->GetAttachment( "muzzle", muzzleOrigin, muzzleAngles ) == false ) + { + return Done( "No muzzle attachment!" ); + } + + // lead target + float range = me->GetRangeTo( m_target->EyePosition() ); + float flightTime = range / arrowSpeed; + + Vector aimSpot = m_target->EyePosition() + m_target->GetAbsVelocity() * flightTime; + + Vector to = aimSpot - muzzleOrigin; + VectorAngles( to, muzzleAngles ); + + CTFProjectile_Arrow *arrow = CTFProjectile_Arrow::Create( muzzleOrigin, muzzleAngles, arrowSpeed, arrowGravity, TF_PROJECTILE_ARROW, me, me ); + if ( arrow ) + { + arrow->SetLauncher( me ); + arrow->SetCritical( false ); + + arrow->SetDamage( tf_bot_npc_archer_arrow_damage.GetFloat() ); + + me->EmitSound( "Weapon_CompoundBow.Single" ); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +ActionResult< CBotNPCArcher > CBotNPCArcherShootBow::Update( CBotNPCArcher *me, float interval ) +{ + if ( me->IsSequenceFinished() ) + { + return Done(); + } + + return Continue(); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCArcherGuardSpot : public Action< CBotNPCArcher > +{ +public: + virtual ActionResult< CBotNPCArcher > OnStart( CBotNPCArcher *me, Action< CBotNPCArcher > *priorAction ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM2 ); + + return Continue(); + } + + CTFPlayer *GetVictim( CBotNPCArcher *me ) + { + 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 ) + { + float rangeSq = me->GetRangeSquaredTo( playerVector[i] ); + if ( rangeSq < victimRangeSq ) + { + if ( playerVector[i]->m_Shared.IsStealthed() ) + { + continue; + } + + if ( me->IsLineOfSightClear( playerVector[i] ) ) + { + closeVictim = playerVector[i]; + victimRangeSq = rangeSq; + } + } + } + + return closeVictim; + } + + virtual ActionResult< CBotNPCArcher > Update( CBotNPCArcher *me, float interval ) + { + if ( TFGameRules()->GetActiveBoss() == NULL ) + { + // the Boss has been defeated - give up + return ChangeTo( new CBotNPCArcherSurrender, "The Boss is dead! I give up!" ); + } + + CTFPlayer *victim = GetVictim( me ); + + if ( victim ) + { + // look at visible victim out of range + me->GetLocomotionInterface()->FaceTowards( victim->WorldSpaceCenter() ); + + if ( m_shootTimer.IsElapsed() ) + { + m_shootTimer.Start( tf_bot_npc_archer_shoot_interval.GetFloat() ); + + return SuspendFor( new CBotNPCArcherShootBow( victim ), "Fire!" ); + } + } + + if ( me->GetLocomotionInterface()->IsAttemptingToMove() ) + { + // play running animation + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_DEPLOYED_IDLE_ITEM2 ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_DEPLOYED_IDLE_ITEM2 ); + } + } + else + { + // standing still + if ( !me->GetBodyInterface()->IsActivity( ACT_MP_STAND_ITEM2 ) ) + { + me->GetBodyInterface()->StartActivity( ACT_MP_STAND_ITEM2 ); + } + } + + return Continue(); + } + + virtual const char *GetName( void ) const { return "GuardSpot"; } // return name of this action + +private: + CountdownTimer m_shootTimer; +}; + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCArcherMoveToMark : public Action< CBotNPCArcher > +{ +public: + virtual ActionResult< CBotNPCArcher > OnStart( CBotNPCArcher *me, Action< CBotNPCArcher > *priorAction ) + { + ShortestPathCost cost; + m_path.Compute( me, me->GetHomePosition(), cost ); + + me->GetBodyInterface()->StartActivity( ACT_MP_RUN_ITEM2 ); + + return Continue(); + } + + virtual ActionResult< CBotNPCArcher > Update( CBotNPCArcher *me, float interval ) + { + m_path.Update( me ); + + if ( !m_path.IsValid() ) + { + return ChangeTo( new CBotNPCArcherGuardSpot, "Reached my mark" ); + } + + return Continue(); + } + + virtual const char *GetName( void ) const { return "MoveToMark"; } // return name of this action + +private: + PathFollower m_path; +}; + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCArcherBehavior : public Action< CBotNPCArcher > +{ +public: + virtual Action< CBotNPCArcher > *InitialContainedAction( CBotNPCArcher *me ) + { + return new CBotNPCArcherMoveToMark; + } + + virtual ActionResult< CBotNPCArcher > Update( CBotNPCArcher *me, float interval ) + { + return Continue(); + } + + virtual EventDesiredResult< CBotNPCArcher > OnKilled( CBotNPCArcher *me, const CTakeDamageInfo &info ) + { + // Calculate death force + Vector forceVector = me->CalcDamageForceVector( info ); + + // See if there's a ragdoll magnet that should influence our force. + CRagdollMagnet *magnet = CRagdollMagnet::FindBestMagnet( me ); + if ( magnet ) + { + forceVector += magnet->GetForceVector( me ); + } + + me->BecomeRagdoll( info, forceVector ); + + return TryDone(); + } + + virtual const char *GetName( void ) const { return "Behavior"; } // return name of this action +}; + + +IMPLEMENT_INTENTION_INTERFACE( CBotNPCArcher, CBotNPCArcherBehavior ); diff --git a/game/server/tf/bot_npc/bot_npc_archer.h b/game/server/tf/bot_npc/bot_npc_archer.h new file mode 100644 index 0000000..6b7973b --- /dev/null +++ b/game/server/tf/bot_npc/bot_npc_archer.h @@ -0,0 +1,79 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// bot_npc_archer.h +// A NextBot non-player derived archer +// Michael Booth, November 2010 + +#ifndef BOT_NPC_ARCHER_H +#define BOT_NPC_ARCHER_H + +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "NextBotGroundLocomotion.h" +#include "Path/NextBotPathFollow.h" +#include "bot_npc.h" +#include "bot_npc_body.h" + + +class CTFPlayer; + + +//---------------------------------------------------------------------------- +class CBotNPCArcher : public NextBotCombatCharacter +{ +public: + DECLARE_CLASS( CBotNPCArcher, NextBotCombatCharacter ); + + CBotNPCArcher(); + virtual ~CBotNPCArcher(); + + virtual void Precache(); + virtual void Spawn( void ); + + // INextBot + DECLARE_INTENTION_INTERFACE( CBotNPCArcher ); + virtual NextBotGroundLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; } + virtual CBotNPCBody *GetBodyInterface( void ) const { return m_body; } + + virtual Vector EyePosition( void ); + + virtual unsigned int PhysicsSolidMaskForEntity( void ) const; + virtual bool ShouldCollide( int collisionGroup, int contentsMask ) const; + + CBaseAnimating *GetBow( void ) const; + + void SetHomePosition( const Vector &pos ); + const Vector &GetHomePosition( void ) const; + +private: + NextBotGroundLocomotion *m_locomotor; + CBotNPCBody *m_body; + + CBaseAnimating *m_bow; + Vector m_eyeOffset; + + Vector m_homePos; +}; + + +inline void CBotNPCArcher::SetHomePosition( const Vector &pos ) +{ + m_homePos = pos; +} + +inline const Vector &CBotNPCArcher::GetHomePosition( void ) const +{ + return m_homePos; +} + +inline Vector CBotNPCArcher::EyePosition( void ) +{ + return GetAbsOrigin() + m_eyeOffset; +} + + +inline CBaseAnimating *CBotNPCArcher::GetBow( void ) const +{ + return m_bow; +} + +#endif // BOT_NPC_ARCHER_H diff --git a/game/server/tf/bot_npc/bot_npc_body.cpp b/game/server/tf/bot_npc/bot_npc_body.cpp new file mode 100644 index 0000000..3d6ce8e --- /dev/null +++ b/game/server/tf/bot_npc/bot_npc_body.cpp @@ -0,0 +1,153 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#include "cbase.h" + +#include "NextBot.h" +//#include "bot_npc.h" +#include "bot_npc_body.h" + +//------------------------------------------------------------------------------------------- +CBotNPCBody::CBotNPCBody( INextBot *bot ) : IBody( bot ) +{ + m_moveXPoseParameter = -1; + m_moveYPoseParameter = -1; + m_currentActivity = -1; + m_desiredAimAngles = vec3_angle; +} + + +//------------------------------------------------------------------------------------------- +bool CBotNPCBody::StartActivity( Activity act, unsigned int flags ) +{ + NextBotCombatCharacter *me = (NextBotCombatCharacter *)GetBot()->GetEntity(); + + int animSequence = ::SelectWeightedSequence( me->GetModelPtr(), act, me->GetSequence() ); + + if ( animSequence ) + { + m_currentActivity = act; + me->SetSequence( animSequence ); + me->SetPlaybackRate( 1.0f ); + me->SetCycle( 0 ); + me->ResetSequenceInfo(); + + return true; + } + + return false; +} + + +//------------------------------------------------------------------------------------------- +void CBotNPCBody::Update( void ) +{ + NextBotCombatCharacter *me = (NextBotCombatCharacter *)GetBot()->GetEntity(); + + if ( m_moveXPoseParameter < 0 ) + { + m_moveXPoseParameter = me->LookupPoseParameter( "move_x" ); + } + + if ( m_moveYPoseParameter < 0 ) + { + m_moveYPoseParameter = me->LookupPoseParameter( "move_y" ); + } + + + // Update the pose parameters + float speed = me->GetLocomotionInterface()->GetGroundSpeed(); // me->GetAbsVelocity().Length(); + + if ( speed < 0.01f ) + { + // stopped + if ( m_moveXPoseParameter >= 0 ) + { + me->SetPoseParameter( m_moveXPoseParameter, 0.0f ); + } + + if ( m_moveYPoseParameter >= 0 ) + { + me->SetPoseParameter( m_moveYPoseParameter, 0.0f ); + } + } + else + { + Vector forward, right, up; + me->GetVectors( &forward, &right, &up ); + + const Vector &motionVector = me->GetLocomotionInterface()->GetGroundMotionVector(); + + // move_x == 1.0 at full forward motion and -1.0 in full reverse + if ( m_moveXPoseParameter >= 0 ) + { + float forwardVel = DotProduct( motionVector, forward ); + + me->SetPoseParameter( m_moveXPoseParameter, forwardVel ); + } + + if ( m_moveYPoseParameter >= 0 ) + { + float sideVel = DotProduct( motionVector, right ); + + me->SetPoseParameter( m_moveYPoseParameter, sideVel ); + } + } + + // adjust animation speed to actual movement speed + if ( me->m_flGroundSpeed > 0.0f ) + { + // Clamp playback rate to avoid datatable warnings. Anything faster would look silly, anyway. + float playbackRate = clamp( speed / me->m_flGroundSpeed, -4.f, 12.f ); + me->SetPlaybackRate( playbackRate ); + } + + // move the animation ahead in time + me->StudioFrameAdvance(); + me->DispatchAnimEvents( me ); + + // update aim angles + QAngle currentAngles = me->GetAbsAngles(); + + QAngle angles; + const float approachRate = GetMaxHeadAngularVelocity(); // 3000.0f; + angles.y = ApproachAngle( m_desiredAimAngles.y, currentAngles.y, approachRate * TICK_INTERVAL ); + angles.x = ApproachAngle( m_desiredAimAngles.x, currentAngles.x, 0.5f * approachRate * TICK_INTERVAL ); + angles.z = 0.0f; + + angles.x = AngleNormalize( angles.x ); + angles.y = AngleNormalize( angles.y ); + + me->SetAbsAngles( angles ); +} + + +//--------------------------------------------------------------------------------------------- +// return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) +unsigned int CBotNPCBody::GetSolidMask( void ) const +{ + return MASK_NPCSOLID | CONTENTS_PLAYERCLIP; +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCBody::AimHeadTowards( const Vector &lookAtPos, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason ) +{ + CBaseCombatCharacter *me = GetBot()->GetEntity(); + + Vector toTarget = lookAtPos - me->WorldSpaceCenter(); + VectorAngles( toTarget, m_desiredAimAngles ); +} + + +//--------------------------------------------------------------------------------------------- +void CBotNPCBody::AimHeadTowards( CBaseEntity *subject, LookAtPriorityType priority, float duration, INextBotReply *replyWhenAimed, const char *reason ) +{ + if ( !subject ) + return; + + CBaseCombatCharacter *me = GetBot()->GetEntity(); + + Vector toTarget = subject->WorldSpaceCenter() - me->WorldSpaceCenter(); + + QAngle angles; + VectorAngles( toTarget, m_desiredAimAngles ); +} diff --git a/game/server/tf/bot_npc/bot_npc_body.h b/game/server/tf/bot_npc/bot_npc_body.h new file mode 100644 index 0000000..3d58bee --- /dev/null +++ b/game/server/tf/bot_npc/bot_npc_body.h @@ -0,0 +1,64 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +#ifndef BOT_NPC_BODY_H +#define BOT_NPC_BODY_H + +#include "animation.h" +#include "NextBotBodyInterface.h" + +class INextBot; + + +//---------------------------------------------------------------------------------------------------------------- +/** + * The interface for control and information about the bot's body state (posture, animation state, etc) + */ +class CBotNPCBody : public IBody +{ +public: + CBotNPCBody( INextBot *bot ); + virtual ~CBotNPCBody() { } + + virtual void Update( void ); + + virtual bool StartActivity( Activity act, unsigned int flags = 0 ); + virtual Activity GetActivity( void ) const; // return currently animating activity + virtual bool IsActivity( Activity act ) const; // return true if currently animating activity matches the given one + + virtual void AimHeadTowards( const Vector &lookAtPos, + LookAtPriorityType priority = BORING, + float duration = 0.0f, + INextBotReply *replyWhenAimed = NULL, + const char *reason = NULL ); // aim the bot's head towards the given goal + virtual void AimHeadTowards( CBaseEntity *subject, + LookAtPriorityType priority = BORING, + float duration = 0.0f, + INextBotReply *replyWhenAimed = NULL, + const char *reason = NULL ); // continually aim the bot's head towards the given subject + + virtual unsigned int GetSolidMask( void ) const; // return the bot's collision mask (hack until we get a general hull trace abstraction here or in the locomotion interface) + virtual unsigned int GetCollisionGroup( void ) const; + +private: + int m_currentActivity; + int m_moveXPoseParameter; + int m_moveYPoseParameter; + QAngle m_desiredAimAngles; +}; + + +inline Activity CBotNPCBody::GetActivity( void ) const +{ + return (Activity)m_currentActivity; +} + +inline bool CBotNPCBody::IsActivity( Activity act ) const +{ + return act == m_currentActivity ? true : false; +} + +inline unsigned int CBotNPCBody::GetCollisionGroup( void ) const +{ + return COLLISION_GROUP_PLAYER_MOVEMENT; +} + +#endif // BOT_NPC_BODY_H diff --git a/game/server/tf/bot_npc/bot_npc_decoy.cpp b/game/server/tf/bot_npc/bot_npc_decoy.cpp new file mode 100644 index 0000000..757a467 --- /dev/null +++ b/game/server/tf/bot_npc/bot_npc_decoy.cpp @@ -0,0 +1,246 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// bot_npc_decoy.cpp +// A NextBot non-player decoy that imitates a real player +// Michael Booth, January 2011 + +#include "cbase.h" +#include "tf_player.h" +#include "tf_gamerules.h" +#include "tf_team.h" +#include "nav_mesh/tf_nav_area.h" +#include "bot_npc_decoy.h" +#include "econ_wearable.h" + +LINK_ENTITY_TO_CLASS( bot_npc_decoy, CBotNPCDecoy ); +PRECACHE_REGISTER( bot_npc_decoy ); + +ConVar tf_decoy_lifetime( "tf_decoy_lifetime", "5", FCVAR_CHEAT, "The lifetime of a decoy, in seconds" ); + + +//----------------------------------------------------------------------------------------------------- +CBotNPCDecoy::CBotNPCDecoy() +{ + ALLOCATE_INTENTION_INTERFACE( CBotNPCDecoy ); + + m_locomotor = new CBotNPCDecoyLocomotion( this ); + m_body = new CBotNPCBody( this ); + + m_eyeOffset = vec3_origin; +} + + +//----------------------------------------------------------------------------------------------------- +CBotNPCDecoy::~CBotNPCDecoy() +{ + DEALLOCATE_INTENTION_INTERFACE; + + if ( m_locomotor ) + delete m_locomotor; + + if ( m_body ) + delete m_body; +} + +//----------------------------------------------------------------------------------------------------- +void CBotNPCDecoy::Precache() +{ + BaseClass::Precache(); +} + + +//----------------------------------------------------------------------------------------------------- +void CBotNPCDecoy::Spawn( void ) +{ + BaseClass::Spawn(); + + SetCollisionGroup( COLLISION_GROUP_NONE ); + SetSolid( SOLID_NONE ); + AddSolidFlags( FSOLID_NOT_SOLID ); + + CTFPlayer *owner = ToTFPlayer( GetOwnerEntity() ); + if ( !owner ) + { + Warning( "Decoy spawned without an owner\n" ); + return; + } + + int ownerClass = owner->m_Shared.InCond( TF_COND_DISGUISED ) ? owner->m_Shared.GetDisguiseClass() : owner->GetPlayerClass()->GetClassIndex(); + int ownerTeam = owner->m_Shared.InCond( TF_COND_DISGUISED ) ? owner->m_Shared.GetDisguiseTeam() : owner->GetTeamNumber(); + + SetModel( GetPlayerClassData( ownerClass )->m_szModelName ); + ChangeTeam( ownerTeam ); + + if ( ownerTeam == TF_TEAM_BLUE ) + { + m_nSkin = 1; + } + else + { + m_nSkin = 0; + } + + SetAbsOrigin( owner->GetAbsOrigin() ); + SetAbsAngles( owner->GetAbsAngles() ); + SetAbsVelocity( owner->GetAbsVelocity() ); + + Vector headPos; + QAngle headAngles; + if ( GetAttachment( "head", headPos, headAngles ) ) + { + m_eyeOffset = headPos - GetAbsOrigin(); + } + + CTFWeaponBase *theirWeapon = owner->m_Shared.GetDisguiseWeapon(); + if ( !theirWeapon ) + { + theirWeapon = owner->GetActiveTFWeapon(); + } + + if ( theirWeapon ) + { + CBaseAnimating *weapon = (CBaseAnimating *)CreateEntityByName( "prop_dynamic" ); + if ( weapon ) + { + weapon->SetModel( theirWeapon->GetWorldModel() ); + + // bonemerge the weapon into our model + weapon->FollowEntity( this, true ); + + // choose the appropriate run animation for this weapon + switch( theirWeapon->GetTFWpnData().m_iWeaponType ) + { + case TF_WPN_TYPE_PRIMARY: + m_runActivity = ACT_MP_RUN_PRIMARY; + break; + + case TF_WPN_TYPE_SECONDARY: + m_runActivity = ACT_MP_RUN_SECONDARY; + break; + + case TF_WPN_TYPE_MELEE: + default: + m_runActivity = ACT_MP_RUN_MELEE; + break; + } + } + } +} + + +//--------------------------------------------------------------------------------------------- +unsigned int CBotNPCDecoy::PhysicsSolidMaskForEntity( void ) const +{ + // Only collide with the other team + int teamContents = ( GetTeamNumber() == TF_TEAM_RED ) ? CONTENTS_BLUETEAM : CONTENTS_REDTEAM; + + return BaseClass::PhysicsSolidMaskForEntity() | teamContents; +} + + +//--------------------------------------------------------------------------------------------- +bool CBotNPCDecoy::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 ); +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +float CBotNPCDecoyLocomotion::GetRunSpeed( void ) const +{ + CTFPlayer *owner = ToTFPlayer( GetBot()->GetEntity()->GetOwnerEntity() ); + if ( !owner ) + { + return 0.0f; + } + + int ownerClass = owner->m_Shared.InCond( TF_COND_DISGUISED ) ? owner->m_Shared.GetDisguiseClass() : owner->GetPlayerClass()->GetClassIndex(); + return GetPlayerClassData( ownerClass )->m_flMaxSpeed; +} + + +//--------------------------------------------------------------------------------------------- +// return maximum acceleration of locomotor +float CBotNPCDecoyLocomotion::GetMaxAcceleration( void ) const +{ + return 1500.0f; +} + + +//--------------------------------------------------------------------------------------------- +// return maximum deceleration of locomotor +float CBotNPCDecoyLocomotion::GetMaxDeceleration( void ) const +{ + return 1500.0f; +} + + +//--------------------------------------------------------------------------------------------- +//--------------------------------------------------------------------------------------------- +class CBotNPCDecoyBehavior : public Action< CBotNPCDecoy > +{ +public: + virtual ActionResult< CBotNPCDecoy > OnStart( CBotNPCDecoy *me, Action< CBotNPCDecoy > *priorAction ) + { + m_timer.Start( tf_decoy_lifetime.GetFloat() ); + + // play running animation + if ( !me->GetBodyInterface()->IsActivity( me->GetRunActivity() ) ) + { + me->GetBodyInterface()->StartActivity( me->GetRunActivity() ); + } + + return Continue(); + } + + virtual ActionResult< CBotNPCDecoy > Update( CBotNPCDecoy *me, float interval ) + { + if ( m_timer.IsElapsed() ) + { + // we're out of time + UTIL_Remove( me ); + return Done( "Lifetime expired" ); + } + + CTFPlayer *owner = ToTFPlayer( me->GetOwnerEntity() ); + if ( !owner ) + { + UTIL_Remove( me ); + return Done( "No owner!" ); + } + + Vector forward; + me->GetVectors( &forward, NULL, NULL ); + + me->GetLocomotionInterface()->SetDesiredSpeed( FLT_MAX ); // this is just a rate limiter + me->GetLocomotionInterface()->Run(); + me->GetLocomotionInterface()->Approach( me->GetAbsOrigin() + 100.0f * forward ); + + return Continue(); + } + + virtual const char *GetName( void ) const { return "Behavior"; } // return name of this action + +private: + CountdownTimer m_timer; +}; + + +IMPLEMENT_INTENTION_INTERFACE( CBotNPCDecoy, CBotNPCDecoyBehavior ); + diff --git a/game/server/tf/bot_npc/bot_npc_decoy.h b/game/server/tf/bot_npc/bot_npc_decoy.h new file mode 100644 index 0000000..6c6227f --- /dev/null +++ b/game/server/tf/bot_npc/bot_npc_decoy.h @@ -0,0 +1,80 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// bot_npc_decoy.h +// A NextBot non-player decoy that imitates a real player +// Michael Booth, January 2011 + +#ifndef BOT_NPC_DECOY_H +#define BOT_NPC_DECOY_H + +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "NextBotGroundLocomotion.h" +#include "Path/NextBotPathFollow.h" +#include "bot_npc.h" +#include "bot_npc_body.h" + + +class CTFPlayer; + + +//---------------------------------------------------------------------------- +class CBotNPCDecoyLocomotion : public NextBotGroundLocomotion +{ +public: + DECLARE_CLASS( CBotNPCDecoyLocomotion, NextBotGroundLocomotion ); + + CBotNPCDecoyLocomotion( INextBot *bot ) : NextBotGroundLocomotion( bot ) { } + virtual ~CBotNPCDecoyLocomotion() { } + + virtual float GetRunSpeed( void ) const; // get maximum running speed + + virtual float GetMaxAcceleration( void ) const; // return maximum acceleration of locomotor + virtual float GetMaxDeceleration( void ) const; // return maximum deceleration of locomotor +}; + + +//---------------------------------------------------------------------------- +class CBotNPCDecoy : public NextBotCombatCharacter +{ +public: + DECLARE_CLASS( CBotNPCDecoy, NextBotCombatCharacter ); + + CBotNPCDecoy(); + virtual ~CBotNPCDecoy(); + + virtual void Precache(); + virtual void Spawn( void ); + + // INextBot + DECLARE_INTENTION_INTERFACE( CBotNPCDecoy ); + virtual CBotNPCDecoyLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; } + virtual CBotNPCBody *GetBodyInterface( void ) const { return m_body; } + + virtual Vector EyePosition( void ); + + virtual unsigned int PhysicsSolidMaskForEntity( void ) const; + virtual bool ShouldCollide( int collisionGroup, int contentsMask ) const; + + Activity GetRunActivity( void ) const; + +private: + CBotNPCDecoyLocomotion *m_locomotor; + CBotNPCBody *m_body; + + Vector m_eyeOffset; + Activity m_runActivity; +}; + + +inline Activity CBotNPCDecoy::GetRunActivity( void ) const +{ + return m_runActivity; +} + + +inline Vector CBotNPCDecoy::EyePosition( void ) +{ + return GetAbsOrigin() + m_eyeOffset; +} + +#endif // BOT_NPC_DECOY_H diff --git a/game/server/tf/bot_npc/bot_npc_mini.cpp b/game/server/tf/bot_npc/bot_npc_mini.cpp new file mode 100644 index 0000000..a802721 --- /dev/null +++ b/game/server/tf/bot_npc/bot_npc_mini.cpp @@ -0,0 +1,101 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// bot_npc_mini.cpp +// A NextBot non-player derived actor +// Michael Booth, March 2011 + +#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_projectile_rocket.h" +#include "tf_weapon_grenade_pipebomb.h" +#include "tf_ammo_pack.h" +#include "tf_obj_sentrygun.h" +#include "nav_mesh/tf_nav_area.h" +#include "bot_npc_mini.h" +#include "NextBot/Path/NextBotChasePath.h" +#include "econ_wearable.h" +#include "team_control_point_master.h" +#include "particle_parse.h" +#include "CRagdollMagnet.h" +#include "nav_mesh/tf_path_follower.h" +#include "bot_npc_minion.h" +#include "player_vs_environment/monster_resource.h" + + +extern ConVar tf_bot_npc_reaction_time; +extern ConVar tf_bot_npc_grenade_interval; +extern float ModifyBossDamage( const CTakeDamageInfo &info ); + + +ConVar tf_raid_mini_rocket_boss_health( "tf_raid_mini_rocket_boss_health", "5000", 0/*FCVAR_CHEAT*/ ); + + +//----------------------------------------------------------------------------------------------------- +// The Bot NPC mini-boss +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( bot_boss_mini_rockets, CBotNPCMiniRockets ); +PRECACHE_REGISTER( bot_boss_mini_rockets ); + + +//----------------------------------------------------------------------------------------------------- +void CBotNPCMiniRockets::Precache() +{ + BaseClass::Precache(); + + int model = PrecacheModel( "models/bots/knight/knight_mini.mdl" ); + PrecacheGibsForModel( model ); + + PrecacheScriptSound( "RobotMiniBoss.LaunchRocket" ); +} + + +//----------------------------------------------------------------------------------------------------- +void CBotNPCMiniRockets::Spawn( void ) +{ + BaseClass::Spawn(); + + SetModel( "models/bots/knight/knight_mini.mdl" ); + + int health = tf_raid_mini_rocket_boss_health.GetInt(); + SetHealth( health ); + SetMaxHealth( health ); +} + + +//----------------------------------------------------------------------------------------------------- +// The Bot NPC mini-boss +//----------------------------------------------------------------------------------------------------- +LINK_ENTITY_TO_CLASS( bot_boss_mini_nuker, CBotNPCMiniNuker ); +PRECACHE_REGISTER( bot_boss_mini_nuker ); + +ConVar tf_raid_mini_nuker_boss_health( "tf_raid_mini_nuker_boss_health", "5000", 0/*FCVAR_CHEAT*/ ); + + +//----------------------------------------------------------------------------------------------------- +void CBotNPCMiniNuker::Precache() +{ + BaseClass::Precache(); + + int model = PrecacheModel( "models/bots/knight/knight_mini.mdl" ); + PrecacheGibsForModel( model ); +} + + +//----------------------------------------------------------------------------------------------------- +void CBotNPCMiniNuker::Spawn( void ) +{ + BaseClass::Spawn(); + + SetModel( "models/bots/knight/knight_mini.mdl" ); + + int health = tf_raid_mini_nuker_boss_health.GetInt(); + SetHealth( health ); + SetMaxHealth( health ); +} + +#endif // TF_RAID_MODE diff --git a/game/server/tf/bot_npc/bot_npc_mini.h b/game/server/tf/bot_npc/bot_npc_mini.h new file mode 100644 index 0000000..f2f95b8 --- /dev/null +++ b/game/server/tf/bot_npc/bot_npc_mini.h @@ -0,0 +1,83 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// bot_npc_mini.h +// A NextBot non-player derived actor +// Michael Booth, March 2011 + +#ifndef BOT_NPC_MINI_H +#define BOT_NPC_MINI_H + +#ifdef TF_RAID_MODE + +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "NextBotGroundLocomotion.h" +#include "Path/NextBotPathFollow.h" +#include "bot_npc_body.h" +#include "bot/map_entities/tf_spawner_boss.h" +#include "player_vs_environment/boss_alpha/boss_alpha.h" + + +//---------------------------------------------------------------------------- +class CBotNPCMiniRockets : public CBossAlpha +{ +public: + DECLARE_CLASS( CBotNPCMiniRockets, CBossAlpha ); + + virtual void Precache(); + virtual void Spawn( void ); + + virtual bool HasAbility( Ability ability ) const; + + virtual bool IsMiniBoss( void ) const { return true; } + + virtual float GetMoveSpeed( void ) const { return 150.0f; } + + virtual int GetRocketLaunchCount( void ) const { return 3; } + virtual float GetRocketDamage( void ) const { return 25.0f; } + virtual float GetRocketAimError( void ) const { return 3.0f; } + virtual float GetRocketInterval( void ) const { return 0.5f; } + virtual const char *GetRocketSoundEffect( void ) const { return "RobotMiniBoss.LaunchRocket"; } + + virtual float GetBecomeStunnedDamage( void ) const { return 300.0f; } +}; + +inline bool CBotNPCMiniRockets::HasAbility( Ability ability ) const +{ + const int myAbilities = CAN_BE_STUNNED | CAN_FIRE_ROCKETS; + + return myAbilities & ability ? true : false; +} + + +//---------------------------------------------------------------------------- +class CBotNPCMiniNuker : public CBossAlpha +{ +public: + DECLARE_CLASS( CBotNPCMiniNuker, CBossAlpha ); + + virtual void Precache(); + virtual void Spawn( void ); + + virtual bool HasAbility( Ability ability ) const; + + virtual bool IsMiniBoss( void ) const { return true; } + + virtual float GetMoveSpeed( void ) const { return 150.0f; } + + virtual float GetGrenadeInterval( void ) const { return 2.0f; } + + virtual float GetBecomeStunnedDamage( void ) const { return 300.0f; } +}; + +inline bool CBotNPCMiniNuker::HasAbility( Ability ability ) const +{ + const int myAbilities = CAN_BE_STUNNED | CAN_NUKE | CAN_LAUNCH_STICKIES; + + return myAbilities & ability ? true : false; +} + + + +#endif // TF_RAID_MODE + +#endif // BOT_NPC_MINI_H 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 diff --git a/game/server/tf/bot_npc/bot_npc_minion.h b/game/server/tf/bot_npc/bot_npc_minion.h new file mode 100644 index 0000000..6d2deee --- /dev/null +++ b/game/server/tf/bot_npc/bot_npc_minion.h @@ -0,0 +1,199 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// bot_npc_minion.h +// Minions for the Boss +// Michael Booth, November 2010 + +#ifndef BOT_NPC_MINION_H +#define BOT_NPC_MINION_H + +#ifdef TF_RAID_MODE + +#include "NextBot.h" +#include "NextBotBehavior.h" +#include "Path/NextBotPathFollow.h" +#include "bot_npc.h" +#include "bot_npc_body.h" + + +//---------------------------------------------------------------------------- +// Bypass vision system +class CDisableVision : public IVision +{ +public: + CDisableVision( INextBot *bot ) : IVision( bot ) { } + virtual ~CDisableVision() { } + + virtual void Reset( void ) { } + virtual void Update( void ) { } +}; + + +//---------------------------------------------------------------------------- +class CNextBotFlyingLocomotion : public ILocomotion +{ +public: + CNextBotFlyingLocomotion( INextBot *bot ); + virtual ~CNextBotFlyingLocomotion(); + + virtual void Reset( void ); // (EXTEND) reset to initial state + virtual void Update( void ); // (EXTEND) update internal state + + virtual void Approach( const Vector &goalPos, float goalWeight = 1.0f ); // (EXTEND) move directly towards the given position + + virtual void SetDesiredSpeed( float speed ); // set desired speed for locomotor movement + virtual float GetDesiredSpeed( void ) const; // returns the current desired speed + + virtual void SetDesiredAltitude( float height ); // how high above our Approach goal do we float? + virtual float GetDesiredAltitude( void ) const; + + virtual const Vector &GetGroundNormal( void ) const; // surface normal of the ground we are in contact with + + virtual const Vector &GetVelocity( void ) const; // return current world space velocity + void SetVelocity( const Vector &velocity ); + + void Deflect( CBaseEntity *deflector ); + +protected: + float m_desiredSpeed; + float m_currentSpeed; + Vector m_forward; + + float m_desiredAltitude; + void MaintainAltitude( void ); + + Vector m_velocity; + Vector m_acceleration; +}; + +inline const Vector &CNextBotFlyingLocomotion::GetGroundNormal( void ) const +{ + static Vector up( 0, 0, 1.0f ); + + return up; +} + +inline const Vector &CNextBotFlyingLocomotion::GetVelocity( void ) const +{ + return m_velocity; +} + +inline void CNextBotFlyingLocomotion::SetVelocity( const Vector &velocity ) +{ + m_velocity = velocity; +} + + +//---------------------------------------------------------------------------- +class CBotNPCMinion : public NextBotCombatCharacter +{ +public: + DECLARE_CLASS( CBotNPCMinion, NextBotCombatCharacter ); + DECLARE_SERVERCLASS(); + + CBotNPCMinion(); + virtual ~CBotNPCMinion(); + + virtual void Precache(); + virtual void Spawn( void ); + + virtual int OnTakeDamage_Alive( const CTakeDamageInfo &info ); + + virtual bool IsDeflectable() { return true; } // for flamethrower + virtual void Deflected( CBaseEntity *pDeflectedBy, Vector &vecDir ); + + // INextBot + DECLARE_INTENTION_INTERFACE( CBotNPCMinion ); + virtual CNextBotFlyingLocomotion *GetLocomotionInterface( void ) const { return m_locomotor; } + virtual CBotNPCBody *GetBodyInterface( void ) const { return m_body; } + virtual IVision *GetVisionInterface( void ) const { return m_vision; } + + virtual Vector EyePosition( void ); + + virtual unsigned int PhysicsSolidMaskForEntity( void ) const; + virtual bool ShouldCollide( int collisionGroup, int contentsMask ) const; + + void BecomeAmmoPack( void ); + + CTFPlayer *FindTarget( void ); // Find the closest living player not already being targeted by another minion + + void UpdateTarget( void ); + + void SetTarget( CTFPlayer *target ); + CTFPlayer *GetTarget( void ) const; + bool HasTarget( void ) const; + bool IsTarget( CTFPlayer *target ) const; + + const Vector &GetLastKnownTargetPosition( void ) const; + + void StartStunEffects( CTFPlayer *victim ); + void EndStunEffects( void ); + + bool IsAlert( void ) const; + void BecomeAlert( void ); + +private: + CNextBotFlyingLocomotion *m_locomotor; + CBotNPCBody *m_body; + CDisableVision *m_vision; + + Vector m_eyeOffset; + + CTFPlayer *m_target; + Vector m_lastKnownTargetPosition; + + CountdownTimer m_invulnTimer; + + CNetworkHandle( CBaseEntity, m_stunTarget ); + + bool m_isAlert; +}; + +inline bool CBotNPCMinion::IsAlert( void ) const +{ + return m_isAlert; +} + +inline Vector CBotNPCMinion::EyePosition( void ) +{ + return GetAbsOrigin() + m_eyeOffset; +} + +inline bool CBotNPCMinion::HasTarget( void ) const +{ + return m_target == NULL ? false : true; +} + +inline bool CBotNPCMinion::IsTarget( CTFPlayer *target ) const +{ + return ( m_target == target ) ? true : false; +} + +inline void CBotNPCMinion::SetTarget( CTFPlayer *target ) +{ + m_target = target; +} + +inline CTFPlayer *CBotNPCMinion::GetTarget( void ) const +{ + return m_target; +} + +inline const Vector &CBotNPCMinion::GetLastKnownTargetPosition( void ) const +{ + return m_lastKnownTargetPosition; +} + +inline void CBotNPCMinion::StartStunEffects( CTFPlayer *victim ) +{ + m_stunTarget = victim; +} + +inline void CBotNPCMinion::EndStunEffects( void ) +{ + m_stunTarget = NULL; +} + + +#endif // TF_RAID_MODE + +#endif // BOT_NPC_MINION_H |