Modified source engine (2017) developed by valve and leaked in 2020. Not for commercial purporses
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1385 lines
38 KiB

//========= Copyright Valve Corporation, All rights reserved. ============//
// boss_alpha.cpp
// Our first "real" TF Boss
// Michael Booth, November 2010
#include "cbase.h"
#ifdef TF_RAID_MODE
#include "tf_player.h"
#include "tf_gamerules.h"
#include "tf_team.h"
#include "tf_projectile_arrow.h"
#include "tf_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 "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/bot_npc_minion.h"
#include "player_vs_environment/monster_resource.h"
#include "bot/map_entities/tf_bot_generator.h"
#include "player_vs_environment/boss_alpha/boss_alpha.h"
#include "player_vs_environment/boss_alpha/behavior/boss_alpha_behavior.h"
//#define USE_BOSS_SENTRY
ConVar tf_boss_alpha_health( "tf_boss_alpha_health", "30000"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_attack_range( "tf_boss_alpha_attack_range", "300"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_threat_tolerance( "tf_boss_alpha_threat_tolerance", "100"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_chase_range( "tf_boss_alpha_chase_range", "300"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_grenade_launch_range( "tf_boss_alpha_grenade_launch_range", "300"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_quit_range( "tf_boss_alpha_quit_range", "2500"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_reaction_time( "tf_boss_alpha_reaction_time", "0.5"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_stunned_injury_multiplier( "tf_boss_alpha_stunned_injury_multiplier", "10" );
ConVar tf_boss_alpha_head_radius( "tf_boss_alpha_head_radius", "75" ); // 50
ConVar tf_boss_alpha_hate_taunt_cooldown( "tf_boss_alpha_hate_taunt_cooldown", "10"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_debug_damage( "tf_boss_alpha_debug_damage", "0"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_min_nuke_after_stun_time( "tf_boss_alpha_min_nuke_after_stun_time", "5" /*, FCVAR_CHEAT */ );
ConVar tf_boss_alpha_always_stun( "tf_boss_alpha_always_stun", "0"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_stun_rocket_reflect_count( "tf_boss_alpha_stun_rocket_reflect_count", "2"/*, FCVAR_CHEAT */ );
ConVar tf_boss_alpha_stun_rocket_reflect_duration( "tf_boss_alpha_stun_rocket_reflect_duration", "1"/*, FCVAR_CHEAT */ );
ConVar tf_boss_alpha_debug_skill_shots( "tf_boss_alpha_debug_skill_shots", "0"/*, FCVAR_CHEAT */ );
extern ConVar tf_boss_alpha_nuke_interval;
//-----------------------------------------------------------------------------------------------------
// The Alpha Boss: A rocket and stickybomb firing giant robot that periodically charges up a big
// "nuke" attack, and is invulnerable unless stunned.
//-----------------------------------------------------------------------------------------------------
LINK_ENTITY_TO_CLASS( boss_alpha, CBossAlpha );
PRECACHE_REGISTER( boss_alpha );
IMPLEMENT_SERVERCLASS_ST( CBossAlpha, DT_BossAlpha )
SendPropBool( SENDINFO( m_isNuking ) ),
END_SEND_TABLE()
BEGIN_DATADESC( CBossAlpha )
DEFINE_OUTPUT( m_outputOnStunned, "OnStunned" ),
DEFINE_OUTPUT( m_outputOnHealthBelow90Percent, "OnHealthBelow90Percent" ),
DEFINE_OUTPUT( m_outputOnHealthBelow80Percent, "OnHealthBelow80Percent" ),
DEFINE_OUTPUT( m_outputOnHealthBelow70Percent, "OnHealthBelow70Percent" ),
DEFINE_OUTPUT( m_outputOnHealthBelow60Percent, "OnHealthBelow60Percent" ),
DEFINE_OUTPUT( m_outputOnHealthBelow50Percent, "OnHealthBelow50Percent" ),
DEFINE_OUTPUT( m_outputOnHealthBelow40Percent, "OnHealthBelow40Percent" ),
DEFINE_OUTPUT( m_outputOnHealthBelow30Percent, "OnHealthBelow30Percent" ),
DEFINE_OUTPUT( m_outputOnHealthBelow20Percent, "OnHealthBelow20Percent" ),
DEFINE_OUTPUT( m_outputOnHealthBelow10Percent, "OnHealthBelow10Percent" ),
DEFINE_OUTPUT( m_outputOnKilled, "OnKilled" ),
END_DATADESC()
//-----------------------------------------------------------------------------------------------------
CBossAlpha::CBossAlpha()
{
m_intention = new CBossAlphaIntention( this );
m_locomotor = new CBossAlphaLocomotion( this );
m_body = new CBotNPCBody( this );
m_vision = new CBossAlphaVision( this );
m_conditionFlags = 0;
m_isNuking = false;
m_ageTimer.Invalidate();
m_lastHealthPercentage = 1.0f;
ClearStunDamage();
}
//-----------------------------------------------------------------------------------------------------
CBossAlpha::~CBossAlpha()
{
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 CBossAlpha::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 );
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( "RobotBoss.HardHitSkillShot" );
PrecacheScriptSound( "RobotBoss.DamageSpongeSkillShot" );
PrecacheScriptSound( "RobotBoss.PreciseHit1SkillShot" );
PrecacheScriptSound( "RobotBoss.PreciseHit2SkillShot" );
PrecacheScriptSound( "RobotBoss.PreciseHit3SkillShot" );
PrecacheScriptSound( "Cart.Explode" );
PrecacheScriptSound( "Weapon_Crowbar.Melee_HitWorld" );
PrecacheParticleSystem( "asplode_hoodoo_embers" );
PrecacheParticleSystem( "charge_up" );
}
//-----------------------------------------------------------------------------------------------------
void CBossAlpha::Spawn( void )
{
BaseClass::Spawn();
#ifdef USE_BOSS_SENTRY
SetModel( "models/bots/boss_sentry/boss_sentry.mdl" );
#else
SetModel( "models/bots/knight/knight.mdl" );
#endif
m_conditionFlags = 0;
ClearStunDamage();
ResetSkillShots();
int health = tf_boss_alpha_health.GetInt();
SetHealth( health );
SetMaxHealth( health );
// show Boss' health meter on HUD
if ( g_pMonsterResource )
{
g_pMonsterResource->SetBossHealthPercentage( 1.0f );
}
m_damagePoseParameter = -1;
// randomize initial check
m_nearestVisibleEnemy = NULL;
m_nearestVisibleEnemyTimer.Start( RandomFloat( 0.0f, tf_boss_alpha_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_boss_alpha_nuke_interval.GetFloat() );
m_isNuking = false;
m_grenadeTimer.Start( GetGrenadeInterval() );
m_ageTimer.Start();
m_lastHealthPercentage = 1.0f;
ChangeTeam( TF_TEAM_RED );
TFGameRules()->SetActiveBoss( this );
// CollisionProp()->SetSurroundingBoundsType( USE_HITBOXES );
Vector mins( -50, -50, 0 );
Vector maxs( 100, 100, 275 );
CollisionProp()->SetSurroundingBoundsType( USE_SPECIFIED_BOUNDS, &mins, &maxs );
Vector collideMins( -50, -50, 125 );
Vector collideMaxs( 50, 50, 260 );
CollisionProp()->SetCollisionBounds( collideMins, collideMaxs );
}
//-----------------------------------------------------------------------------------------------------
ConVar tf_boss_alpha_dmg_mult_sniper( "tf_boss_alpha_dmg_mult_sniper", "1"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_dmg_mult_minigun( "tf_boss_alpha_dmg_mult_minigun", "0.3"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_dmg_mult_flamethrower( "tf_boss_alpha_dmg_mult_flamethrower", "1"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_dmg_mult_sentrygun( "tf_boss_alpha_dmg_mult_sentrygun", "0.3"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_dmg_mult_grenade( "tf_boss_alpha_dmg_mult_grenade", "0.3"/*, FCVAR_CHEAT*/ );
ConVar tf_boss_alpha_dmg_mult_rocket( "tf_boss_alpha_dmg_mult_rocket", "0.5"/*, 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_boss_alpha_dmg_mult_sniper.GetFloat();
case TF_WEAPON_MINIGUN:
return info.GetDamage() * tf_boss_alpha_dmg_mult_minigun.GetFloat();
case TF_WEAPON_FLAMETHROWER:
return info.GetDamage() * tf_boss_alpha_dmg_mult_flamethrower.GetFloat();
case TF_WEAPON_SENTRY_BULLET:
return info.GetDamage() * tf_boss_alpha_dmg_mult_sentrygun.GetFloat();
case TF_WEAPON_GRENADELAUNCHER:
case TF_WEAPON_PIPEBOMBLAUNCHER:
case TF_WEAPON_GRENADE_DEMOMAN:
return info.GetDamage() * tf_boss_alpha_dmg_mult_grenade.GetFloat();
case TF_WEAPON_ROCKETLAUNCHER:
case TF_WEAPON_ROCKETLAUNCHER_DIRECTHIT:
return info.GetDamage() * tf_boss_alpha_dmg_mult_rocket.GetFloat();
}
}
// unmodified
return info.GetDamage();
}
#define HITBOX_SKILL_STICKYBOMB_1 23
#define HITBOX_SKILL_STICKYBOMB_2 24
#define HITBOX_SKILL_PRECISION_1 20
#define HITBOX_SKILL_PRECISION_2 21
#define HITBOX_SKILL_PRECISION_3 22
#define PRECISION_SHOT_COUNT 3
#define HITBOX_SKILL_DAMAGE_SPONGE 19
#define HITBOX_SKILL_HARD_HIT 18
ConVar tf_boss_alpha_skill_shot_combo_time( "tf_boss_alpha_skill_shot_combo_time", "10"/*, FCVAR_CHEAT */ );
ConVar tf_boss_alpha_skill_shot_count( "tf_boss_alpha_skill_shot_count", "3"/*, FCVAR_CHEAT */ );
ConVar tf_boss_alpha_skill_shot_precision_time( "tf_boss_alpha_skill_shot_precision_time", "6"/*, FCVAR_CHEAT */ );
ConVar tf_boss_alpha_skill_shot_hard_hit_damage( "tf_boss_alpha_skill_shot_hard_hit_damage", "40"/*, FCVAR_CHEAT */ );
ConVar tf_boss_alpha_skill_shot_hard_hit_z( "tf_boss_alpha_skill_shot_hard_hit_z", "0"/*, FCVAR_CHEAT */ );
ConVar tf_boss_alpha_skill_shot_damage_sponge_total( "tf_boss_alpha_skill_shot_damage_sponge_total", "500"/*, FCVAR_CHEAT */ );
ConVar tf_boss_alpha_skill_shot_damage_sponge_decay( "tf_boss_alpha_skill_shot_damage_sponge_decay", "100"/*, FCVAR_CHEAT */ );
//-----------------------------------------------------------------------------------------------------
void CBossAlpha::ResetSkillShots( void )
{
m_skillShotComboTimer.Invalidate();
m_skillShotCount = 0;
m_isPrecisionShotDone = false;
m_precisionSkillShotTimer.Invalidate();
for( int i=0; i<PRECISION_SHOT_COUNT; ++i )
{
m_isPrecisionShotHit[i] = false;
}
m_isDamageSpongeSkillShotDone = false;
m_damageSpongeSkillShotAmount = 0.0f;
m_isHardHitSkillShotDone = false;
if ( g_pMonsterResource )
{
g_pMonsterResource->HideSkillShotComboMeter();
}
}
//-----------------------------------------------------------------------------------------------------
void CBossAlpha::OnSkillShotComboStarted( void )
{
m_skillShotComboTimer.Start( tf_boss_alpha_skill_shot_combo_time.GetFloat() );
if ( g_pMonsterResource )
{
g_pMonsterResource->StartSkillShotComboMeter( tf_boss_alpha_skill_shot_combo_time.GetFloat() );
}
}
//-----------------------------------------------------------------------------------------------------
void CBossAlpha::OnSkillShot( void )
{
if ( !m_skillShotComboTimer.HasStarted() || m_skillShotComboTimer.IsElapsed() )
{
// start a new combo
OnSkillShotComboStarted();
m_skillShotCount = 1;
}
else
{
// combo in progress
++m_skillShotCount;
if ( g_pMonsterResource )
{
g_pMonsterResource->IncrementSkillShotComboMeter();
}
}
if ( m_skillShotCount >= tf_boss_alpha_skill_shot_count.GetInt() )
{
AddCondition( STUNNED );
EmitSound( "RobotBoss.Vulnerable" );
}
}
//-----------------------------------------------------------------------------------------------------
//
// Invoked when we are struck. Check if a vulnerability was hit, and update the skill shot combo
//
bool CBossAlpha::CheckSkillShots( const CTakeDamageInfo &info )
{
if ( !HasAbility( CBossAlpha::CAN_BE_STUNNED ) || !info.GetAttacker() )
{
return false;
}
// skill shots are not available until the boss recovers
if ( IsInCondition( STUNNED ) )
{
return false;
}
if ( tf_boss_alpha_always_stun.GetBool() )
{
m_skillShotComboTimer.Start( 1.0f );
m_skillShotCount = 999;
OnSkillShot();
return true;
}
// const Vector &hitSpot = info.GetDamagePosition();
CBaseEntity *inflictor = info.GetInflictor();
if ( !inflictor )
{
return false;
}
Vector hitDir = m_lastTraceAttackDir;
/*
Vector hitDir = inflictor->GetAbsVelocity();
if ( inflictor->IsPlayer() )
{
hitDir = hitSpot - inflictor->EyePosition();
}
else
{
CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( inflictor );
if ( sentry )
{
hitDir = hitSpot - sentry->EyePosition();
}
}
hitDir.NormalizeInPlace();
*/
Vector traceFrom = m_lastTraceAttackTrace.startpos - m_lastTraceAttackDir * 10.0f;
Vector traceTo = m_lastTraceAttackTrace.endpos + m_lastTraceAttackDir * 100.0f;
trace_t result;
//UTIL_TraceLine( hitSpot - 50.0f * hitDir, hitSpot + 50.0f * hitDir, MASK_SOLID | CONTENTS_HITBOX, inflictor, COLLISION_GROUP_NONE, &result );
UTIL_TraceLine( traceFrom, traceTo, MASK_SOLID | CONTENTS_HITBOX, inflictor, COLLISION_GROUP_NONE, &result );
if ( tf_boss_alpha_debug_skill_shots.GetBool() )
{
if ( result.hitbox != 0 )
{
NDebugOverlay::HorzArrow( traceFrom, traceTo, 3.0f, 0, 255, 0, 255, true, 9999.9f );
}
else
{
NDebugOverlay::HorzArrow( traceFrom, traceTo, 3.0f, 255, 0, 0, 255, true, 9999.9f );
}
}
if ( !result.DidHit() )
{
return false;
}
switch( result.hitbox )
{
case HITBOX_SKILL_PRECISION_1:
case HITBOX_SKILL_PRECISION_2:
case HITBOX_SKILL_PRECISION_3:
{
int which = result.hitbox - HITBOX_SKILL_PRECISION_1;
if ( !m_isPrecisionShotDone && !m_isPrecisionShotHit[ which ] )
{
if ( !m_precisionSkillShotTimer.HasStarted() )
{
m_precisionSkillShotTimer.Start( tf_boss_alpha_skill_shot_precision_time.GetFloat() );
}
m_isPrecisionShotHit[ which ] = true;
UTIL_ClientPrintAll( HUD_PRINTTALK, CFmtStr( "PRECISION SHOT %d...", which+1 ) );
int i;
for( i=0; i<PRECISION_SHOT_COUNT; ++i )
{
if ( !m_isPrecisionShotHit[i] )
break;
}
if ( i == PRECISION_SHOT_COUNT )
{
// successfully completed the precision shot
m_isPrecisionShotDone = true;
UTIL_ClientPrintAll( HUD_PRINTTALK, "PRECISION SKILL SHOT!" );
EmitSound( CFmtStr( "RobotBoss.PreciseHit%dSkillShot", which+1 ) );
OnSkillShot();
}
return true;
}
break;
}
case HITBOX_SKILL_DAMAGE_SPONGE:
if ( !m_isDamageSpongeSkillShotDone )
{
m_damageSpongeSkillShotAmount += info.GetDamage();
if ( m_damageSpongeSkillShotAmount > tf_boss_alpha_skill_shot_damage_sponge_total.GetFloat() )
{
// successfully completed the damage sponge shot
m_isDamageSpongeSkillShotDone = true;
m_damageSpongeSkillShotAmount = 0.0f;
UTIL_ClientPrintAll( HUD_PRINTTALK, "DAMAGE SPONGE SKILL SHOT!" );
EmitSound( "RobotBoss.DamageSpongeSkillShot" );
OnSkillShot();
return true;
}
UTIL_ClientPrintAll( HUD_PRINTTALK, CFmtStr( "DAMAGE SPONGE = %3.2f", m_damageSpongeSkillShotAmount ) );
return true;
}
break;
case HITBOX_SKILL_HARD_HIT:
if ( !m_isHardHitSkillShotDone )
{
if ( info.GetDamage() > tf_boss_alpha_skill_shot_hard_hit_damage.GetFloat() )
{
// make sure player hit from above
if ( info.GetAttacker() )
{
Vector toAttacker = info.GetAttacker()->EyePosition() - m_lastTraceAttackTrace.endpos;
toAttacker.NormalizeInPlace();
if ( toAttacker.z > tf_boss_alpha_skill_shot_hard_hit_z.GetFloat() )
{
// successfully completed the hard hit shot
m_isHardHitSkillShotDone = true;
UTIL_ClientPrintAll( HUD_PRINTTALK, "HARD HIT SKILL SHOT!" );
EmitSound( "RobotBoss.HardHitSkillShot" );
OnSkillShot();
}
}
}
return true;
}
break;
}
return false;
}
//-----------------------------------------------------------------------------------------------------
void CBossAlpha::UpdateSkillShots( void )
{
m_damageSpongeSkillShotAmount -= tf_boss_alpha_skill_shot_damage_sponge_decay.GetFloat() * gpGlobals->frametime;
if ( m_damageSpongeSkillShotAmount < 0.0f )
{
m_damageSpongeSkillShotAmount = 0.0f;
}
else
{
UTIL_ClientPrintAll( HUD_PRINTTALK, CFmtStr( "DAMAGE SPONGE = %3.2f/%3.2f", m_damageSpongeSkillShotAmount, tf_boss_alpha_skill_shot_damage_sponge_total.GetFloat() ) );
}
if ( m_skillShotComboTimer.HasStarted() && m_skillShotComboTimer.IsElapsed() )
{
// took too long to perform skill shots - reset combo
ResetSkillShots();
UTIL_ClientPrintAll( HUD_PRINTTALK, "SKILL SHOT CHAIN FAILED - TOO SLOW!" );
}
if ( !m_isPrecisionShotDone && m_precisionSkillShotTimer.HasStarted() && m_precisionSkillShotTimer.IsElapsed() )
{
// took too long to hit all the precision targets - reset
m_precisionSkillShotTimer.Invalidate();
for( int i=0; i<PRECISION_SHOT_COUNT; ++i )
{
m_isPrecisionShotHit[i] = false;
}
UTIL_ClientPrintAll( HUD_PRINTTALK, "PRECISION SHOTS RESET - TOO SLOW!" );
}
}
//-----------------------------------------------------------------------------------------------------
int CBossAlpha::OnTakeDamage_Alive( const CTakeDamageInfo &rawInfo )
{
CTakeDamageInfo info = rawInfo;
// don't take damage from myself
if ( info.GetAttacker() == this )
{
return 0;
}
// weapon-specific damage modification
info.SetDamage( ModifyBossDamage( info ) );
// do the critical damage increase
if ( info.GetDamageType() & DMG_CRITICAL )
{
info.SetDamage( info.GetDamage() * TF_DAMAGE_CRIT_MULTIPLIER );
}
bool isSkillShot = false;
if ( CheckSkillShots( info ) )
{
isSkillShot = true;
// skill shots don't deal damage
info.SetDamage( 0 );
}
bool isHeadHit = false;
if ( IsInCondition( VULNERABLE_TO_STUN ) )
{
// track head damage when vulnerable
Vector headPos;
QAngle headAngles;
if ( GetAttachment( "head", headPos, headAngles ) )
{
Vector damagePos = info.GetDamagePosition();
if ( tf_boss_alpha_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 );
}
isHeadHit = ( damagePos - headPos ).IsLengthLessThan( tf_boss_alpha_head_radius.GetFloat() );
if ( isHeadHit )
{
// hit the head
AccumulateStunDamage( info.GetDamage() );
DispatchParticleEffect( "asplode_hoodoo_embers", info.GetDamagePosition(), GetAbsAngles() );
if ( tf_boss_alpha_debug_damage.GetBool() )
{
DevMsg( "Stun dmg = %f\n", GetStunDamage() );
NDebugOverlay::Circle( headPos, tf_boss_alpha_head_radius.GetFloat(), 255, 0, 0, 255, true, 5.0f );
}
}
else if ( tf_boss_alpha_debug_damage.GetBool() )
{
NDebugOverlay::Circle( headPos, tf_boss_alpha_head_radius.GetFloat(), 255, 255, 0, 255, true, 5.0f );
}
}
}
// take extra damage when stunned
if ( IsInCondition( STUNNED ) )
{
info.SetDamage( info.GetDamage() * tf_boss_alpha_stunned_injury_multiplier.GetFloat() );
if ( m_ouchTimer.IsElapsed() )
{
m_ouchTimer.Start( 1.0f );
EmitSound( "RobotBoss.Hurt" );
}
}
else if ( !isHeadHit && !isSkillShot )
{
// invulnerable until stunned
if ( m_ricochetSoundTimer.IsElapsed() )
{
TFGameRules()->BroadcastSound( 255, "Weapon_Crowbar.Melee_HitWorld" );
m_ricochetSoundTimer.Start( 0.15f );
}
return 0;
}
// keep a list of everyone who hurt me, and when
if ( info.GetAttacker() && info.GetAttacker()->MyCombatCharacterPointer() && !InSameTeam( info.GetAttacker() ) )
{
CBaseCombatCharacter *attacker = info.GetAttacker()->MyCombatCharacterPointer();
// sentry guns are first class attackers
if ( info.GetInflictor() )
{
CObjectSentrygun *sentry = dynamic_cast< CObjectSentrygun * >( info.GetInflictor() );
if ( sentry )
{
attacker = sentry;
}
}
RememberAttacker( attacker, info.GetDamage(), ( info.GetDamageType() & DMG_CRITICAL ) ? true : false );
CTFPlayer *playerAttacker = ToTFPlayer( attacker );
if ( playerAttacker )
{
for( int i=0; i<playerAttacker->m_Shared.GetNumHealers(); ++i )
{
CTFPlayer *medic = ToTFPlayer( playerAttacker->m_Shared.GetHealerByIndex( i ) );
if ( medic )
{
// medics healing my attacker are also considered attackers
RememberAttacker( medic, 0, 0 );
}
}
}
// if we don't have an attack target yet, we do now
if ( !HasAttackTarget() )
{
SetAttackTarget( attacker );
}
}
// 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 );
// emit injury outputs
float healthPercentage = (float)GetHealth() / (float)GetMaxHealth();
if ( m_lastHealthPercentage > 0.9f && healthPercentage < 0.9f )
{
m_outputOnHealthBelow90Percent.FireOutput( this, this );
}
else if ( m_lastHealthPercentage > 0.8f && healthPercentage < 0.8f )
{
m_outputOnHealthBelow80Percent.FireOutput( this, this );
}
else if ( m_lastHealthPercentage > 0.7f && healthPercentage < 0.7f )
{
m_outputOnHealthBelow70Percent.FireOutput( this, this );
}
else if ( m_lastHealthPercentage > 0.6f && healthPercentage < 0.6f )
{
m_outputOnHealthBelow60Percent.FireOutput( this, this );
}
else if ( m_lastHealthPercentage > 0.5f && healthPercentage < 0.5f )
{
m_outputOnHealthBelow50Percent.FireOutput( this, this );
}
else if ( m_lastHealthPercentage > 0.4f && healthPercentage < 0.4f )
{
m_outputOnHealthBelow40Percent.FireOutput( this, this );
}
else if ( m_lastHealthPercentage > 0.3f && healthPercentage < 0.3f )
{
m_outputOnHealthBelow30Percent.FireOutput( this, this );
}
else if ( m_lastHealthPercentage > 0.2f && healthPercentage < 0.2f )
{
m_outputOnHealthBelow20Percent.FireOutput( this, this );
}
else if ( m_lastHealthPercentage > 0.1f && healthPercentage < 0.1f )
{
m_outputOnHealthBelow10Percent.FireOutput( this, this );
}
m_lastHealthPercentage = healthPercentage;
if ( g_pMonsterResource )
{
g_pMonsterResource->SetBossHealthPercentage( healthPercentage );
}
return result;
}
//---------------------------------------------------------------------------------------------
// Returns true if we're in a condition that means we can't start another action
bool CBossAlpha::IsBusy( void ) const
{
return IsInCondition( (Condition)( CHARGING | STUNNED | VULNERABLE_TO_STUN | BUSY ) );
}
//---------------------------------------------------------------------------------------------
void CBossAlpha::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 *CBossAlpha::GetClosestMinionPrisoner( void )
{
CUtlVector< CBotNPCMinion * > minionVector;
CBotNPCMinion *minion = NULL;
while( ( minion = (CBotNPCMinion *)gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL )
{
minionVector.AddToTail( minion );
}
CTFPlayer *closeCapture = NULL;
float captureRangeSq = FLT_MAX;
for( int m=0; m<minionVector.Count(); ++m )
{
minion = minionVector[m];
if ( minion->HasTarget() )
{
CTFPlayer *victim = minion->GetTarget();
if ( victim->m_Shared.InCond( TF_COND_STUNNED ) )
{
// they've got one!
float rangeSq = GetRangeSquaredTo( victim );
if ( rangeSq < captureRangeSq )
{
closeCapture = victim;
captureRangeSq = rangeSq;
}
}
}
}
return closeCapture;
}
//----------------------------------------------------------------------------------
bool CBossAlpha::IsPrisonerOfMinion( CBaseCombatCharacter *victim )
{
if ( !victim->IsPlayer() )
{
return false;
}
CUtlVector< CBotNPCMinion * > minionVector;
CBotNPCMinion *minion = NULL;
while( ( minion = (CBotNPCMinion *)gEntList.FindEntityByClassname( minion, "bot_npc_minion" ) ) != NULL )
{
minionVector.AddToTail( minion );
}
for( int m=0; m<minionVector.Count(); ++m )
{
minion = minionVector[m];
if ( minion->HasTarget() && minion->GetTarget() == victim )
{
if ( minion->GetTarget()->m_Shared.InCond( TF_COND_STUNNED ) )
{
return true;
}
}
}
return false;
}
//----------------------------------------------------------------------------------
void CBossAlpha::UpdateDamagePerSecond( void )
{
m_lastDamagePerSecond = m_currentDamagePerSecond;
m_currentDamagePerSecond = 0.0f;
const float windowDuration = 10.0f; // 5.0f;
int i;
m_threatVector.RemoveAll();
for( i=0; i<m_attackerVector.Count(); ++i )
{
float age = gpGlobals->curtime - m_attackerVector[i].m_timestamp;
if ( age > windowDuration )
{
// too old
break;
}
float decayedDamage = ( ( windowDuration - age ) / windowDuration ) * m_attackerVector[i].m_damage;
m_currentDamagePerSecond += decayedDamage;
CBaseCombatCharacter *attacker = m_attackerVector[i].m_attacker;
if ( attacker && attacker->IsAlive() )
{
int j;
for( j=0; j<m_threatVector.Count(); ++j )
{
if ( m_threatVector[j].m_who == attacker )
{
m_threatVector[j].m_threat += decayedDamage;
break;
}
}
if ( j >= m_threatVector.Count() )
{
// new threat
ThreatInfo threat;
threat.m_who = attacker;
threat.m_threat = decayedDamage;
m_threatVector.AddToTail( threat );
}
}
}
// if ( m_currentDamagePerSecond > 0.0001f )
// {
// DevMsg( "%3.2f: dps = %3.2f\n", gpGlobals->curtime, m_currentDamagePerSecond );
// }
}
//----------------------------------------------------------------------------------
const CBossAlpha::ThreatInfo *CBossAlpha::GetMaxThreat( void ) const
{
int maxThreatIndex = -1;
for( int i=0; i<m_threatVector.Count(); ++i )
{
if ( maxThreatIndex < 0 || m_threatVector[i].m_threat > m_threatVector[ maxThreatIndex ].m_threat )
{
maxThreatIndex = i;
}
}
if ( maxThreatIndex < 0 )
{
// no threat yet
return NULL;
}
return &m_threatVector[ maxThreatIndex ];
}
//----------------------------------------------------------------------------------
const CBossAlpha::ThreatInfo *CBossAlpha::GetThreat( CBaseCombatCharacter *who ) const
{
for( int i=0; i<m_threatVector.Count(); ++i )
{
if ( m_threatVector[i].m_who == who )
{
return &m_threatVector[i];
}
}
return NULL;
}
//----------------------------------------------------------------------------------
void CBossAlpha::UpdateAttackTarget( void )
{
if ( m_isAttackTargetLocked && HasAttackTarget() )
{
return;
}
// who is most dangerous to me at the moment
const ThreatInfo *maxThreat = GetMaxThreat();
if ( !maxThreat )
{
// nobody is hurting me at the moment
if ( HasAttackTarget() )
{
// stay focused on current target
return;
}
// we have no current target, either
// if my minions have captured someone, go get them
CTFPlayer *closeCapture = GetClosestMinionPrisoner();
if ( closeCapture )
{
SetAttackTarget( closeCapture );
return;
}
// if we see an enemy, attack them
CBaseCombatCharacter *visible = GetNearestVisibleEnemy();
if ( visible )
{
SetAttackTarget( visible );
}
return;
}
// we are under attack, if we don't have a target, attack the highest threat
if ( !HasAttackTarget() )
{
SetAttackTarget( maxThreat->m_who );
return;
}
if ( IsAttackTarget( maxThreat->m_who ) )
{
// our current target is still dealing the most damage to us
return;
}
// switch to new threat if is is more dangerous
const ThreatInfo *attackTargetThreat = GetThreat( GetAttackTarget() );
if ( !attackTargetThreat || maxThreat->m_threat > attackTargetThreat->m_threat + tf_boss_alpha_threat_tolerance.GetFloat() )
{
// change threats
SetAttackTarget( maxThreat->m_who );
}
}
//----------------------------------------------------------------------------------
void CBossAlpha::RemoveCondition( Condition c )
{
if ( c == STUNNED )
{
// reset the accumulator
ClearStunDamage();
ResetSkillShots();
}
m_conditionFlags &= ~c;
}
//---------------------------------------------------------------------------------------------
void CBossAlpha::Update( void )
{
BaseClass::Update();
UpdateNearestVisibleEnemy();
UpdateDamagePerSecond();
UpdateAttackTarget();
UpdateSkillShots();
if ( m_damagePoseParameter < 0 )
{
m_damagePoseParameter = LookupPoseParameter( "damage" );
}
if ( m_damagePoseParameter >= 0 )
{
SetPoseParameter( m_damagePoseParameter, 1.0f - ( (float)GetHealth() / (float)GetMaxHealth() ) );
}
// chase down players who taunt me
if ( m_hateTauntTimer.IsElapsed() )
{
CUtlVector< CTFPlayer * > playerVector;
CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS );
for( int i=0; i<playerVector.Count(); ++i )
{
if ( playerVector[i]->IsTaunting() )
{
m_hateTauntTimer.Start( tf_boss_alpha_hate_taunt_cooldown.GetFloat() );
if ( IsLineOfSightClear( playerVector[i], IGNORE_ACTORS ) )
{
// the taunter becomes our new attack target
SetAttackTarget( playerVector[i], tf_boss_alpha_hate_taunt_cooldown.GetFloat() );
}
}
}
}
}
//---------------------------------------------------------------------------------------------
bool CBossAlpha::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 CBossAlpha::UpdateNearestVisibleEnemy( void )
{
if ( !m_nearestVisibleEnemyTimer.IsElapsed() )
{
return;
}
m_nearestVisibleEnemyTimer.Start( tf_boss_alpha_reaction_time.GetFloat() );
// collect everyone
CUtlVector< CTFPlayer * > playerVector;
//CollectPlayers( &playerVector, TF_TEAM_RED, COLLECT_ONLY_LIVING_PLAYERS );
CollectPlayers( &playerVector, TF_TEAM_BLUE, COLLECT_ONLY_LIVING_PLAYERS, APPEND_PLAYERS );
Vector myForward;
GetVectors( &myForward, NULL, NULL );
m_nearestVisibleEnemy = NULL;
float victimRangeSq = FLT_MAX;
for( int i=0; i<playerVector.Count(); ++i )
{
CTFPlayer *victim = playerVector[i];
if ( IsIgnored( victim ) )
{
continue;
}
float rangeSq = GetRangeSquaredTo( playerVector[i] );
if ( rangeSq < victimRangeSq )
{
// FOV check
Vector to = playerVector[i]->WorldSpaceCenter() - WorldSpaceCenter();
to.NormalizeInPlace();
if ( DotProduct( to, myForward ) > -0.7071f )
{
if ( IsLineOfSightClear( playerVector[i] ) )
{
m_nearestVisibleEnemy = playerVector[i];
victimRangeSq = rangeSq;
}
}
}
}
}
//---------------------------------------------------------------------------------------------
void CBossAlpha::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 *CBossAlpha::GetAttackTarget( void ) const
{
if ( m_attackTarget != NULL && m_attackTarget->IsAlive() )
{
return m_attackTarget;
}
return NULL;
}
//---------------------------------------------------------------------------------------------
void CBossAlpha::Break( void )
{
CPVSFilter filter( GetAbsOrigin() );
UserMessageBegin( filter, "BreakModel" );
WRITE_SHORT( GetModelIndex() );
WRITE_VEC3COORD( GetAbsOrigin() );
WRITE_ANGLES( GetAbsAngles() );
WRITE_SHORT( GetSkin() );
MessageEnd();
}
//---------------------------------------------------------------------------------------------
void CBossAlpha::CollectPlayersStandingOnMe( CUtlVector< CTFPlayer * > *playerVector )
{
CUtlVector< CTFPlayer * > allPlayerVector;
CollectPlayers( &allPlayerVector, TEAM_ANY, COLLECT_ONLY_LIVING_PLAYERS );
for( int i=0; i<allPlayerVector.Count(); ++i )
{
CTFPlayer *player = allPlayerVector[i];
if ( player->GetGroundEntity() == this )
{
playerVector->AddToTail( player );
}
}
}
//---------------------------------------------------------------------------------------------
void CBossAlpha::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr, CDmgAccumulator *pAccumulator )
{
// cache the trace info so we can precisely re-trace to find hitbox hits in OnTakeDamage_Alive() later
if ( ptr )
{
m_lastTraceAttackTrace = *ptr;
}
m_lastTraceAttackDir = vecDir;
BaseClass::TraceAttack( info, vecDir, ptr, pAccumulator );
}
//---------------------------------------------------------------------------------------------
// Intention interface
//---------------------------------------------------------------------------------------------
CBossAlphaIntention::CBossAlphaIntention( CBossAlpha *me ) : IIntention( me )
{
m_behavior = new Behavior< CBossAlpha >( new CBossAlphaBehavior );
}
CBossAlphaIntention::~CBossAlphaIntention()
{
delete m_behavior;
}
void CBossAlphaIntention::Reset( void )
{
delete m_behavior;
m_behavior = new Behavior< CBossAlpha >( new CBossAlphaBehavior );
}
void CBossAlphaIntention::Update( void )
{
m_behavior->Update( static_cast< CBossAlpha * >( GetBot() ), GetUpdateInterval() );
}
QueryResultType CBossAlphaIntention::IsPositionAllowed( const INextBot *meBot, const Vector &pos ) const
{
// is this a place we can be?
return ANSWER_YES;
}
//---------------------------------------------------------------------------------------------
// Locomotion interface
//---------------------------------------------------------------------------------------------
CBossAlphaLocomotion::CBossAlphaLocomotion( INextBot *bot ) : NextBotGroundLocomotion( bot )
{
CBossAlpha *me = (CBossAlpha *)GetBot()->GetEntity();
m_runSpeed = me->GetMoveSpeed();
}
//---------------------------------------------------------------------------------------------
float CBossAlphaLocomotion::GetRunSpeed( void ) const
{
CBossAlpha *me = (CBossAlpha *)GetBot()->GetEntity();
return me->IsInCondition( CBossAlpha::CHARGING ) ? 1000.0f : m_runSpeed;
}
//---------------------------------------------------------------------------------------------
// if delta Z is greater than this, we have to jump to get up
float CBossAlphaLocomotion::GetStepHeight( void ) const
{
return 18.0f;
}
//---------------------------------------------------------------------------------------------
// return maximum height of a jump
float CBossAlphaLocomotion::GetMaxJumpHeight( void ) const
{
return 18.0f;
}
//---------------------------------------------------------------------------------------------
// Vision interface
//---------------------------------------------------------------------------------------------
//---------------------------------------------------------------------------------------------
// Return true to completely ignore this entity (may not be in sight when this is called)
bool CBossAlphaVision::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