//========= 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; im_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; mHasTarget() ) { 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; mHasTarget() && 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; icurtime - 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() ) { // 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[ 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; im_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; iIsTaunting() ) { 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; iWorldSpaceCenter() - 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; iGetGroundEntity() == 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; im_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; iIsAlive() && player->GetTeamNumber() == TF_TEAM_BLUE ) { victimVector.AddToTail( player ); } } // objects CTFTeam *team = GetGlobalTFTeam( TF_TEAM_BLUE ); if ( team ) { for ( i=0; iGetNumObjects(); ++i ) { CBaseObject *object = team->GetObject( i ); if ( object ) { victimVector.AddToTail( object ); } } } #ifdef SKIPME team = GetGlobalTFTeam( TF_TEAM_RED ); if ( team ) { for ( i=0; iGetNumObjects(); ++i ) { CBaseObject *object = team->GetObject( i ); if ( object ) { victimVector.AddToTail( object ); } } } // non-player bots CUtlVector< INextBot * > botVector; TheNextBots().CollectAllBots( &botVector ); for( i=0; iGetEntity(); if ( !bot->IsPlayer() && bot->IsAlive() ) { victimVector.AddToTail( bot ); } } #endif // SKIPME for( int i=0; iIsSelf( 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; iIsRangeGreaterThan( 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( 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 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; iDetonate(); } } return Done(); } return Continue(); } //--------------------------------------------------------------------------------------------- void CBotNPCLaunchGrenades::OnEnd( CBotNPC *me, Action< CBotNPC > *nextAction ) { // fizzle any outstanding stickies for( int i=0; iFizzle(); 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; rGetRandomPoint() + 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; iEmitSound( "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